]> git.somenet.org - pub/jan/ctf-seminar.git/blob - writeups/TwentyOneCool/OTW_Advent_day17.md
Add writeup for day 17 of OTW Advent CTF
[pub/jan/ctf-seminar.git] / writeups / TwentyOneCool / OTW_Advent_day17.md
1 # 17 - Snowflake Idle
2
3 ## About
4
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:
6
7 * Collect 1 Snoflake
8 * Melt 1 Snowflake
9 * Spend snoflakes to upgrade collection speed
10 * Draw a graph showing our progress
11
12 If we had 10^63 flakes, we could also buy a flag. There is a delay of a few seconds after each collection.
13
14 ## Analysis
15
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.
17
18 The following API endpoints are used:
19
20 `POST /control`
21
22 Used to register a new user by sending the payload
23
24     {"action":"new","name":"foobar"}
25
26 `POST /client`
27
28 Most actions are done here. We can collect snoflakes by posting
29
30     {"action":"collect","amount":1}
31
32 and retrieve a state
33
34     {"action":"state"}
35
36 which returns
37
38     {"collect_speed":1,"elf_name":"foobar","snowflakes":1.0,"speed_upgrade_cost":11.0}
39
40 `GET /history/client`
41
42 Returns a log of all actions done by the user:
43
44 ```json
45 [
46     [
47         1577358960360,
48         {
49             "action": "state"
50         }
51     ],
52     [
53         1577358966503,
54         {
55             "action": "collect",
56             "amount": 1
57         }
58     ],
59     [
60         1577358967774,
61         {
62             "action": "state"
63         }
64     ]
65 ]
66 ```
67
68 The timestamps are like UNIX timestamps but using milliseconds instead of seconds.
69
70 ## Exploitation
71
72 Here is a list of things that were tried:
73
74 ### Collecting the snow
75
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:
77
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."}
79
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%).
81
82 Simply increasing the `collection_speed` value n the request doesn't work either.
83
84 ### Overflowing the snowflakes
85
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
87
88 ### Sending fake actions
89
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.
91
92 ### Faking the graph
93
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.
95
96 ### Session hijacking
97
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:
99
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
102
103 Other options, like a PBKDF2 hash or some encryptions sounded plausible but we couldn't figure a way leading to an exploitation
104
105 ### Faking the state
106
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`.
108
109 Anyway, it returns some interesting JSON data:
110
111 ```json
112 [
113     [
114         1577358960360,
115         {
116             "action": "load"
117         }
118     ],
119     [
120         1577358960360,
121         {
122             "action": "save",
123             "data": "hX9TKHH2fsbWimuXCKHKyx0b4Y6afwRnLr8nxoLLNtwat8rLD0n5"
124         }
125     ],
126     [
127         1577358966503,
128         {
129             "action": "load"
130         }
131     ],
132     [
133         1577358966503,
134         {
135             "action": "save",
136             "data": "hX9TKHH2fsbWimqXCKHKyx0b4Y6afwRnLr8nxoLLNtwat8rLD0n5"
137         }
138     ],
139     [
140         1577358967774,
141         {
142             "action": "load"
143         }
144     ],
145     [
146         1577358967774,
147         {
148             "action": "save",
149             "data": "hX9TKHH2fsbWimqXCKHKyx0b4Y6afwRnLr8nxoLLNtwat8rLD0n5"
150         }
151     ]
152 ]
153 ```
154
155 We imediately noticed:
156
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
160
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.
162
163 All we needed to do from here was to find the position of the snowflakes in the state and alter the value.
164
165 After creating a new user and collection a single snowflake, we can compare the state:
166
167 ```bash
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
176 ```
177
178 Byte 11 is the only one which is different. Trying to change the value with python actually worked:
179
180 ```bash
181 $ python3
182 Python 3.6.9 (default, Nov  7 2019, 10:44:02) 
183 [GCC 8.3.0] on linux
184 Type "help", "copyright", "credits" or "license" for more information.
185 >>> import base64
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'
191
192 $ http --print hb http://3.93.128.89:1217/control "Cookie: id=Lx2WdSf8XSnHwjLHGejOiUYu0w40IJJyjENuw1PEpeY=" action=save data="hX9TKHH2fsbWimKXCKHKyx0b4Y6afwRnLr8nxoLLNtwat8rLD0n5"
193
194 HTTP/1.0 200 OK
195 Content-Length: 5
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
199
200 null
201
202 $ http --print hb http://3.93.128.89:1217/client "Cookie: id=Lx2WdSf8XSnHwjLHGejOiUYu0w40IJJyjENuw1PEpeY=" action=state
203
204 HTTP/1.0 200 OK
205 Content-Length: 115
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
209
210 {
211     "collect_speed": 1,
212     "elf_name": "a",
213     "snowflakes": 9.0,
214     "speed_upgrade_cost": 11.0
215 }
216 ```
217
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:
219
220 ```bash
221 $ python3
222 Python 3.6.9 (default, Nov  7 2019, 10:44:02) 
223 [GCC 8.3.0] on linux
224 Type "help", "copyright", "credits" or "license" for more information.
225 >>> import base64
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=='
231 ```
232
233 Posting the new state again, we have a lot of snowflakes and can simply buy our flag: **AOTW{leaKinG_3ndp0int5}**
234
235 ## Vulnerabilities
236
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):
238
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.
240
241 Second, the encryption of the state was done very bad. It used unauthenticated encryption and, to make matters worse, a completely broken cipher.
242
243 ## Fixes
244
245 The following fixed should be applied, any of which would have prevented our exploit:
246
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)
250
251 ## Acknowledments
252
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):
254
255 * superwayne
256 * Bernhard Neumann
257 * stiefel40k
258 * Gerhard Jungwirth
259 * TwentyOneCool
260 * HaH
261 * theguy
262 * chgue
263 * georg
264 * lavish
265 * litplus
266 * Hetti