Knocker - Crypto 150 GiS Writeup
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
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.