Freedom Hosting 2: Password cracking

In the first post about Freedom Hosting 2, I mentioned that the customers' passwords were hashed using an unusual algorithm. In addition to research, Hyperion Gray also conducts penetration testing, and in many of our penetration testing engagements we encounter password hashes. So in this post, I'm going to bridge these two worlds–dark web research and pen testing–to flesh out what that hash algorithm looks like and how to crack them.

This post assumes that you know what password hashes are and how password cracking basically works. Here is a quick primer from Wired magazine if you feel like reading up on the topic.

Hash construction

In the first post, I mentioned that the _fhosting database contains all the administrative and operational databases for FH2. Inside this database, a domains table contains the master records for all of the customers' sites, each site has a corresponding username and password hash:

MariaDB [_fhosting]> select user, password from domains limit 5;
+--------------+------------------------------------------+
| user         | password                                 |
+--------------+------------------------------------------+
| supercoolbro | 9d828eedbdd00eb6d1f09e8e5fb9588d85befee1 |
| kangpin      | f4e25f225ccd6f89896c4078dc5132073a3c9799 |
| miacut       | 90625abe2d3f8fc1484bd9574d397f9d3c67adb4 |
| wilandre     | 43d7e9d159d8edd20169675c95d972ce92252490 |
| heyjude      | a31f0d1544dedb50738a9d564b2ff06f59032b5d |
+--------------+------------------------------------------+
5 rows in set (0.00 sec)

Each password hashes is 40 hexademical characters, which immediately suggests that they might be SHA-1 hashes. Therefore, I wanted to try running them through a password cracker. Here's a protip: export the hashes from MySQL directly into a format that JohnTheRipper or Hashcat can read:

MariaDB [_fhosting]> select concat(user, ':', password) from domains into outfile '/data/fhosting.hash';
    Query OK, 15356 rows affected (0.02 sec)

This produces a file formatted with username:hash that is easy to run through a password cracker:

$ head -n 5 /data/fhosting.hash
supercoolbro:9d828eedbdd00eb6d1f09e8e5fb9588d85befee1
kangpin:f4e25f225ccd6f89896c4078dc5132073a3c9799
miacut:90625abe2d3f8fc1484bd9574d397f9d3c67adb4
wilandre:43d7e9d159d8edd20169675c95d972ce92252490
heyjude:a31f0d1544dedb50738a9d564b2ff06f59032b5d

I tried running this file against the well-known rock-you.txt password list, but I didn't get any matches! Either these 15k dark web users all picked good passwords, or else these are not bare SHA-1 hashes. The latter is far more likely, but I wanted to see if I could find anything else in the dump that was more conclusive.

For the most part, the FH2 file system dump was uninteresting, but it did contain some of the code that powered the site. I searched through that code for something that touched the password column in the domains table, and came across this PHP function:

function myPassword($password, $salt) {
 $magic = 'Vw-nw9.WgmTUrppJw12s';
 return sha1(md5(sha1($salt.$password.$magic)));
}

This function is used to create the password hash when registering an account, and it is also used to verify a password when logging in. The function shows that the hash construction has 3 rounds: SHA-1, then MD5, then SHA-1 again! It also uses a constant string and the lowercase username (not shown here) as salts. Hashcat doesn't have a cracking mode for this unusual construction.

Write a cracking script

Since hashcat doesn't have a built-in mode for this homebrew hash construction, I whipped up a Python script.

def make_hash(username, password):
    username = username.lower()
    key = (username + password + 'Vw-nw9.WgmTUrppJw12s').encode('ascii',
        errors='ignore')
    sha1 = hashlib.sha1()
    sha1.update(key)
    md5 = hashlib.md5()
    md5.update(sha1.hexdigest().encode('ascii'))
    sha1 = hashlib.sha1()
    sha1.update(md5.hexdigest().encode('ascii'))
    return sha1.hexdigest()

This Python snippet just does the same thing as the PHP code above. I won't bother showing all the code here, but basically it just checks every user against every word in the rock-you wordlist. I also scripted a progress bar so I could see how quickly the cracking was going. I started it running, took a break to eat, and came back to...

python cracker progress

After running for an hour and fourteen minutes, the script was still at 0% progress! It was only checking 7 words from the wordlist per second. This wordlist has 14,000,000 items, so I was looking at over 500 hours of cracking time!

Hurry up!

I knew I would need to speed up the cracking, but how? I'm not a C programmer; I'm also not a GPU programmer; and I just know that hashcat must be filled with lots of arcane black magic that I will never understand, so it's not feasible for me to add support for this hash format to hashcat.

JohnTheRipper, on the other hand, does support a bunch of unusual hash constructions. I listed all of the constructions that contain a SHA-1 round.

mhaase@paddyspub ~> john --list=subformats | grep sha1
Format = dynamic_22  type = dynamic_22: md5(sha1($p))
Format = dynamic_23  type = dynamic_23: sha1(md5($p))
Format = dynamic_24  type = dynamic_24: sha1($p.$s)
Format = dynamic_25  type = dynamic_25: sha1($s.$p)
Format = dynamic_26  type = dynamic_26: sha1($p) raw-sha1
Format = dynamic_35  type = dynamic_35: sha1(uc($u).:.$p) (ManGOS)
Format = dynamic_36  type = dynamic_36: sha1($u.:.$p) (ManGOS2)
Format = dynamic_37  type = dynamic_37: sha1(lc($u).$p) (SMF)
Format = dynamic_38  type = dynamic_38: sha1($s.sha1($s.sha1($p))) (Wolt3BB)
...

