I could not participate in GiS CTF which was conducted around 10 days back. But, the one good thing about these people is that they always have an archive of all their past events.

This is a 150 point Crypto challenge named as Knockers. Here is the question.

Dude, here’s a knocker token that will let you access my service on port 80. One day I will let you see my cool stuff on port 7175.

Your token: 6e248dc3dec420c42f48f720475f9cb70f2485d8214715c66106050fd1a7b687326b2c82114419474042b

b5df1602d578c059ba5dac260f644b8584dd5a0a38b0050

Server: knockers.2015.ghostintheshellcode.com

We have a token which allows us to access port 80 of the above address (currently down). We submit the token to the Knockers service which in turn validates the token and modifies the firewall rules and grants access to the valid ports.

Our aim here is to make a request with a valid MAC so that we will be able to access port 7175 and obtain the flag.

You can look at the source code of the Knockers service below.

# 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()

We can see how the service generates and authenticates the tokens. The token itself consists of two parts. The first 64 bytes (or 128 characters when hex-encoded as string) consists of the MAC and the remaining part of the string consists of the message. To verify the integrity of the message, it takes the message part, concatenates with the key and computes SHA-512 and checks if the computed value matches with the given MAC.

So, the entire challenge now boils down to getting the secret key. I mean, not exactly. But, we will come to that. My first intuition was to check for weak randomness. But, as we can see the secret key is obtained from os.urandom() which harvests the key from /dev/urandom. The key is completely random and now there is no way to guess it.

Next comes the key length. It is 16 bytes. That is 16 * 8 = 128 bits, which means 2^128 combinations. So, brute-forcing the key with the help of plaintext and the resultant MAC is also out of the question.

Then, something else caught my eye. MAC is computed by mac = h(k+m).digest(). This is vulnerable to length extension attacks. Just when I got excited and was about to do something as naive as starting to reinvent the wheel, I found that there is already a tool available called HashPump which exploits length extension attacks. It is coded in C++ but, has got Python bindings too (Yay!). So, the exploit code here.

#!/usr/bin/env python

#knocked_out.py
   
from hashpumpy import hashpump
from hashlib import sha512
import struct

orig_token = "bcd49f2cd21d67043beb89cce85a753420a0646f5712a  
d3b4df23d8b9213c2f7117c9ed572491b5768d408b2757f911455a5023f  
9ad03e91cf1a960016ed59050050"

mac = orig_token[:128]
msg = orig_token[128:].decode('hex')

port = [7175]
data = struct.pack('!H', *port)
k_len = 16

new_mac, new_msg = hashpump(mac, msg, data, k_len)

print new_mac + new_msg.encode('hex')

The result is

465adf335fd067ab0b3baeda2279b9c2ee9dcee386dab30c25d8ce937fa584e1bc9f  
f459e9846ad873ca47190eb48da976afa4b3ba5b73fa6341d929936adc9000508000  
00000000000000000000000000000000000000000000000000000000000000000000  
00000000000000000000000000000000000000000000000000000000000000000000  
00000000000000000000000000000000000000000000000000000000000000000000  
0000000000901c07

Feeding this to the knocker service we get,

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
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 0
allowing host ::1 on port 144
allowing host ::1 on port 7175

The firewall rules are now modded and we can access the service at port 7175 and obtain our flag.

Note: I am working on a token I previously generated in my system to demonstrate the attack.