Table of contents
Introduction
MIT released their puzzles for HackMIT. The top 50 on the leaderboard got guaranteed admission to the hackathon, so that was a great motivation to work solve some puzzles.
Accessing Puzzles
The clue for the first puzzle was wOOfwoOf wOOfwooF wOOfWOof wOOfWOof wOOfWOOF wOOfWOOF wOOfWOOf wOOFwoOF
. I converted each of the characters to numbers initially to spot differences in the lines. The solution ended up just being a conversion to binary. I converted the lowercase letters to 0 and uppercase letters to 1. After throwing it into an online converter, I got balloons
.
With the answer, I clicked on the balloons on the HackMIT website and got access to the puzzle website.
Gaslight (750 points)
I tried convincing Nate with many methods. One that got me close was pretending to be another HackMIT organizer and saying that I lost my computer with the flag. I ended up convincing Nate to give me a word similar to the flag which was “Great Dome.” Unfortunately, this wasn’t the right flag.
I ended up convincing Nate to give me the flag by making me think we were partners that shared the flag.
Xd (1000 points)
I managed to brute force the first puzzle by manually going through 471 out of the 511 directories to find the flag. I tracked all my paths with an Excel sheet, and went through the remaining directories to help find the pattern.
First, I looked at when the directories branched. I noticed that there were always a fixed difference when moving onto the next directory (+/- 1, 31). I couldn’t think of an algorithm since the forks seemed to happen at random intervals.
I decided to manually go through 500 directories of the second puzzle to find patterns.
After a few days, I decided to look at the difference between each of the directories in the path. I noticed that the paths also differed by the same amount (+/- 1, 31). They also did this in pairs, so if it subtracted one, the directory after must also subtract one. Puzzle two had similar behaviour with (+/- 1, 5, 25, 125, 15625, 78125, 390625, 1953125).
With this knowledge, I created a recrusive program to go backwards from the final directory to the first. Initially, I kept getting stack overflow errors.
I copied the final complete 2D array before the error, and set it as input before running the program again. After two runs, the program couldn’t find any paths.
I decided to test the program on the first puzzle and it was able to determine the correct path. In hindsight, I should’ve tested it on this before doing the second.
I noticed that the possible differences were powers of 5, but both 625 and 3125 were never included. After adding those numbers, I ran the program again from the final number. To mitigate the stack overflows, I did node index --stack_size=3125
to increase the stack size.
After repeating pasting the final 2D array many times, the program finally spit out a directory. With this, I was able to solve Xd.
HackVM (1550 points)
The puzzle had two files, vm and puzzle1. The puzzle involved finding a passphrase matching /hack{[A-Za-z0-9_]+}/
to get the flag. Another hint mentioned that memcmp is inecsure.
Googling “memcmp ctf” showed many results. An article by X41 showed how it was possible to see the timing differences between correct and incorrect bytes. The difference is in nanoseconds and I never coded in C or shell, so I opted to look for another solution.
I found 0XCD4’s post about another CTF. They used The GNU Project Debugger to disassemble the functions. In my Linux VM, I ran sudo apt install gdb
and tried anaylzing the functions of both the files.
Unfortunately, the files required interacting with each other. I tried running the program with sudo ./vm puzzle1
and then doing ps ax
to get the PID of the program. I tried running gdb with the pid, but when I did info functions
, I didn’t get any functions.
I left this puzzle as I felt I was closer in completing Hackscope.
Hackscope (2300 points)
Examining the index.js
Hackscope required finding the correct string for the third exam question. I downloaded the index.js
file from the website and examined it for hints. I found the answers from the first two questions in plain text, but the third one was a variable s
.
I tried tracing the variable, but ended up getting lost on line 13498 with useState.
With some more searching, I found the async function b2
on line 13182 that retrieved answers from a GET request.
I used Postman to sent a get request to the endpoint using my access_token
from my browser cookie. Unfortunately, I couldn’t get access since I wasn’t admin.
I needed a way to modify my access token so that the server would believe I had the appropriate permissions.
I used a JWT Decoder to decode my access_token
. I noticed my role was user, so I likely needed to change it to admin. I tried that and re-encoding, but I ended up getting an error that Token was not signed or formatted properly!
.
This made sense since I didn’t have a public or private key required for the RS256 algorithm.
Finding Vulnerabilities
None Algorithm
I replaced the algorithm header in the JWT with NONE
as in Sajan Dgakate’s Medium article, but it didn’t work.
Unchecked Signature
I deleted the signature as in Sajan Dgakate’s Medium article, but the website seemed to check the signature of JWTs.
RS256 to HS256
If the server accepted both RS256 and HS256 algorithms, it could be possible to convert to HS256 with a modified payload. I attempted the method from this YouTube video, but the website wouldn’t send back the public key needed to re-encode.
Next, I followed the method by Okhomiak posted on Habr.
I installed OpenSSL using brew install openssl@3
.
I then tried connecting with openssl s_client -connect hackscope.hackxgpt.com:443
and the rest of the commands, but I couldn’t get it working. I tried a solution from this forum, and got a public key. openssl s_client -connect hackscope.hackxgpt.com:443 | openssl x509 -pubkey -noout
After re-encoding with this public key, I the same error that the Token was not signed or formatted properly!
.
A possible issue was that I extracted the wrong public key, so I tried a solution from PortSwigger. Apparently it was possible to determine the public key using two RSA tokens.
I did docker run --rm -it portswigger/sig2n <token1> <token2>
with two tokens and ended up with two public keys. I tried re-encoding with both of them, but got the same error.
Finally, I tried using Burp as demonstrated in Sajan Dgakate’s Medium article, but it yielded the same result.
KID injection
The Inviciti article had information about multiple attacks including KID. It talked about directory traversal and SQL injections.
It could be possible to do a SQL injection via the kid
header. I tried replacing the kid
with "' or 1=1"
signed with my private and public keys from earlier, but I still got an error Token was not signed or formatted properly!
.
I read a slide deck from PentesterLab. It mentioned open redirect and header injection as possible exploits. RedHunt Labs also had information on getting through URL filters including, chaining with open redirect, SSRF, and header injection. These were the same as the Inviciti article.
I wasn’t that familiar with injection attacks and the Bakery puzzle likely used one. The solution likely wasn’t via this method.
jku Misuse
In the header of the web tokens under jku was http://127.0.0.1:4000/.well-known/jwks.json
. I replaced the local host with the website and got access to the public keys.
I found this article about jku misuse.
I saved the website’s public keys locally from https://hackscope.hackxgpt.com/.well-known/jwks.json
.
I then generated a new public-private key pair:
openssl genrsa -out keypair.pem 2048
openssl rsa -in keypair.pem -pubout -out publickey.crt
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in keypair.pem -out pkcs8.key
Using auth0’s JWT.io, I pasted my access_token
and used pkcs8.key
as the private key and publickey.crt
as the public key. I also changed my role to admin
.
I downloaded their Python script and tried running it with python getPublicParams.py
. I didn’t have the module so I tried both pip install crypto
and pip install pycrypto
. I ended up uninstalling them as it didn’t work. As per a suggestion from Stack Overflow I tried pip install pycryptodome
.
I then copied the n and e values without the first two characters (0x) and pasted it into the jwks.json
from the website. I hosted the modified jwks.json
on my CDN and changed the jku
in the header of the JWT. My signature was verified, so I tried this new access token, but I got JKU specified not allowed; wrong prefix.
.
This article from System Weakness talks about multiple vulnerabilities. I tried the open redirect by adding ../redirect/?url=<MY_CDN>
but that didn’t work. It seemed that http://127.0.0.1:4000/.well-known/jwks.json
had to be in front no matter what. SecNigma also had a similar redirect.
This video from nu11 secur1ty bypassed the JKU via header injection utilizing PortSwigger’s lab. This one didn’t fix the issue with the prefix so I moved onto another resource.
I tried replacing the localhost with IP address formats from here but I kept getting JKU specified not allowed; wrong prefix.
I found another resource about JKU header injection, but the URL whitelist wouldn’t work with the method.
I used some of the suggestions from PortSwigger. This resulted in Token was not formatted properly!
or Token was not signed or formatted properly!
. I tried some of the open redirects from fastly, but these also didn’t work. HackTricks had a longer list of open redirects as well as URL bypasses which I tried but didn’t work.
I noticed that http://hackscope.hackxgpt.com/.well-known/jwks.json/../../cdn.com/jwks.json
would result in the url shortening to https://hackscope.hackxgpt.com/cdn.com/jwks.json
. If I could find a way to remove the trailing slash, I could use @ to redirect to my domain. Unfortunately, I couldn’t find anything, so I tried looking for another vulnerability.
I noticed in the index.js
that there was a comment reading <!-- TODO(hackX): update APIFlask documentation -->
. There was likely a vulnerability in APIFlask with redirects. I searched through the Flask and APIFlask documentation, but I wasn’t able to find anything.
Unfortunately I wasn’t able to solve this puzzle before the deadline.
Analysis of Hackscope Solution
The comment in the page’s html, <!-- TODO(hackX): update APIFlask documentation -->
, was a hint to check the documentation for the website.
This was accessed via https://hackscope.hackxgpt.com/docs.
The documentation reveals the load
endpoint, which loads external resources. By getting the access_token
and inputting it into JWT.io, I can forge the token.
flocto shared their Python program that they used to generate their jwk file.
They generated an RSA key-pair and wrote the private key into a file. They extracted the public key’s e and n values and converted it into base64.
After running the program, a jwks.json
and private.pem
file were created. I hosted the JSON file on a CDN so the server could access it (I used Discord as it was quick and easy). I copied my access_token
from the website cookies, and inputted it into JWT.io.
I redirected the jku to my CDN using /../../load/
. I changed the kid to match the JSON file. Since I needed admin permissions, I changed my role to admin
. Finally, I copied the private key from the private.pen
file and pasted it into the VERIFY SIGNATURE
section. With that, the forged token was complete.
I replaced my access_token
on the website and refreshed. With that, I was able to get the flag.
I was close to solving this puzzle. Unfortunately, without discovering the load endpoint, I wasn’t able to complete it.
Conclusion
In the end, I finished Gaslight and Xd. I finished 63rd on the leaderboard, but fortunately, I was still admitted based on the admission puzzle. I’ve never done a Capture the Flag before and this was a great experience. I was able to learn about different vulnerabilities and even solved a few puzzles. The software and knowledge I gained from this will prove useful in future Capture the Flags.