Some of these are close to what I need, i.e. md5(sha1($pass)) or sha1(md5($pass)), but none of them contain the 3 rounds that I need. Whats more, none of them contains the unusual salt construction involving username and a constant string.

Fortunately, JohnTheRipper has a feature called "dynamic" hashes, which allow you to "program" your own hash constructions using a declarative—albeit confusing and awkwardly documented—wrapper to the C API. Custom hash constructions are defined in dynamic.conf (or several other places along a search path). This was extremely tedious to write and debug, but I eventually figured out how to do it:

1292 ################################################
1293 # Dynamic type for Freedom Hosting 2: sha1(md5(sha1($u.$p)))
1294 ################################################
1295 [List.Generic:dynamic_3000]
1296 Expression=sha1(md5(sha1($u.$p.MAGIC))) (Freedom Hosting 2)
1297 Flag=MGF_USERNAME_LOCASE
1298 Flag=MGF_INPUT_20_BYTE
1299 Flag=MGF_StartInX86Mode
1300 SaltLen=-24
1301 CONST1=Vw-nw9.WgmTUrppJw12s
1302 Func=DynamicFunc__clean_input
1303 Func=DynamicFunc__append_userid
1304 Func=DynamicFunc__append_keys
1305 Func=DynamicFunc__append_input1_from_CONST1
1306 Func=DynamicFunc__SHA1_crypt_input1_overwrite_input1
1307 Func=DynamicFunc__X86toSSE_switch_input1
1308 Func=DynamicFunc__crypt_md5_in1_to_out2
1309 Func=DynamicFunc__SSEtoX86_switch_output2
1310 Func=DynamicFunc__clean_input
1311 Func=DynamicFunc__append_from_last_output2_to_input1_as_base16
1312 Func=DynamicFunc__SHA1_crypt_input1_to_output1_FINAL
1313 Test=$dynamic_3000$297cf0cb6b0365bb2eb6d34a334a46cc1df1ed1e:Password1:john
1314 Test=$dynamic_3000$6fc9e45053b697a835a7fdbd879fcf9e2e4f709f:asdf1234:jane
1315 Test=$dynamic_3000$7423e0aacfe66bc46b544680a43e934db6281403:iloveyou:fido

I won't go into too much detail about how this works, except to point out that you give your construction a number (I chose 3,000) and name it accordingly, i.e. dynamic_3000. You also have to add some test cases that have sample hashes as well as the password that was used to generate them. Then you're done: there's no compilation step or anything like that. With my code and test cases in place, I ran a benchmark:

$ /opt/john-the-ripper/run/john --test --format=dynamic_3000
Benchmarking: dynamic_3000 [sha1(md5(sha1($u.$p.MAGIC) (Freedom Hosting 2) 128/128 AVX 4x1]... DONE
Many salts: 4258K c/s real, 4258K c/s virtual
Only one salt:  3576K c/s real, 3541K c/s virtual

The test mode verifies that the test cases work (somewhat helpful for debugging) and also measures how fast the construction runs. The benchmark here is not super helpful, the c/s unit means crypts per second, i.e. individual rounds of MD5 or SHA-1, and I found that in practice this benchmark drastically overstated the speed of this cipher.

When using a custom hash construction, John requires the hashes to be formatted specially, where the construction name is included with the hash. Therefore, I reformatted my hashes file to look like this:

supercoolbro:$dynamic_3000$9d828eedbdd00eb6d1f09e8e5fb9588d85befee1
kangpin:$dynamic_3000$f4e25f225ccd6f89896c4078dc5132073a3c9799
miacut:$dynamic_3000$90625abe2d3f8fc1484bd9574d397f9d3c67adb4
wilandre:$dynamic_3000$43d7e9d159d8edd20169675c95d972ce92252490
heyjude:$dynamic_3000$a31f0d1544dedb50738a9d564b2ff06f59032b5d

Finally, I am ready to start cracking... Dynamic formats aren't parallelized automatically the way some of the built-in formats are, so I used the --fork argument to run this on 8 CPUs instead.

$ time /opt/john-the-ripper/run/john --fork=8 --format=dynamic_3000 --wordlist=/usr/share/wordlist/rockyou.txt hashes.txt
Using default input encoding: UTF-8
Loaded 14809 password hashes with 14622 different salts (dynamic_3000 [sha1(md5(sha1($u.$p.MAGIC) (Freedom Hosting 2) 128/128 AVX 4x1])
Node numbers 1-8 of 8 (fork)
Press 'q' or Ctrl-C to abort, almost any other key for status
Password1        (123456)
Password1        (leeecp)
Password1        (plokij)
Passw0rd         (puccap)
Password1        (abc1234)
Password1        (abqdude)
...
Session completed
79424.71user 86.40system 2:54:33elapsed 759%CPU (0avgtext+0avgdata 209528maxresident)k
0inputs+13344outputs (0major+379948minor)pagefaults 0swaps

I've only shown a bit of the output here, but running through the wordlist ended up taking almost 3 hours, which is something like 1,300 candidate words per second. Although this is slow compared to most of John's built-in formats, this is much faster than the Python script's 7 candidates per second. I managed to crack 797 passwords (out of ~15k) using the rock-you wordlist.

Conclusion

I am always amazed by how flexible John The Ripper is. This was the first time I ever created a dynamic format completely from scratch, and although it was a bit mind-bending, it was certainly a lot easier than learning John's internal C API and writing a bunch of C code or OpenCL kernels or whatever, and the performance wasn't great–but it was good enough.