As many of you know, last weekend was Ghost in the Shellcode 2015! There were plenty of fun challenges, and as always I had a great time competing! This will be my first of four writeups, and will be pretty simple (since it simply required me to use a tool that already exists (and that I wrote :) ))
The level was called "knockers". It's a simple python script that listens on an IPv6 UDP port and, if it gets an appropriately signed request, opens one or more other ports. The specific challenge gave you a signed token to open port 80, and challenged you to open up port 7175. The service itself listened on port 8008 ("BOOB", to go with the "knockers" name :) ).
You can download the original level here (Python).
To track down the vulnerability, let's have a look at the signature algorithm:
def generate_token(h, k, *pl): m = struct.pack('!'+'H'*len(pl), *pl) mac = h(k+m).digest() return mac + m
In that function, h is a hash function (sha-512, specifically), k is a random 16-byte token, randomly generated, and m is an array of 16-bit representation of the ports that the user wishes to open. So if the user wanted to open port 1 and 2, they'd send "\x00\x01\x00\x02", along with the appropriate token (which the server administrator would have to create/send, see below).
Hmm... it's generating a mac-protected token and string by concatenating strings and hashing them? If you've followed my blog, this might sound very familiar! This is a pure hash extension vulnerability!
I'm not going to re-iterate what a hash extension vulnerability is in great detail—if you're interested, check out the blog I just linked—but the general idea is that if you generate a message in the form of msg + H(secret + msg), the user can arbitrarily extend the message and generate a new signature! That means if we have access to any port, we have access to every port!
Let's see how!
Generating a legit token
To use the python script linked above, first run 'setup':
$ python ./knockers.py setup wrote new secret.txt
Which generates a new secret. The secret is just a 16-byte random string that's stored on the server. We don't really need to know what the secret is, but for the curious, if you want to follow along and verify your numbers against mine, it's:
$ cat secret.txt 2b396fb91a76307ce31ef7236e7fd3df
Now we use the tool (on the same host as the secret.txt file) to generate a token that allows access on port 80:
$ python ./knockers.py newtoken 80 83a98996f0acb4ad74708447b303c081c86d0dc26822f4014abbf4adcbc4d009fbd8397aad82618a6d45de8d944d384542072d7a0f0cdb76b51e512d88de3eb20050
Notice the first 512 bits (64 bytes) is the signature—which is logical, since it's sha512—and the last 16 bits (2 bytes) are 0050, which is the hex representation of 80. We'll split those apart later, when we run hash_extender, but for now let's make sure the token actually works first!
We start the server:
$ python ./knockers.py serve
And in another window, or on another host if you prefer, send the generated token:
$ python ./knockers.py knock localhost 83a98996f0acb4ad74708447b303c081c86d0dc26822f4014abbf4adcbc4d009fbd8397aad82618a6d45de8d944d384542072d7a0f0cdb76b51e512d88de3eb20050
In the original window, you'll see that it was successful:
$ python ./knockers.py serve Client: ::1 len 66 allowing host ::1 on port 80
Now, let's figure out how to create a token for port 7175!
Generating an illegit (non-legit?) token
So this is actually the easiest part. It turns out that the awesome guy who wrote hash_extender (just kidding, he's not awesome) built in everything you needed for this attack!
Download and compile hash_extender if needed (definitely works on Linux, but I haven't tested on any other platforms—testers are welcome!), and run it with no arguments to get the help dump. You need to pass in the original data (that's "\x00\x80"), the data you want to append (7175 => "\x1c\x07"), the original signature, and the length of the secret (which is 16 bytes). You also need to pass in the types for each of the parameters ("hex") in case the defaults don't match (in this case, they don't—the appended data is assumed to be raw).
All said and done, here's the command:
./hash_extender --data-format hex --data 0050 \ --signature-format hex --signature 83a98996f0acb4ad74708447b303c081c86d0dc26822f4014abbf4adcbc4d009fbd8397aad82618a6d45de8d944d384542072d7a0f0cdb76b51e512d88de3eb2 \ --append "1c07" --append-format hex \ -l 16
You can pass in the algorithm and the desired output format as well, if we don't, it'll just output in every 512-bit-sized hash type. The output defaults to hex, so we're happy with that.
$ ./hash_extender --data-format hex --data 0050 --signature-format hex --signature 83a98996f0acb4ad74708447b303c081c86d0dc26822f4014abbf4adcbc4d009fbd8397aad82618a6d45de8d944d384542072d7a0f0cdb76b51e512d88de3eb2 --append "1c07" --append-format hex -l 16 Type: sha512 Secret length: 16 New signature: 4bda887c0fc43636f39ff38be6d592c2830723197b93174b04d0115d28f0d5e4df650f7c48d64f7ca26ef94c3387f0ca3bf606184c4524600557c7de36f1d894 New string: 005080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000901c07
Type: whirlpool Secret length: 16 New signature: f4440caa0da933ed497b3af8088cb78c49374853773435321c7f03730386513912fb7b165121c9d5fb0cb2b8a5958176c4abec35034c2041315bf064de26a659 New string: 0050800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000901c07
Ignoring the whirlpool token, since that's the wrong algorithm, we now have a new signature and a new string. We can just concatenate them together and use the built-in client to use them:
$ python ./knockers.py knock localhost 4bda887c0fc43636f39ff38be6d592c2830723197b93174b04d0115d28f0d5e4df650f7c48d64f7ca26ef94c3387f0ca3bf606184c4524600557c7de36f1d894005080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000901c07
And checking our server, we see a ton of output, including successfully opening port 7175:
$ python ./knockers.py serve Client: ::1 len 66 allowing host ::1 on port 80 Client: ::1 len 178 allowing host ::1 on port 80 allowing host ::1 on port 32768 allowing host ::1 on port 0 allowing host ::1 on port 0 [...repeated like 100 times...] allowing host ::1 on port 0 allowing host ::1 on port 0 allowing host ::1 on port 144 allowing host ::1 on port 7175
And that's it! At that point, you can visit http://knockers.2015.ghostintheshellcode.com:7175 and get the key.
This is a pure hash extension vulnerability, which required no special preparation—the hash_extender tool, as written, was perfect for the job!
My favourite part of that is looking at my traffic graph on github:
It makes me so happy when people use my tool, it really does!