CSAW’18 RTC Quals | Clicker Challenges

George O
CTF Writeups
Published in
8 min readOct 7, 2018

--

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:

The API reference homepage.

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:

User related endpoints.

It looks like standard user interaction. The register section tells us the following:

The /user/register endpoint.

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.

Clicker related endpoints.

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:

The /clicker/click 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:

__init__.py, lines 37–46.

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:

__init__py, lines 28–34.

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:

__init__.py, lines 49–59.

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:

/service/user_click.py, lines 11–62.

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:

/clicker/config.py, lines 14–17.

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:

__init__.py, lines 99–109.

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:

/clicker/service/user_click.py, lines 95–103, part of use_click().

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:

I used CyberChef to decode it.

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!

(I started the script at 100 for clarity).

And with that, all parts of the challenge have been completed.

--

--