From 28241456f25d9bf0a54f7933781638fcc1ac7c4e Mon Sep 17 00:00:00 2001 From: Mathias Tausig Date: Thu, 26 Dec 2019 13:57:37 +0100 Subject: [PATCH] Add writeup for day 17 of OTW Advent CTF --- writeups/TwentyOneCool/OTW_Advent_day17.md | 266 +++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 writeups/TwentyOneCool/OTW_Advent_day17.md diff --git a/writeups/TwentyOneCool/OTW_Advent_day17.md b/writeups/TwentyOneCool/OTW_Advent_day17.md new file mode 100644 index 0000000..d228d9b --- /dev/null +++ b/writeups/TwentyOneCool/OTW_Advent_day17.md @@ -0,0 +1,266 @@ +# 17 - Snowflake Idle + +## About + +We are presented with a web interface that is all about collecting snowflakes. After we have chosen a username we have the following options: + +* Collect 1 Snoflake +* Melt 1 Snowflake +* Spend snoflakes to upgrade collection speed +* Draw a graph showing our progress + +If we had 10^63 flakes, we could also buy a flag. There is a delay of a few seconds after each collection. + +## Analysis + +After entering the username a cookie named `id` is set in the browser containing 32 BASE64 encoded bytes. The value of the ID is indistinguishable from random. + +The following API endpoints are used: + +`POST /control` + +Used to register a new user by sending the payload + + {"action":"new","name":"foobar"} + +`POST /client` + +Most actions are done here. We can collect snoflakes by posting + + {"action":"collect","amount":1} + +and retrieve a state + + {"action":"state"} + +which returns + + {"collect_speed":1,"elf_name":"foobar","snowflakes":1.0,"speed_upgrade_cost":11.0} + +`GET /history/client` + +Returns a log of all actions done by the user: + +```json +[ + [ + 1577358960360, + { + "action": "state" + } + ], + [ + 1577358966503, + { + "action": "collect", + "amount": 1 + } + ], + [ + 1577358967774, + { + "action": "state" + } + ] +] +``` + +The timestamps are like UNIX timestamps but using milliseconds instead of seconds. + +## Exploitation + +Here is a list of things that were tried: + +### Collecting the snow + +The delay after collecting the snowflake(s) is just done on the client side using JS. Unfortunately, sending the requests faster manually, doesn't work either, since the server returns an error if we send our requests too fast: + + {"error":"Throttled at one request per second. Please note that this challenge does NOT require brute forcing, and does NOT require sending an excessive number of requests."} + +Increasing the collection speed is also bound to fail, since the collection speed only grows lineary (+1 with each step) while the cost for increasing the speed grows exponentially (+10%). + +Simply increasing the `collection_speed` value n the request doesn't work either. + +### Overflowing the snowflakes + +The ability to melt snowflakes (reduce the amount) made us consider trying to underflow the snowflake value. Unfortunately, since the application uses a floating point value, which is not using Twos-complement, this won't work + +### Sending fake actions + +The `/client` endpoint accepts any action sent and also records it (as is visible in the history). We couldn't find any actions that had any impact, though. + +### Faking the graph + +While setting the `collection_speed` to a high value did not have an impact on the snowflakes assigned to us, it did impact the graph of our progress. But while we were able to draw a graph ending at 10^64 snowflakes, the flag wasn't delivered to us anyway. + +### Session hijacking + +We tried to find a way to get a session ID which gave us that of some rich elf allowing us to buy a flag. We tried: + +* if the ID is a SHA-256 hash of a numerical session number +* is the HMAC-SHA256 value of the username and some passphrase + +Other options, like a PBKDF2 hash or some encryptions sounded plausible but we couldn't figure a way leading to an exploitation + +### Faking the state + +We noticed that GET requests to `/history/asdf` did cause a 500 error and not a 404. Thus we assumed, that there was another hidden endpoint here. We finally, found it to be `GET /history/control` using the fuzzing tool of OWASP ZAP, even though we could have considered it without it, since the other paths known to us were `/client`, `/history/client` and `control`. + +Anyway, it returns some interesting JSON data: + +```json +[ + [ + 1577358960360, + { + "action": "load" + } + ], + [ + 1577358960360, + { + "action": "save", + "data": "hX9TKHH2fsbWimuXCKHKyx0b4Y6afwRnLr8nxoLLNtwat8rLD0n5" + } + ], + [ + 1577358966503, + { + "action": "load" + } + ], + [ + 1577358966503, + { + "action": "save", + "data": "hX9TKHH2fsbWimqXCKHKyx0b4Y6afwRnLr8nxoLLNtwat8rLD0n5" + } + ], + [ + 1577358967774, + { + "action": "load" + } + ], + [ + 1577358967774, + { + "action": "save", + "data": "hX9TKHH2fsbWimqXCKHKyx0b4Y6afwRnLr8nxoLLNtwat8rLD0n5" + } + ] +] +``` + +We imediately noticed: + +* The timestamps are corresponding with those in the client history +* The data which is *saved* changes only slightly +* The data which is *saved* does not have a fixed length. Especially, it grows with the length of the username + +We deduced, that this must be some form of the user state. By creating a user named `aaaaaaaaaaaaaa...` and noticing a repeating pattern in the data we assumed that it was just vignere enciphered. + +All we needed to do from here was to find the position of the snowflakes in the state and alter the value. + +After creating a new user and collection a single snowflake, we can compare the state: + +```bash +$ echo "hX9TKHH2fsbWimuXCKHKyx0b4Y6afwRnLr8nxoLLNtwat8rLD0n5"|base64 -d |xxd +00000000: 857f 5328 71f6 7ec6 d68a 6b97 08a1 cacb ..S(q.~...k..... +00000010: 1d1b e18e 9a7f 0467 2ebf 27c6 82cb 36dc .......g..'...6. +00000020: 1ab7 cacb 0f49 f9 .....I. + $ echo "hX9TKHH2fsbWimqXCKHKyx0b4Y6afwRnLr8nxoLLNtwat8rLD0n5"|base64 -d |xxd +00000000: 857f 5328 71f6 7ec6 d68a 6a97 08a1 cacb ..S(q.~...j..... +00000010: 1d1b e18e 9a7f 0467 2ebf 27c6 82cb 36dc .......g..'...6. +00000020: 1ab7 cacb 0f49 f9 +``` + +Byte 11 is the only one which is different. Trying to change the value with python actually worked: + +```bash +$ python3 +Python 3.6.9 (default, Nov 7 2019, 10:44:02) +[GCC 8.3.0] on linux +Type "help", "copyright", "credits" or "license" for more information. +>>> import base64 +>>> data_b64 = 'hX9TKHH2fsbWimqXCKHKyx0b4Y6afwRnLr8nxoLLNtwat8rLD0n5' +>>> data = base64.b64decode(data_b64) +>>> mydata = data[:10] + bytes([data[10]^0x31^0x39]) + data[11:] +>>> base64.b64encode(mydata) +b'hX9TKHH2fsbWimKXCKHKyx0b4Y6afwRnLr8nxoLLNtwat8rLD0n5' + +$ http --print hb http://3.93.128.89:1217/control "Cookie: id=Lx2WdSf8XSnHwjLHGejOiUYu0w40IJJyjENuw1PEpeY=" action=save data="hX9TKHH2fsbWimKXCKHKyx0b4Y6afwRnLr8nxoLLNtwat8rLD0n5" + +HTTP/1.0 200 OK +Content-Length: 5 +Content-Type: application/json +Date: Thu, 26 Dec 2019 11:28:48 GMT +Server: Werkzeug/0.16.0 Python/2.7.17 + +null + +$ http --print hb http://3.93.128.89:1217/client "Cookie: id=Lx2WdSf8XSnHwjLHGejOiUYu0w40IJJyjENuw1PEpeY=" action=state + +HTTP/1.0 200 OK +Content-Length: 115 +Content-Type: application/json +Date: Thu, 26 Dec 2019 12:37:23 GMT +Server: Werkzeug/0.16.0 Python/2.7.17 + +{ + "collect_speed": 1, + "elf_name": "a", + "snowflakes": 9.0, + "speed_upgrade_cost": 11.0 +} +``` + +The exploit works, but we need a longer `snoflakes` value field to hold the value we need (*1e63*). After collecting one more flake to bring the value to *10.0* we simply repeat the procedure above: + +```bash +$ python3 +Python 3.6.9 (default, Nov 7 2019, 10:44:02) +[GCC 8.3.0] on linux +Type "help", "copyright", "credits" or "license" for more information. +>>> import base64 +>>> data_b64 = "hX9TKHH2fsbWimqJFr3GyUwY9I6bORx9P6IrxM7EOtRdr9DJTAqmlg==" +>>> data = base64.b64decode(data_b64) +>>> mydata = data[:10] + bytes([data[10]^0x31^0x32]) + bytes([data[11]^0x30^ord('e')]) + bytes([data[12]^ord('.')^ord('6')]) + bytes([data[13]^ord('0')^ord('4')]) + data[14:] +>>> base64.b64encode(mydata) +b'hX9TKHH2fsbWimncDrnGyUwY9I6bORx9P6IrxM7EOtRdr9DJTAqmlg==' +``` + +Posting the new state again, we have a lot of snowflakes and can simply buy our flag: **AOTW{leaKinG_3ndp0int5}** + +## Vulnerabilities + +Out exploit was possibly due to two vulnerabilities in the code. One web based and one cryptographic (as suggested by the category of the challenge): + +First, an administrative endpoint (`/history/control`) was exposed to the internet for no reasong without any authorization. Also, state manipulation was poosible via the `save` action without any authentication. + +Second, the encryption of the state was done very bad. It used unauthenticated encryption and, to make matters worse, a completely broken cipher. + +## Fixes + +The following fixed should be applied, any of which would have prevented our exploit: + +* Do not expose `/history/control` to the internet +* Only allow access to `/ history/control` and the `save` action of `/control` to authorized users (the backend service) +* Use a modern authenticated encryption scheme to secure the state (like AES-GCM) + +## Acknowledments + +The solution was a great team effort which took more than a week. The following people actively participated in the Mattermost channel (in chronological order of appearance): + +* superwayne +* Bernhard Neumann +* stiefel40k +* Gerhard Jungwirth +* TwentyOneCool +* HaH +* theguy +* chgue +* georg +* lavish +* litplus +* Hetti -- 2.43.0