Link Search Menu Expand Document

Secure Uploader

A new secure, safe and smooth uploader!

Challenge

TL;DR: Python’s os.path.join will ignore all previous path components if it reads an absolute path from any intermediate component.

Looking at the source code, it appears that we are unable to use ../ in filenames to obtain path traversal during our upload since it checks for . in the filename. A slight catch is that we could still upload files with filenames like /foo. The uploaded file is then saved to uploads/FILE_NAME. At the same time, the filename is stored into an SQL table files.

Source: app/app.py

@app.route('/upload', methods=['POST'])
def upload():
    if 'file' not in request.files:
        return redirect('/')
    file = request.files['file']
    if "." in file.filename:
        return "Bad filename!", 403
    conn = db()
    cur = conn.cursor()
    uid = uuid.uuid4().hex
    try:
        cur.execute("insert into files (id, path) values (?, ?)", (uid, file.filename,))
    except sqlite3.IntegrityError:
        return "Duplicate file"
    conn.commit()
    file.save('uploads/' + file.filename)
    return redirect('/file/' + uid)

Looking at the Dockerfile, we see that the flag is stored at /flag. This is a big hint as it reveals that our goal is reachable without using ..

Source: Dockerfile

COPY app /app
COPY flag.txt /flag
WORKDIR /app

Okay, what about when the file is retrieved then? It appears that it obtains the path from the files table, which was inserted into previously.

Source: app/app.py

@app.route('/file/<id>')
def file(id):
    conn = db()
    cur = conn.cursor()
    cur.execute("select path from files where id=?", (id,))
    res = cur.fetchone()
    if res is None:
        return "File not found", 404
    with open(os.path.join("uploads/", res[0]), "r") as f:
        return f.read()

Looking at how the final path for the open() call is obtained, we see that it uses os.path.join(), specifying the path uploads/ followed by the value from the files table. So, it would open files from uploads/FILE_NAME directory… right?

What if our filename is simply just /flag, would the open() call result in opening uploads//flag? Well, inspecting the docs for os.path.join() revealed the answer:

If a component is an absolute path, all previous components are thrown away and joining continues from the absolute path component.

So the uploads/ relative path will be ignored since we are specifying an absolute path /flag, resulting in the final path being /flag.

Following the redirect gives us the flag:

Flag: rarctf{4lw4y5_r34d_th3_d0c5_pr0p3rly!-71ed16}