These are the writeups for most of the Reverse and Web challenges of the 2021 edition of the Cybersecurity Challenge Portugal.
Doge - 100 pts
In this challenge we were given a simple website that only contained a number input field (and a beautiful doge image). Once we submit a valid number it redirects us to a blank page that reflects our input.
My first instinct was to test for XSS or SSTI but as I examined the response headers I saw that most of the XSS protection headers were set.
On to SSTI.
At this point I needed a bit more information on the infrastructure that the website is running on.
So I crashed it.
If we send a broken CSRF token it shows us a Ruby on Rails error page.
When I sent
<% 7*7 %> the input didn’t reflect itself. Bingo! After testing some more payloads with the
system function I eventually ended up using
<% p `cat flag.txt` %> . Unfortunately it was too late and ended up solving it 2 minutes after the CTF ended. Sad.
If you want to learn more about Ruby on Rails SSTI please refer to this post.
Censos - 200 pts
We are given an unfinished webapp that Zé wants us to test. It uses the
flask_cors package to configure CORS and it has a rather interesting line commented.
if(request.headers["Sec-Fetch-Site"] == "same-site" or request.headers["Sec-Fetch-Site"] == "none"):
The organizers also gave us a bot that was logged in to the site with an account that contained sensitive information and would connect to any user controlled domain. At this point it became pretty obvious that this was a CSRF challenge. The challenge author also hints us to check what browser the bot is running on. So I sent him to a request bin and checked the
user-agent header. It was running Firefox 89.
If we take a deeper look at the code, we find that there is an
/api endpoint that let’s us request any information about the user using the
params GET parameter and that the website checks the session cookie to know if we are logged in. The CORS configuration is the following:
cors = CORS(app, supports_credentials=True, send_wildcard= False)
After reading flask cors’ documentation we see that the
supports_credentials=True allows us to make authenticated requests and
This allows cookies and credentials to be submitted across domains.
This is the part of the writeup where knowing what browser the bot is running is crucial. Chrome uses
SameSite cookies by default whereas Firefox doesn’t. I learned this while solving this challenge from Angstrom Ctf with ice.
Exploit time! We only need a webpage that creates a
XMLHttpRequest and sets the
withCredentials flag so that Firefox uses the session cookie that was already set. After visiting the
/api/?params=codigo_secreto endpoint it should send the response to a user controlled request bin.
If you want to know more about CSRF you can check out OWASP’s page and hack tricks’ page.
Diário - 300 pts
This time we are given a web page that lets us log in or register a user and after logging in lets us create, delete or search for notes. We are also given a bot with the same browser as the Censos challenge and the description said the flag only contained lowercase letters and curly braces O.o . So far it looks like a pretty common XSS scenario but after reviewing the code (and some braindead testing :P) I noticed the following PostgreSQL query in the note search endpoint:
query = '%%%s%%' % str(query)
It instantly clicked inside my head. It is a XSLeaks challenge (shook). I also noticed it creates an
iframe for each post that is found by the query
After some testing I ran into a problem. we can search for a single character but we don’t know in what position it is in the string or how many times it’s repeated. I’m not that good at Scrabble so I thought I needed to know how the flag started or ended. As the common prefix for all the flags was CSCPT I wasn’t sure if I could use that because the flag only contained lowercase letters. So I started from the end. If I started to search backwards I could always find the letter before the one I already knew. Time to write an exploit.
All that is required to exploit this challenge is a webpage with a script that iterates over all the lowercase letters and curly braces and creates an
iframe setting its
src to the search endpoint. After the created
iframe has finished loading we can use the
contentWindow.length attribute to check whether it loaded an
iframe containing the flag or not and send the char that triggered that to a request bin.
To know more about XSLeaks I 1001% recommend the XSLeaks Wiki.
You can find the code for all the exploits here.
Truz Truz, Quem é? - 100 pts
We were given a binary that checks for the flag. It calls
sleep(1) if a character is incorrect. By sending an empty string we can calculate the length of the flag, based on how many seconds the server takes to answer. After that we can bruteforce one character at a time. To speed up the process I ended up guessing parts of the flag
Smali - 150 pts
We were given part of the source of an Android ransomware (in Smali), some logs and an encrypted file.
After not being able to use a decompiler to translate the Smali file to Java, I took advantage of some skills I learned in last year’s Hack The Vote Ctf and proceeded to read the Smali file.
The first thing I searched for was for the
Log function. It was called after a function named
generateCipher and logged the following
06-02 20:49:11.493 2802 2802 V Cryptor : Key generated
While digging inside the
encryptFile function I started analysing the
initAESCipher function and noticed the file was encrypted with AES CBC with PKCS5Padding with an hardcoded IV:
"QQqun 571012706 ".
So far so good. The only thing remaining was to discover where the AES key came from. I searched for
SecretKeySpec and right after the call to
generateCipher and to
SecretKeySpec constructor was being called with the result of
generateCipher function uses the
java.util.Random as a pseudo-random number generator to select 16 random characters (that would be the AES key) from an alphabet containing all the letters, decimal numbers and some symbols. The constructor of
Random was being called with a seed that was the current time in milliseconds.
To recover the key I translated the
generateCipher (replacing the call to
currentTimeMillis with the milliseconds since epoch from the UTC timestamp in the log) and ran it, passing the result to a python script that received the base64 encoded ciphertext and the static IV, decrypted the ciphertext with AES CBC and returned the flag.
Secure Token - 200 pts
We were given a web service and its python source. It features a OTP auth system system.
After reading some of the code I concluded that each user seed is generated from the admin seed, xored with the admin username, xored with the user username.
That seed is passed into the
gera_token function, that also receives the user username and the current
tick (this is just a timestamp that changes every minute) as arguments and it returns a valid login token.
gera_token it only calls
gera_token_aux with the same arguments and converts its output from
bytes to a
gera_token_aux is a bit more complex. It xors the current seed with the padded username and then it calls the function
f and that becomes the new seed value, doing that
Inside the function
f is where things get interesting. It takes a list of integers and has 2 temporary internal states:
- The first loop takes each integer from the input and changes the order of its bits. This order is determined by the list
res is built appending each bit (in the order determined by
S) of each integer.
- The second loop takes all the i-th bit of each 8 bit sub-sequence that could be formed from
res appending them and building
- The last step simply exchanges 4 bits with the following 4 bits
In my approach I prefered to think about
res2 as 8 bit integer arrays. Taking this into consideration it’s much easier to reverse the algorithm.
- If we apply the last step in the algorithm to any number 2 times we get the original number back, so all we need to do to undo this step is apply the same step again obtaining
- Then we build an array containing arrays of 8 bits from the
reversed_res2 we just obtained.
- Then we can build
reversed_res by iterating sequentially through
reversed_res2 and putting each bit in the i-th position of each element in the
- Then we build the original input by changing the position of each bit of each element of
reversed_res following the order of
S' = [7-v for v in S].
Now we can recover the original user seed xoring the result of the inverse function of
f with the padded username
To recover the admin seed it is only needed to xor the user seed with the padded admin username with the padded user username, and by calling
gera_token with the admin username, the admin seed and the current tick we can generate a valid admin password. GG
Treasure Island - 300 pts
We were given a Unity game that consisted in a player weilding an axe in an island with a treasure chest.
Using dnSpy I opened the
Assembly-CSharp.dll file that contains all the game logic for Unity based games and looked at the
Update function (that is called every frame) and found that there was a call to 2 functions named
f3 would only be called if
f3 receives a string
s as a parameter and calls
f6 passing it an array of bytes (aka a ciphertext) and
f6 receives a ciphertext and a key (the byte array in
s+"133337" ) and decrypts the ciphertext using AES CBC with an hardcoded IV.
f1 receives a string and checks the following conditions:
s == s + 1
s == s - 2
s == s - 2
s == s + 1
s == s + 1
s == s
s == s - 1
s == s
s == s - 1
Back to the
Update function, I checked where the string
s was being created/changed and to my surprise each byte could only be one of 3 options:
"C" . So I took my favourite SAT solver (z3) out of my pocket, inserted all the restrictions and bams! I had a valid key.
Next thing I did was to patch the
Update function to call
f3 with the correct key each frame. Et voila. The chest opened and the flag appeared in the sky.
You can find the code for all the exploits here.
Overall it was a really fun CTF. I would like to congratulate all the participants and thank all the organizers and challenge creators.
See you in Prague, Space Cowboy