I was taking part in the OverTheWire Advent Bonanza 2019. All in all, the CTF was very pleasant because the challenges were interesting to solve, there was quite some time available as well (I spent about two weeks on and off on it) and I also managed to solve a few challenges as well.

The following write-up is about the challenge from day 12: naughty list.

Description

Santa has been abusing his power and is union busting us elves. Any elves caught participating in union related activities have been put on a naughty list, if you can get me off the list I will give you a flag.

The challenge features a simple web interface with three pages reachable from the homepage: Home, contact and login. Further, you can also register an account. The contact page is a dead end, however, and not relevant to the challenge. The main functionality is exposed after registering and logging in: Registered users can transfer credits to another user. After registration, a user has initially 1 credit, see the image below:

The Naughty List web interface in which a user can supposedly transfer credits.

It is possible to transfer the credit to the given destination code. The destination code changes after a refresh. In the credit field, it says 1 / 5000, i. e. 1 credit out of 5000.

Goal: The goal is to have an account with 5000 credits to get the flag.

Solution

Unsuccessful attempts

In the beginning, it was not totally clear that we had to forge a destination code. Therefore we also tried to mess around with the page parameter to try out if file inclusion would be possible. We found out that requesting http://3.93.128.89:1212/a/ still shows the original site (but breaks the styling) and also used the oracle (described blow) to request 000000000000 and \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00. The idea was to somehow decrypt the destination code by XORing it with the result of the encrypted 0 bytes, or something similar. In the end, however, these attempts were not fruitful.

Forging destination codes

To achieve the goal of 5000 credits we have to find out how to transfer credits to a user that we control. This turned out to be quite hard and involved some guesswork which made the challenge a bit annoying in my opinion.

The destination code is base64url-encoded. Decoding a destination code, e. g. 6GeVE89hSydXhCljZHIyTjEwQ3BlbnQra0xxOY9ceqtQiNEDWPds8K, reveals that there is another base64 string inside:

$ echo 6GeVE89hSydXhCljZHIyTjEwQ3BlbnQra0xxOY9ceqtQiNEDWPds8K-fTD8 | sed -e 's/-/+/g' -e 's#_#/#g' -e 's/$/=/g' | base64 -d | hd
00000000  e8 67 95 13 cf 61 4b 27  57 84 29 63 64 72 32 4e  |.g...aK'W.)cdr2N|
00000010  31 30 43 70 65 6e 74 2b  6b 4c 71 39 8f 5c 7a ab  |10Cpent+kLq9.\z.|
00000020  50 88 d1 03 58 f7 6c f0  af 9f 4c 3f              |P...X.l...L?|
0000002c

If we send a credit to the destination code, the response is: "Successfully transferred 1 credits to santa." It is therefore likely that the destination code contains the username in an encrypted (and signed?) way.

Clicking around on the page quickly reveals that the URLs for accessing Contact, Account, Login contain a string that is similar to the destination code. The URL for account is, for example: http://3.93.128.89:1212/?page=HTv6mUVltvgRV3DHN2dSSVhaMlhwQT09-qpBqw01iH2-qnyuzEU7Ow Interestingly enough, the URL changes on every refresh but old URLs can still be accessed. Decoding the different strings shows that the base64 encoded payload inside of them correlates with the lengths of "contact", "account", "login".

This leads to the following theory: The decoded string contains IV (12 bytes), a base64 encoded ciphertext (where the plaintext is contact, account or login for the pages and something else for a destination code) and a signature (16 bytes).

To transfer credits we now have to find out two things:

  1. How do we encrypt and sign data that we control?
  2. What data do we need to encrypt and sign?

Solving 1 was easy: If we access http://3.93.128.89:1212/?page=1 we are redirected to http://3.93.128.89:1212/?page=404&from_page=VFBTJgMHkLfWVJ3WVkE9PZAHUM8279yZcF5Xy8Nb71A. The from_page parameter contains our given 1 in an encrypted and signed form, i. e. we have discovered a way to encrypt and sign arbitrary data!

Solving 2 was hard: The length of the ciphertext for the user "santa" is longer (12 bytes vs. 5 bytes) than the username "santa". Also, encrypting "a" and using the result as a destination code to transfer credits to the user "a" does not work. I tried target=santa and other variations, but was out of luck. At this point I already gave up and focused on other challenges.

About a week later, @lavish appeared and was trying to solve the challenge as well. We speculated about different approaches how to solve the challenge. One idea was to use an SQL injection because of the "NO UNION" "hint" on the homepage. After some time, however, he found out how to forge the destination code and send credits to arbitrary users. He used the oracle to encrypt lavish:ciaociao and used the result as a destination code. The error message That user does not exist in our system. was different than Invalid destination code. which hinted that a part of the plaintext was indeed interpreted as a username. It quickly became clear that the plaintext is split on ":" and the second part is used as a destination username.

With this knowledge, it is easy to send a credit to an arbitrary user: Use the oracle to encrypt and sign target:<username> and use the result as a destination code to transfer the credit to an account that we control.

There is just one problem left: We would need to do this 5000 times, but the website only allows to create 15 accounts per hour per IP address: Sorry little elf, only 15 accounts per hour. An easy solution would be to circumvent the check by using proxies but we can use a second vulnerability.

Transferring more than one credit

We quickly discovered that sending a credit always takes about 1 s which is far too long for a simple update/insert transaction in the database. We found out that sending a credit is vulnerable to a race condition, i. e. a double spending attack is possible by sending the request multiple times in parallel. This allows to transfer more credits than the user actually has but also that only one credit is deducted even if more than one is sent (i.e. credits are created).

Final exploit

The final exploit was just a matter of putting everything together in a Python script. It allowed us to collect the flag: AOTW{S4n7A_c4nT_hAv3_3lF-cOnTroL_wi7H0uT_eLf-d1sCipl1N3}