CSAW’18 RTC Quals | Clicker Challenges
This is a write-up for three of the challenges in the CSAW 2018 Red Team Qualifiers. I participated in this with my team, even though we aren’t eligible for the prizes. The competition lasted the from September 21st to September 30th.
For these challenges, we were given a REST API which we had to exploit, built on a Flask back-end. The first challenge was worth 100 points, the second was worth 250, and the third was worth 500. To put this in perspective, only 3 of the 48 challenges were worth more than 400 points, and the average points per challenge was around 150.
Part One: Making a Wrapper
If you want to skip straight to the exploit stage, go ahead to the next part (this whole section is fairly useless).
We were given access to this website, and also the source code for it:
This consisted of documentation for a clicker game. Before trying to exploit it, let’s try and understand how it works.
In order to play, we must first register an account. Let’s take a look at the documentation for user to find out more about this:
It looks like standard user interaction. The register section tells us the following:
Since we now have all we need to register an account, let’s begin a program that should take care of all this for us.
The above code can do two things:
- Create an account (and automatically login).
- Login to an account.
In order to use this feature, we do the following:
The -i feature runs through the script and then opens a terminal, which means that we can go ahead and call our defined functions. The registered user will have the same username and password (secure, I know).
With this now sorted, let’s look at the actual game mechanics.
Before we can use a clicker, we have to buy one. We can view all of the available clickers with the /clicker/ endpoint:
$ curl -X GET "http://web.chal.csaw.io:10106/clicker/" -H "accept: application/json"{'name': 'base', 'value': 1, 'price': 5, 'scale': 1.15, 'max': 10},
{'name': 'momo', 'value': 5, 'price': 10, 'scale': 1.15, 'max': 10},
{'name': 'mgb', 'value': 10, 'price': 1000, 'scale': 1.15, 'max': 10},
{'name': 'profk', 'value': 100, 'price': 10000, 'scale': 1.15, 'max': 10},
{'name': 'bigj', 'value': 100, 'price': 50000, 'scale': 1.15, 'max': 10},
{'name': 'passion', 'value': 500, 'price': 2000000, 'scale': 1.15, 'max': 10},
{'name': 'hyper', 'value': 10000, 'price': 2000000, 'scale': 1.15, 'max': 10},
{'name': 'ghost', 'value': 100000, 'price': 120000000, 'scale': 1.15, 'max': 10},
{'name': 'tnek', 'value': 1000000, 'price': 4000000000, 'scale': 1.15, 'max': 10},
{'name': 'captiosus', 'value': 10000000, 'price': 50000000000, 'scale': 1.15, 'max': 10}
It looks like we need to start with the base clicker. I created a purchase function so that we can buy/upgrade clickers fairly easily:
Let’s purchase our first clicker:
Now that we have bought a clicker, let’s actually use it. In order to make a click, we use the following endpoint:
I added the following function to my code for this to happen:
I know this is getting quite tedious, but there’s one more that we need to do. Since we have to keep track of the amount of money/number of clickers that we have, we should add a function that checks this:
With the final script now finished, we can manually play the game:
PART TWO: Header Manipulation
Before we start looking for any flags, let’s search for “flag” in all the provided files:
It looks like all of the flag references are in the __init__.py file.
We can then find the following about flag1.txt:
This code tells us that if we create a user and use the bring_back_random_click header, along with the output of “random_string()”, we should get the flag.
Let’s look for some references to this function:
Since this function is also defined in the __init__.py file, let’s go ahead and take a look at it:
All this function seems to do is pick a letter, and create a string based of that same letter 10 times (i.e. “aaaaaaaaaa” or “zzzzzzzzzz”). This means that the string can only be 1 of 26 different things.
By sending off a request with “aaaaaaaaaa” as the bring_back_random_click header repeatedly, we should eventually be rewarded with the flag.
After being left for a few seconds, we are given the first flag:
george@kali:~$ python h.py
"flag{wh@t_Ar3_lAt3_b1nDiNg_cl0sUreS}"
That’s the first flag out of the way.
PART THREE: Python Rounding
This flag was discovered entirely by my teammate Sam Wedgwood, who then went on to explain the solution to me.
The part of the code relating to flag2.txt is as follows:
This code says that if the user’s money is greater than or equal to 0, a 404 error will be thrown. Therefore, we need to have a negative amount of money to get the flag. 🤔
While looking through some more of the source code, we can find this function, which is called whenever the user purchases a new clicker:
The two important things to note here are:
- Line 36: if round(user.money — price) >= 0:
- Line 53: user.money -= round(price)
The if statement here is used to validate that the user has enough money to buy a clicker, and the second statement is used to take the funds from their account.
However, the if statement rounds both the user’s money and the price, whereas the second statement only valuates the rounded price.
After messing around with the round function for a bit, we can find the following:
As shown, the round function will round n.5 up if the number is odd, and down if the number is even (only in Python 3.x).
In order to apply this to what we need to do, we can do this:
So if we can perform something similar to this in our program, we can get a negative balance!
We will need to have an odd number of currency, and buy something which is odd, and is worth n.5.
Although none of the clickers are worth n.5, we do have the option to upgrade clickers.
As shown in clicker/service/clicker.py…
… an upgrade will cost 1.15 the previous value of the clicker.
So, if the clicker costs 10, then the upgrade will cost 11.5.
Fortunately for us, the momo clicker costs 10:
With this in mind, I can launch the program we created earlier, and perform these actions:
[Register user]
[Buy "base" clicker]
[Click 10 times]>>> purchase("momo")
'Success!'
>>> click("momo")
'Success!'
>>> click("momo")
'Success!'
>>> click("base")
'Success!'
>>> purchase("momo")
'Success!'
>>> stats()
##########
Stats for kjgnfjkgnn:
##########
Money: -1
##########
Clicker Name | Clicker Value | Clicker Price
--------------------------------------------------
base | 1 | 5
momo | 10 | 13
--------------------------------------------------
>>>
As you can see, we now have -1 money!
All we now need to do is send a request to the /money endpoint:
>>> print(rq.get("http://web.chal.csaw.io:10106/default/money", headers=auth).text)
"flag{d1d_u_eXp3r1ence_gr0wTh}"
Which means that the second part to this challenge has been completed!
PART FOUR: Data Extraction through Classes
The source code only mentions flag3.txt here:
Seeing as this doesn’t seem to do much, I searched the other files for references to “record”, and discovered this in /clicker/service/user_click.py:
This tells us that we need to make 5000000000 “money” in 5 seconds.
We’ll come back to that later.
Looking back into the code, we can see some very important variables being declared in the Config class:
We also have an endpoint which allows us to read data from the CLICKERS object:
Let’s try to abuse this endpoint:
george@kali:~$ curl -X GET "http://web.chal.csaw.io:10106/clicker/Config?field=SECRET_KEY" -H "accept: application/json"
{"SECRET_KEY": "dId_you_r3aLly_think_I_w0u1dnt_s3t_a_key"}
george@kali:~$
Unfortunately, this isn’t the key.
After a couple more hours of enumeration, I started looking at the JWT session token.
Let’s take a look at this fresh token:
george@kali:~$ python -i clicker.py
>>> register("ngjkfnjkghn")
'Logged in as ngjkfnjkghn.'
>>> auth
{'Authorization': u'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1Mzg5NTQ1MjcsImlhdCI6MTUzODg2ODEyMiwic3ViIjozMjgsImFkbWluIjpmYWxzZX0.abLB2wnlcuiSloaCDDVBi81a8i-8RvjCDrIVZj8iBk0'}
>>>
Some more research revealed that there are 3 main JWT exploits:
- Setting the algorithm to none.
- Cracking the key.
- RS/HS256 “Mismatch” vulnerabilities.
Seeing as we now have the key, we don’t actually need to crack it. As such, we should just be able to decode it and re-sign it with the new key:
Let’s change admin to true, and resign it:
I then tried using this new token to add funds to my account using the /default/admin/money endpoint, but simply received this error:
{"status": "error", "message": "admin token required"}
So I suppose that it just didn’t work.
Let’s see instead whether any other users are already admins, by looking in the main.db file that we were given:
It looks like the user database has a record field!
If you don’t remember from earlier, we obtain the flag by having a record higher than 5000000000.
In order to log in as user, we can either bruteforce the password found in the auth table, or bruteforce the user’s ID in their JWT token.
Seeing as there probably aren’t many users, bruteforcing the token would likely be far easier.
Eventually, I came up with this script:
When left for a while, it finds the flag!
And with that, all parts of the challenge have been completed.
Contact me:
Personal Website
Github
Hack The Box