Jump to content
Fi8sVrs

GitS 2015: knockers.py (hash extension vulnerability)

Recommended Posts

  • Active Members

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).

# python2 please

import sys

import struct

import hashlib

import os

from binascii import hexlify, unhexlify

import SocketServer

import socket

try:

from fw import allow

except ImportError:

def allow(ip,port):

print 'allowing host ' + ip + ' on port ' + str(port)

PORT = 8008

g_h = hashlib.sha512

g_key = None

def generate_token(h, k, *pl):

m = struct.pack('!'+'H'*len(pl), *pl)

mac = h(k+m).digest()

return mac + m

def parse_and_verify(h, k, m):

ds = h().digest_size

if len(m) < ds:

return None

mac = m[:ds]

msg = m[ds:]

if h(k+msg).digest() != mac:

return None

port_list = []

for i in range(0,len(msg),2):

if i+1 >= len(msg):

break

port_list.append(struct.unpack_from('!H', msg, i)[0])

return port_list

class KnockersRequestHandler(SocketServer.BaseRequestHandler):

def handle(self):

global g_key

data, s = self.request

print 'Client: {} len {}'.format(self.client_address[0],len(data))

l = parse_and_verify(g_h, g_key, data)

if l is None:

print 'bad message'

else:

for p in l:

allow(self.client_address[0], p)

class KnockersServer(SocketServer.UDPServer):

address_family = socket.AF_INET6

def load_key():

global g_key

f=open('secret.txt','rb')

g_key = unhexlify(f.read())

f.close()

def main():

global g_h

global g_key

g_h = hashlib.sha512

if len(sys.argv) < 2:

print '''Usage:

--- Server ---

knockers.py setup

Generates a new secret.txt

knockers.py newtoken port [port [port ...]]

Generates a client token for the given ports

knockers.py serve

Runs the service

--- Client ---

knockers.py knock <host> <token>

Tells the server to unlock ports allowed by the given token

'''

elif sys.argv[1]=='serve':

load_key()

server = KnockersServer(('', PORT), KnockersRequestHandler)

server.serve_forever();

elif sys.argv[1]=='setup':

f = open('secret.txt','wb')

f.write(hexlify(os.urandom(16)))

f.close()

print 'wrote new secret.txt'

elif sys.argv[1]=='newtoken':

load_key()

ports = map(int,sys.argv[2:])

print hexlify(generate_token(g_h, g_key, *ports))

elif sys.argv[1]=='knock':

ai = socket.getaddrinfo(sys.argv[2],PORT,socket.AF_INET6,socket.SOCK_DGRAM)

if len(ai) < 1:

print 'could not find address: ' + sys.argv[2]

return

family, socktype, proto, canonname, sockaddr = ai[0]

s = socket.socket(family, socktype, proto)

s.sendto(unhexlify(sys.argv[3]), sockaddr)

else:

print 'unrecognized command'

if __name__ == '__main__':

main()

The vulnerability

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

[strike]
Type: whirlpool
Secret length: 16
New signature: f4440caa0da933ed497b3af8088cb78c49374853773435321c7f03730386513912fb7b165121c9d5fb0cb2b8a5958176c4abec35034c2041315bf064de26a659
New string: 0050800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000901c07[/strike]

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.

Source skullsecurity

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.



×
×
  • Create New...