Active Members Fi8sVrs Posted May 1, 2015 Active Members Report Posted May 1, 2015 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 pleaseimport sysimport structimport hashlibimport osfrom binascii import hexlify, unhexlifyimport SocketServerimport sockettry: from fw import allowexcept ImportError: def allow(ip,port): print 'allowing host ' + ip + ' on port ' + str(port)PORT = 8008g_h = hashlib.sha512g_key = Nonedef generate_token(h, k, *pl): m = struct.pack('!'+'H'*len(pl), *pl) mac = h(k+m).digest() return mac + mdef 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_listclass 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_INET6def 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.txtknockers.py newtoken port [port [port ...]] Generates a client token for the given portsknockers.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 vulnerabilityTo 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 + mIn 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 tokenTo use the python script linked above, first run 'setup':$ python ./knockers.py setupwrote new secret.txtWhich 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.txt2b396fb91a76307ce31ef7236e7fd3dfNow 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 8083a98996f0acb4ad74708447b303c081c86d0dc26822f4014abbf4adcbc4d009fbd8397aad82618a6d45de8d944d384542072d7a0f0cdb76b51e512d88de3eb20050Notice 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 serveAnd in another window, or on another host if you prefer, send the generated token:$ python ./knockers.py knock localhost 83a98996f0acb4ad74708447b303c081c86d0dc26822f4014abbf4adcbc4d009fbd8397aad82618a6d45de8d944d384542072d7a0f0cdb76b51e512d88de3eb20050In the original window, you'll see that it was successful:$ python ./knockers.py serveClient: ::1 len 66allowing host ::1 on port 80Now, let's figure out how to create a token for port 7175!Generating an illegit (non-legit?) tokenSo 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 16You 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 16Type: sha512Secret length: 16New signature: 4bda887c0fc43636f39ff38be6d592c2830723197b93174b04d0115d28f0d5e4df650f7c48d64f7ca26ef94c3387f0ca3bf606184c4524600557c7de36f1d894New string: 005080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000901c07[strike]Type: whirlpoolSecret length: 16New signature: f4440caa0da933ed497b3af8088cb78c49374853773435321c7f03730386513912fb7b165121c9d5fb0cb2b8a5958176c4abec35034c2041315bf064de26a659New 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 4bda887c0fc43636f39ff38be6d592c2830723197b93174b04d0115d28f0d5e4df650f7c48d64f7ca26ef94c3387f0ca3bf606184c4524600557c7de36f1d894005080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000901c07And checking our server, we see a ton of output, including successfully opening port 7175:$ python ./knockers.py serveClient: ::1 len 66allowing host ::1 on port 80Client: ::1 len 178allowing host ::1 on port 80allowing host ::1 on port 32768allowing host ::1 on port 0allowing host ::1 on port 0[...repeated like 100 times...]allowing host ::1 on port 0allowing host ::1 on port 0allowing host ::1 on port 144allowing host ::1 on port 7175And that's it! At that point, you can visit http://knockers.2015.ghostintheshellcode.com:7175 and get the key.Source skullsecurity Quote