5 We are presented with a web interface that is all about collecting snowflakes. After we have chosen a username we have the following options:
9 * Spend snoflakes to upgrade collection speed
10 * Draw a graph showing our progress
12 If we had 10^63 flakes, we could also buy a flag. There is a delay of a few seconds after each collection.
16 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.
18 The following API endpoints are used:
22 Used to register a new user by sending the payload
24 {"action":"new","name":"foobar"}
28 Most actions are done here. We can collect snoflakes by posting
30 {"action":"collect","amount":1}
38 {"collect_speed":1,"elf_name":"foobar","snowflakes":1.0,"speed_upgrade_cost":11.0}
42 Returns a log of all actions done by the user:
68 The timestamps are like UNIX timestamps but using milliseconds instead of seconds.
72 Here is a list of things that were tried:
74 ### Collecting the snow
76 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:
78 {"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."}
80 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%).
82 Simply increasing the `collection_speed` value n the request doesn't work either.
84 ### Overflowing the snowflakes
86 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
88 ### Sending fake actions
90 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.
94 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.
98 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:
100 * if the ID is a SHA-256 hash of a numerical session number
101 * is the HMAC-SHA256 value of the username and some passphrase
103 Other options, like a PBKDF2 hash or some encryptions sounded plausible but we couldn't figure a way leading to an exploitation
107 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`.
109 Anyway, it returns some interesting JSON data:
123 "data": "hX9TKHH2fsbWimuXCKHKyx0b4Y6afwRnLr8nxoLLNtwat8rLD0n5"
136 "data": "hX9TKHH2fsbWimqXCKHKyx0b4Y6afwRnLr8nxoLLNtwat8rLD0n5"
149 "data": "hX9TKHH2fsbWimqXCKHKyx0b4Y6afwRnLr8nxoLLNtwat8rLD0n5"
155 We imediately noticed:
157 * The timestamps are corresponding with those in the client history
158 * The data which is *saved* changes only slightly
159 * The data which is *saved* does not have a fixed length. Especially, it grows with the length of the username
161 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.
163 All we needed to do from here was to find the position of the snowflakes in the state and alter the value.
165 After creating a new user and collection a single snowflake, we can compare the state:
168 $ echo "hX9TKHH2fsbWimuXCKHKyx0b4Y6afwRnLr8nxoLLNtwat8rLD0n5"|base64 -d |xxd
169 00000000: 857f 5328 71f6 7ec6 d68a 6b97 08a1 cacb ..S(q.~...k.....
170 00000010: 1d1b e18e 9a7f 0467 2ebf 27c6 82cb 36dc .......g..'...6.
171 00000020: 1ab7 cacb 0f49 f9 .....I.
172 $ echo "hX9TKHH2fsbWimqXCKHKyx0b4Y6afwRnLr8nxoLLNtwat8rLD0n5"|base64 -d |xxd
173 00000000: 857f 5328 71f6 7ec6 d68a 6a97 08a1 cacb ..S(q.~...j.....
174 00000010: 1d1b e18e 9a7f 0467 2ebf 27c6 82cb 36dc .......g..'...6.
175 00000020: 1ab7 cacb 0f49 f9
178 Byte 11 is the only one which is different. Trying to change the value with python actually worked:
182 Python 3.6.9 (default, Nov 7 2019, 10:44:02)
184 Type "help", "copyright", "credits" or "license" for more information.
186 >>> data_b64 = 'hX9TKHH2fsbWimqXCKHKyx0b4Y6afwRnLr8nxoLLNtwat8rLD0n5'
187 >>> data = base64.b64decode(data_b64)
188 >>> mydata = data[:10] + bytes([data[10]^0x31^0x39]) + data[11:]
189 >>> base64.b64encode(mydata)
190 b'hX9TKHH2fsbWimKXCKHKyx0b4Y6afwRnLr8nxoLLNtwat8rLD0n5'
192 $ http --print hb http://3.93.128.89:1217/control "Cookie: id=Lx2WdSf8XSnHwjLHGejOiUYu0w40IJJyjENuw1PEpeY=" action=save data="hX9TKHH2fsbWimKXCKHKyx0b4Y6afwRnLr8nxoLLNtwat8rLD0n5"
196 Content-Type: application/json
197 Date: Thu, 26 Dec 2019 11:28:48 GMT
198 Server: Werkzeug/0.16.0 Python/2.7.17
202 $ http --print hb http://3.93.128.89:1217/client "Cookie: id=Lx2WdSf8XSnHwjLHGejOiUYu0w40IJJyjENuw1PEpeY=" action=state
206 Content-Type: application/json
207 Date: Thu, 26 Dec 2019 12:37:23 GMT
208 Server: Werkzeug/0.16.0 Python/2.7.17
214 "speed_upgrade_cost": 11.0
218 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:
222 Python 3.6.9 (default, Nov 7 2019, 10:44:02)
224 Type "help", "copyright", "credits" or "license" for more information.
226 >>> data_b64 = "hX9TKHH2fsbWimqJFr3GyUwY9I6bORx9P6IrxM7EOtRdr9DJTAqmlg=="
227 >>> data = base64.b64decode(data_b64)
228 >>> 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:]
229 >>> base64.b64encode(mydata)
230 b'hX9TKHH2fsbWimncDrnGyUwY9I6bORx9P6IrxM7EOtRdr9DJTAqmlg=='
233 Posting the new state again, we have a lot of snowflakes and can simply buy our flag: **AOTW{leaKinG_3ndp0int5}**
237 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):
239 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.
241 Second, the encryption of the state was done very bad. It used unauthenticated encryption and, to make matters worse, a completely broken cipher.
245 The following fixed should be applied, any of which would have prevented our exploit:
247 * Do not expose `/history/control` to the internet
248 * Only allow access to `/ history/control` and the `save` action of `/control` to authorized users (the backend service)
249 * Use a modern authenticated encryption scheme to secure the state (like AES-GCM)
253 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):