Mini WebSocket CTF
During the holidays, @stackfault (sysop from the BottomlessAbyss BBS) ran a month long CTF with challenges being released every couple of days. Some of challenges were unsolved or partially solved challenges from earlier HackFest editions as well as some new ones. There was also a point depreciation system in place so challenges solved earlier gave more points. This post is a writeup for the Evilconneck challenge by @pathetiq, a quick but fun challenge with websockets and a bit of crypto.
To start with, we have to connect to the BBS and create an account in order to access the challenge description and flag submission panel. We’re given a URL to connect to as well as a bit more information on the other screen.
On the webpage, there’s not much to see: just a couple of images and a few messages about no vulnerabilities present on a static site.
Checking the HTML source code, we can see that there are some comments about debug mode being turned off.
Debug mode enabled
Enabling the “debug mode” is just matter of sending a GET with the debug variable set: http://18.222.220.65/evilconneck/?debug=1
After enabling the debug mode we can see additional javascript being inserted in the page:
The code is supposed to establish a websocket connection but I’m getting a connection error message when looking at my Firefox console:
Doing some debugging, I found that the Origin header isn’t accepted by the server.
GET / HTTP/1.1
Host: 18.222.220.65:64480
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Sec-WebSocket-Version: 13
Origin: http://18.222.220.65
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Key: 31EYkQq62lBuLoMotKrWZw==
Connection: keep-alive, Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
HTTP/1.1 403 Forbidden
Date: Tue, 28 Jan 2020 02:25:38 GMT
Server: Python/3.6 websockets/8.1
Content-Length: 84
Content-Type: text/plain
Connection: close
Failed to open a WebSocket connection: invalid Origin header: http://18.222.220.65.
This will probably work by using a hostname intead of the IP address so I added evilconneck
to my local hostfile and I was able to successfully connect after using the hostname.
Within Burp I can see my client is sending the uptime
string followed by some base64 encoded data.
And the response is always 49170.12
and never changes
Figuring out the HMAC secret key
The first thing I did next was try to figure what that base64 content is all about and it just decodes to meaningless bytes. Interestingly, the length of the output is 32 bytes so this could be a hash of some sort.
I tried changing the uptime
message to something else or alter the content of the base64 and I got the following signature failure message everytime: Signature failed! - Expected: 'b64(b85(passwd)),base64(hmac256)'
So it’s probably safe to assume at this point that uptime
is the message for which the signature is calculated. Based on the error message, we know that it’s using HMAC with SHA256 for the hash function and that HMAC secret is base85 encoded, then base64 encoded.
To test this theory, I used Python to try to generate the HMAC for the uptime
message using a wordlist so I can bruteforce the HMAC secret key. At first, I made the following script but I wasn’t able to find a match with any wordlist I used, such as rockyou.txt
:
#!/usr/bin/env python3
import base64
import hashlib, hmac
import progressbar
import sys
c = b"Ji/HQqLPH5KpqzZcYFXRdEHnyn2VI1fqU824IzTzAKs="
progressbar.streams.flush()
with open(sys.argv[1], encoding = "ISO-8859-1") as f:
passwords = f.read().splitlines()
message = sys.argv[2]
with progressbar.ProgressBar(max_value=len(passwords)) as bar:
for i, p in enumerate(passwords):
secret = base64.b64encode(base64.b85encode(p.strip().encode('utf-8')))
m = hmac.new(secret, digestmod=hashlib.sha256)
m.update(message.strip().encode('utf-8'))
m_b64 = base64.b64encode(m.digest())
if c in m_b64:
print(f"Found password: {p}")
print(m_b64)
sys.exit(0)
bar.update(i)
After double checking my code for a bit, I saw that there’s an option to enable padding before encoding in the b85encode
function. This pads the input with null bytes so to make the length a multiple of 4 bytes. After enabling padding I was able to find the HMAC secret.
secret = base64.b64encode(base64.b85encode(p.strip().encode('utf-8'), pad=True))
No need for the full rockyou.txt list afterall, this one is pretty simple: secret
Getting access with websockets
Next, I rewrote my script to send commands through the websocket connection with the proper HMAC appended.
#!/usr/bin/env python3
import asyncio
import base64
import hashlib, hmac
import readline
import sys
import websockets
secret = b'secret'
secret = base64.b64encode(base64.b85encode(secret, pad=True))
async def hello():
uri = "ws://evilconneck:64480"
async with websockets.connect(uri, origin='http://evilconneck') as websocket:
cmd = input('> ')
m = hmac.new(secret, digestmod=hashlib.sha256)
m.update(cmd.encode('utf-8'))
m_b64 = base64.b64encode(m.digest())
x = f"{cmd},{m_b64.decode('utf-8')}"
await websocket.send(x)
r = await websocket.recv()
print(f"{r.decode('utf-8')}")
while True:
asyncio.get_event_loop().run_until_complete(hello())
Once I found the help
command, I was quickly able to get the flag. I just needed to issue hello
command as instructed then getflag
returned out the flag.