nanohenry:~$ whoami
Henri

nanohenry:~$ uptime
20 years

nanohenry:~$ man henri
Programmer and digital inventor with a passion in cybersecurity

nanohenry:~$ service life status
Studying Quantum Technology at Aalto University

Open portfolio ▶

Pyg-Pen

LCSC 2020 Python sandbox escape challenge

TL;DR

Use the exit code from Python's os.system() to send a file byte-by-byte back to the user. (I do know that this is not the most efficient or the intended solution, but I documented it regardless since getting it to work was a lot of fun.)

Given information

Instructions to connect to a port on a host.

Solution

Netcatting into the port gives a prompt. After some experimentation, I discovered it to be a Python interpreter, which made sense considering the name of the challenge.

After some more looking around, running globals() showed that there were "unsafe builtins", like __import__ or input(). The function also showed a few modules, including SocketServer.

My first idea to solving it was to simply open and read the flag file, but anything that included the word "open" in it failed instantly and just returned the word "no". I also couldn't import any modules because of the "unsafe builtins" thing I mentioned.

After looking around even more, I found that the SocketServer has an interesting member, os (which is Python's module for interfacing with the OS). Running SocketServer.os.lisdir(".") showed that there was a file called flag.txt. So, target found.

Going through the os module, there wasn't much that was useful, except for os.system(), which allowed me to run arbitrary commands. Unfortunately, I quickly discovered that cat flag.txt wouldn't work, for two reasons: because the interpreter didn't like spaces and because os.system() only returns the exit code and no output. While the former could be fixed by replacing spaces with \x20 (hex 20 is the ASCII code for space), the latter was an actual problem.

The statement "only returns the exit code" is a bit badly put, since it does at least return something. Many commands that I ran with os.system() returned exit code 0, but others didn't. That sparked an idea: I could send at least one byte of data from a script on the server back to me via the exit code. Of course, it would require importing sys for sys.exit(), but since it was running in its own process, the import restrictions didn't apply anymore.

I tested it with the command python3 -c "import sys\nsys.exit(3)" (of course spaces replaced with \x20) and it returned 768. What's that, you might wonder? Well, it's the exit code bit-shifted left by 8 places: 3 << 8 = 768. So I knew the idea would work.

It didn't take me long to write a script that would craft a Python command string to return each of the first 64 characters of the flag:

from pwn import *

l = r"SocketServer.os.system('python3\x20-c\x20\"import\x20sys\nf=\x6Fpen(\'flag.txt\',\'rb\').read()\nsys.exit(f[{}])\"')"

conn = remote("[REDACTED]", 50032)
for i in range(64):
    conn.recvuntil("> ")
    conn.sendline(l.format(i))
    d = conn.readline().decode("utf-8")
    print(chr(int(d.split(" ")[-1].strip()) >> 8), end = "")
conn.close()

The command "template" is as follows:

SocketServer.os.system('python3\x20-c\x20\"import\x20sys\nf=\x6Fpen(\'flag.txt\',\'rb\').read()\nsys.exit(f[{}])\"')

Which runs...

python3 -c "import sys\nf=open('flag.txt','rb').read()\nsys.exit(f[{}])"

Which in turn runs the following:

import sys
f = open('flag.txt', 'rb').read()
sys.exit(f[{}])

Python's string formatting syntax (curly brackets) is used to read a different index each time that the command is run.

Running the script slowly but steadily printed out the flag characters.