PortSwigger: All File Upload Lab Walkthroughs

 
 

In this post, I will cover the all of the File Upload labs located at PortSwigger Academy as well as providing some context regarding what File Upload vulnerabilities are and the problems that they can cause.


File Upload Vulnerabilities

File upload vulnerabilities are when a web server allows users to upload files to its filesystem without sufficiently validating things like their name, type, contents or size. Failing to properly enforce restrictions on these could mean that even a basic image upload function can be used to upload arbitrary and potentially dangerous files instead.

This could even include server-side script files that enable remote code execution.

In some cases, the act of uploading the file is in itself enough to cause damage. Other attacks may involve a follow-up HTTP request for the file, typically to trigger its execution by the server.

Impact of File Upload Vulnerabilities

The impact of file upload vulnerabilities generally depends on two key factors:

  • Which aspect of the file the website fails to validate properly, whether that be its size, type, contents and so on.

  • What restrictions are imposed on the file once it has been successfully uploaded

In the worst case scenario, the file's type is not validated properly, and the server config allows certain types of file (such as .php and .jsp) to be executed as code. In this case, an attacker could potentially upload a server-side code file that functions as a web shell, granting them full control.

If the filename is not validated properly, this could allow an attacker to overwrite critical files simply by uploading a file with the same name. If the server is also vulnerable to directory traversal, this could mean attackers are even able to upload files to unanticipated locations.

Failing to make sure that the size of the file falls within expected thresholds could also enable a form of DoS attacks, whereby the attacker fills the available disk space.

How Do File Upload Vulnerabilities Arise?

Given the fairly obvious dangers, it is rare for websites in the wild to have no restrictions whatsoever on which files users are allowed to upload. More commonly, developers implement what they believe to be robust validation that is either inherently flawed or can be easily bypassed.

For example, they may attempt to blacklist dangerous file types, but fail to account for parsing discrepancies when checking the file extensions. As with any blacklist, it is also easy to accidentally omit more obscure file types that may still be dangerous.

In other cases, the website may attempt to check the file type by verifying properties that can be easily manipulated by an attacker using tools like Burp Proxy or Repeater.

Ultimately, even robust validation measures may be applied inconsistently across the network of hosts and directories that form the website, resulting in discrepancies that can be exploited.

How Do Servers Handle Requests for Static Files?

Before we look at how to exploit file upload vulnerabilities, it's important that you have a basic understanding of how servers handle requests for static files.

Historically, websites consisted almost entirely of static files that would be served to users when requested. As a result, the path of each request could be mapped 1:1 with the hierarchy of directories and files on the server's filesystem.

Nowadays, websites are increasingly dynamic and the path of a request often has no direct relationship to the filesystem at all. Nevertheless, web servers still deal with requests for some static files, including stylesheets, images, and so on.

The process for handling these static files is still largely the same. At some point, the server parses the path in the request to identify the file extension. It then uses this to determine the type of the file being requested, typically by comparing it to a list of preconfigured mappings between extensions and MIME types. What happens next depends on the file type and the server's configuration.

  • If the file type is non-executable, such as an image or static HTML page, the server may just send the file's contents to the client in an HTTP response

  • If the file type is executable, such as a PHP file, and the server is configured to execute files of this type, it will assign variables based on the headers and parameters in the HTTP request before running the script. The resulting output may then be sent to the client in an HTTP response.

  • If the file type is executable, but the server is not configured to execute files of this type, it will generally respond with an error. However, in some cases, the contents of the file may still be served to the client as plain text. Such misconfigs can occasionally be exploited to leak source code and other sensitive information.

The Content-Type response header may provide clues as to what kind of file the server thinks it has served. If this header has not been explicitly set by the app code, it normally contains the result of the file extension/MIME type mapping.

Exploiting Unrestricted File Uploads

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.

If you are able to successfully upload a web shell, you effectively have full control over the server. This means you can read and write arbitrary files, exfiltrate sensitive data and even use the server to pivot attacks against both internal infrastructure and other servers outside the network.

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

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 like:

This script enables you to pass an arbitrary system command via a query parameter as follows:

Exploiting Flawed Validation of File Uploads

In the wild, it is unlikely that you will find a website that has no protection whatsoever against file upload attacks. But, just because defenses are in place, that does not mean that they are robust.

Flawed Type Validation

When submitting HTML forms, the browser typically sends the provided data in a POST request with the content type "application/x-www-form-url-encoded". This is fine for sending simple text like your name, address and so on, but is not 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 the preferred approach.

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:

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 also contain their own Content-Type header which tells the server the MIME type of the data that was submitted using this input.

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 expecting 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 implicitly 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 Repeater.

Preventing File Execution in User-Accessible Directories

While it is clearly better to prevent dangerous file types being uploaded in the first place, the second line of defense is to stop the server from executing any scripts that do slip through the net.

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, in some cases, serve the contents of the file as plain text instead:

This behaviour is potentially 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.

This kind of configuration often differs between directories. A directory to which user-supplied files are uploaded will likely have much stricter controls than other locations on the filesystem that are assumed to be out of reach for end users.

If you can find a way to upload a script to a different directory that is not supposed to contain user-supplied files, the server may execute your script after all.

Web servers often use the filename field in multipart/form-data requests to determine the name and location where the file should be saved.

You should also note that even though you may send all of your requests to the same domain name, this often points to a reverse proxy server of some kind, such as a load balancer. Your requests will often be handled by additional servers behind the scenes, which may also be configured differently.

Insufficient Blacklist 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 is difficult to explicitly block every possible file extension that could be used to execute code.

Such blacklists can sometimes be bypassed uysing lesser known, alternative file extensions that may still be executable such as .php5, .shtml and so on.

Overriding the Server Configuration

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 like:

Many servers also allow developers to create special configuration files within individual directories in order to override or add to one or more global settings. Apache servers, for example, will load a directory specific config 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:

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.

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 fails to recognize that exploit.pHp is in fact a .php file. If the code that subsequently maps the file extension to a MIME type is not case sensitive, this discrepancy allows you to sneak malicious PHP files past validation that may eventually be executed by the server.

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 such like: 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 validation 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 multibyte 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:

Flawed Validation of the File’s Contents

Instead of implicitly trusting the 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 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.

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.

Exploiting File Upload Race Conditions

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.

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 milliseconds, 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 relevant source code.

Race Conditions in URL-based File Uploads

Similar race conditions can occur in functions that allow you to upload a file by providing a URL. In this case, the server has to fetch the file over the internet and create a local copy before it can perform any validation.

As the file is loaded using HTTP, developers are unable to use their framework's built-in mechanisms for securely validating files. Instead, they may manually create their own processes for temporarily storing and validating the file, which may not be quite as secure.

For example, if the file is loaded into a temporary directory with a randomized name, in theory, it should be impossible for an attacker to exploit any race conditions. If they don't know the name of the directory, they will be unable to request the file in order to trigger its execution.

On the other hand, if the randomized directory name is generated using pseudo-random functions like PHP's uniqid(), it can potentially be brute-forced.

To make attacks like this easier, you can try to extend the amount of time taken to process the file, thereby lengthening the window for brute-forcing the directory name. One way of doing this is by uploading a larger file. If it is processed in chunks, you can potentially take advantage of this by creating a malicious file with the payload at the start, followed by a large number of arbitrary padding bytes.

Exploiting File Upload Vulns without RCE

Uploading Malicious Client-Side Scripts

Although you might not be able to execute scripts on the server, you may still be able to upload scripts for client-side attacks. For example, if you can upload HTML files or SVG images, you can potentially use <script> tags to create stored XSS payloads.

If the uploaded file then appears on a page that is visited by other users, their browser will execute the script when it tries to render the page. Note that due to same-origin policy restrictions, these kinds of attacks will only work if the uploaded file is served from the same origin to which you upload it.

Exploiting Vulnerabilities in the Parsing

If the uploaded file seems to be both stored and served securely, the last resort is to try exploiting vulnerabilities specific to the parsing or processing of different file formats. For example, you know that the server parses XML-based files, such as Microsoft Office .doc or .xls files, this may be a potential vector for XXE injection attacks.

Uploading Files using PUT

It's worth noting that some web servers may be configured to support PUT requests. If appropriate defenses aren't in place, this can provide an alternative means of uploading malicious files, even when an upload function isn't available via the web interface.

How to Prevent File Upload Vulnerabilities

Allowing users to upload files is commonplace and doesn't have to be dangerous as long as you take the right precautions. In general, the most effective way to protect your own websites from these vulnerabilities is to implement all of the following practices:

  • Check the file extension against a whitelist of permitted extensions rather than a blacklist of prohibited ones. It's much easier to guess which extensions you might want to allow than it is to guess which ones an attacker might try to upload.

  • Make sure the filename doesn't contain any substrings that may be interpreted as a directory or a traversal sequence (../).

  • Rename uploaded files to avoid collisions that may cause existing files to be overwritten.

  • Do not upload files to the server's permanent filesystem until they have been fully validated.

  • As much as possible, use an established framework for preprocessing file uploads rather than attempting to write your own validation mechanisms.


Lab 1 - Remote code execution via web shell upload

Once we start the lab, we can access it and see the following home page:

For this lab, we are given the credentials (wiener:peter) so let's log in and see if there is any additional functionality:

Here, we see a standard field for changing our email, but we also see a file upload functionality for choosing our avatar or profile picture. With this in mind, let' observer the normal functionality of this.

To do this, let's choose a file. For this example, I downloaded a random image of a dog and have named it "R.jpg" as can be seen below:

Once we select it and open it, we can see it tells us in the web app which file we have chosen:

Now, let's hit the Upload button and see what happens. If it works like we assume it does on any other live website, it should accept a normal image as expected and no error should occur.

And it does! We can see a successful message telling us that the file was uploaded and it appears to give us a full path - avatars/R.jpg. Now, if we go back to the My Account page, we can see the profile picture has been successfully set for our account:

Another thing we can do is verify the path that the app returned to us when we uploaded (/avatars). To do this, we can look at the source code of the image itself and see if it includes any path:

And it does. As we can see, the image is located at inside the files/avatars directory. With this, we can make sure we can directly access the file by trying to navigate to that image ourselves:

Perfect. Look at that cute little puppy! We now know the full path of the images we upload and that we can access them directly. Next, we need to test if there is any sort of validation or security in place stopping us from uploading a malicious file like a PHP file.

To do this, there are many ways but my preferred way is to simply take the existing dog image and rename its extension to ".php". This way, if there is a filter in place looking for certain file extensions, it should tell us that the file is blocked or produce some sort of error:

With this dummy PHP file in place, we can try uploading it and see if it succeeds or fails:

As we can see, it succeeds. Now, if we go back to the My Account page and view the source code, we can see that it simply uploading the entire filename, seemingly without changing or applying any blocking or filtering:

With this in mind, we can now try and get malicious. To do this, you could use a variety of PHP one-liners for grabbing certain documents, but I like to create a PHP web shell to allow for more functionality. The PHP web shell can be seen below:

So, what does this code actually do? Well, the script takes a parameter 'e' through a GET request, which contains the command the attacker wants to execute.

The 'shell_exec' function is called, which executes the command passed as a parameter to 'e' and returns its output as a string.

The output of the command is then displayed on the web page, allowing the attacker to view the results of their command.

The '2>&1' in the command redirects any error messages to the standard output stream, allowing the attacker to see both the output and any error messages generated by their command.

With this in mind, let's select that file to upload. In this case, I have named it "shell.php":

Now, let's hit Upload and see if it uploads successfully:

It does. Perfect. We can see it uploaded successfully to that same avatars folder and kept the same file name. Just as a precaution, we can go back to the My Account page and check:

As we can see our image is now invalid since it is now actually any sort of image file, but if we check the source code, we can see that it is using that shell.php file as our profile picture inside the files/avatars directory.

With this, we can try navigating to that file directly in the browser:

Hmmm.. but we only see a blank white page. Is something wrong? Actually, no. If we go back to that PHP code, we need to use a GET request, specifying the "e" parameter and the command we want to run.

So, to do that, we simply add the e parameter with "?e" and then specify the command we want to run such as "whoami":

And bingo! We now have remote code execution through a web shell where we can specify various commands. Another command we may want to run is "pwd" to find out which directory we are in:

As expected, we are inside the avatars directory which is where our images got uploaded to. Since we know the end goal is to read a file inside Carlos's home directory, we can list out the contents of /home/carlos and see what is in there:

As we can see, there is a file called "secret" which is what we want to read. To read it, we can simply cat out its content:

And we get a randomised string. With this, we can go back to the main page and hit the "submit solution" button:

Once we hit OK and submit the string, we successfully complete the lab:


Lab 2 - Web shell upload via Content-Type restriction bypass

Once the lab starts, we can navigate to it and see a simple looking blog:

As with the previous lab, we can login with the credentials given and notice that there is a profile picture upload functionality we could possibly abuse:

As before, let's get a feel for how this upload functionality works under normal circumstances by uploading the same dog image again (R.jpg):

Once chosen, we can hit the Upload button and view what happens:

As expected, we see the same message telling us the image was uploaded to the avatars directory under the same filename. Again, let's double check this by checking the source code of our profile image:

We see the same familiar /files/avatars path as before indicating nothing has changed on that end. If nothing seems to be changed, let's try uploading the same PHP web shell file (shell.php):

However, once we hit Upload, this time we get a different message:

Here, the web app tells us that the file type of "application/octet-stream" is not allowed and that it only accepts "image/jpeg" and "image/png".

This is a verbose error message telling us exactly what is wrong and what it is looking for. But what the hell is application/octet-stream and image/jpeg? Where do these go? To answer that, let's take a look at the POST request for uploading our PHP shell in Burp Suite:

And as we can see on line 23 and 24, we see some type of headers being sent alongside this uploaded file - them being Content-Disposition and Content-Type.

Feel free to research these on your own, but the interesting part for us is the Content-Type header where we see that application/octet-stream string that seemed to get picked up and block our file from uploading. What is an octet-stream though?

To put it simply, it is some kind of binary file or executable file - such as a PHP file which would match what we are uploading.

That's all good, but is there some way we can bypass this? Yes, we can simply change the Content-Type to one that is accepted (usually some sort of image like JPEG or PNG). It's important to note that this does not change the file extension of our uploaded file or manipulate it in any way.

As some sort of proof, if we take a look at the request for the valid dog image uploaded before, we can see its Content-Type is image/jpeg:

This header is simply sent to the server to tell it what type of content type to expect. If a web app is only filtering based on this header, this is very bad as we have the power to manipulate it on our client side before sending it forward.

And that's exactly what we are going to do. Even nicer for us, the web app tells us that it only accepts two content types - image/jpeg and image/png - therefore, if we change the type to one of these two, the web app should accept it (if it's insecure):

With this in mind, let's intercept an upload request:

Once intercepted, let's modify the Content-Type header and change it to image/jpeg to bypass this filter:

Once modified, we can forward the request and stop intercepting. If we take a look back at our browser, we can see that our shell.php file was uploaded successfully without error, indicating our Content-Type filter bypass worked:

Under the My Account page, we can see the same errored image icon and the source code telling us that the PHP file is located under /files/avatars/shell.php:

Again, let's navigate to it:

A blank white page. This is a very good sign that our PHP is being executed since we told it to not display anything. As before, we can now specify the ?e parameter with the command we want to execute like "whoami":

And we are carlos. With this, we can grab the contents of the secret file inside his home directory:

Once copied, let's submit the solution:

And as we can see, once submitted, we successfully complete the lab and bypass the very insecure filter methods in place on this web app:


Lab 3 - Web shell upload via path traversal

Once the lab has loaded, let's navigate to it and see what we are dealing with:

Again, we see the same blog type of site. Let's login with the provided credentials as done previously:

As we can see, the same profile picture upload functionality is present once more. Let's test it by uploading the same dog image:

We get the message telling us that it was uploaded successfully as expected. From here, let's verify that it was uploaded to the avatars directory by looking at the source code on the account page:

Same as before - the file is stored under /files/avatars. Now, from here, let's try uploading the shell.php we have done many times before:

Interestingly, it tells us it gets uploaded. Strange! Surely this lab is not this easy right? Sadly not, but let's continue on. With this information, we can check the web app didn't do anything weird to our file by checking the source code again:

Hmmm... it seems like it is stored in the same directory and it is still a PHP file. What is going on? Well, let's navigate to this file and see what happens:

Ah! As we can see, it is not actually executing the code inside as PHP, but is rather just rendering its contents like a text file and displaying it back to us. It's hard to say why it is doing this, but there is likely some sort of server-side process happening on the directory that does not execute any type of code inside files as part of a security feature.

To delve deeper, let's open Burp Suite and take a look at two interesting requests - the POST request where we upload and the GET request that is made when we to our account page:

First, let's take a look at the POST request when we upload our PHP file:

Nothing seems out of ordinary. It is using the Content-Type of application/octet-stream and specifying the filename as "shell.php". Next, let's look at the GET request that is performed when we go to the account page:

Again, nothing out of the ordinary - it simply grabs the image, pulls it down but displays our PHP code back to us as text. Something is going on.

However, if we look closely at the GET request, we can see it specifies the full request to get the profile picture (/files/avatars/shell.php):

If there is a filter on place to stop code from getting executed, maybe the developer was lazy and only set this filter in the directory where files get uploaded (avatars) but forget to impement it site-wide. In this case, we could try and chaing together 2 vulnerabilities to gain remote code execution.

If we look at the POST request, we can see that there is a filename parameter containing our filename that we could maybe inject directory traversal sequences into and upload it to a different directory:

With this in mind, let's simply add a single directory traversal sequence which, if it works, would upload our PHP file to the /files directory and escape the avatars directory:

Now, let's send it using Repeater and see what happens in our response:

Interestingly, it seems to escape these characters automatically as the message does not include the ../ when displaying our file name back to us. We've got a challenge on our hands. One of the simplest things we can try is URL encoding these characters and seeing if we can bypass it.

To URL encode "../", we can use an online encoder and see that the dots stay the same, but the forward slash gets encoded to (%2F):

With this, we can simply modify our request and change the forward slash (/) to %2F:

And with this, let's send it forward and view the response again:

Great! As we can see this time, our filename gets displayed back to us - this time including the directory traversal sequence. In this case, instead of uploading our file to avatars, if would go up one directory and instead save it to the files directory.

Just to make sure it was working correctly, we can check the source code on the account page again:

Perfect. Even in the source code, we can see the directory traversal sequence remains. Now, all that is left to do is try and navigate to it and pray the PHP code gets executed this time:

We see a blank page which is a good sign, indicating our PHP code is getting executed. As before, to run commands we need to specify the command we want to run using the "e" parameter in the URL:

And it runs successfully, returning the results of our command. From here, we can simply cat out the contents of the secret file:

Once we have this randomised string, we can hit the "Submit solution" button and input the random string we just extracted:

Finally, once submitted, we complete the lab successfully and can move on to the next:


Lab 4 - Web shell upload via extension blacklist bypass

Once the lab is spun up, we can access the lab and see the following home page:

As before, let's login and see what functionality we have:

Same as always - we see a simple account page that allows us to choose a file to upload for setting our profile picture. Before attacking, it's always good to observe its normal functionality so let's upload the same dog image as before and view what happens:

As we see, the file gets uploaded successfully and the web app tells us that it was uploaded to the avatars directory. Next, let's go back to the account page and check the source code for our newly uploaded profile page:

We see that the image is stored under /files/avatars/R.jpg. With this in mind, let's try uploading our shell.php file and see if there are any filters in place:

An error occurs simply telling us that PHP files are not allowed and that there was an error uploading our file. Unlike before, it does not tell us exactly what files it is looking for. With this, we can guess that image files are accepted such as PHP, JPG, GIF and others since it is a profile picture.

However, as we don't know exactly what type of filter is in place, we can start playing around. One thing we can do is try different PHP extensions as there are many extensions that PHP can have as seen below:

With this in mind, we can copy and paste the original "shell.php" and change its extension to things like "php4" and "php5":

Feel free to try each one, but they will all result in the same outcome. Below, I have tried with the "php5" extension. First, we select it as the file to upload:

Once the file is selected, we can hit the Upload button. When the file is sent and uploaded successfully, we get the following message below:

Interestingly, this tells us that our file was uploaded successfully. Let's try navigating to the file located at /files/avatars/shell.php5 and see if we get execution:

Sadly, we do not. It simply returns the content of the file to us instead of executing it as PHP code. This will happen with every PHP file extension. We can take a deeper look at the POST request that was made when uploading our PHP shell:

And we notice something interesting - the response tells us what kind of server is running on the backend. Here, we can see that it is running Apache and specifically Apache 2/4.4.41. With this in mind, there is a very interesting technique that allows us to speicfy our own file extensions and tell Apache to execute that new extension as an existing one such as PHP.

To do this, we need to use something called the ".htaccess" file. What does this do? Well, by reading the documentation below, it provides a way to make configuration changes on a per-directory basis:

This means that if the /files/avatars directory does not include a .htaccess file already, we can upload one and tell it what we want it to do. Reading into this further, we can find many articles and writeups detailing how we can execute PHP scripts into HTML file by modifying this file:

I'd highly encourage going out and researching this yourself as it is a fascinating topic. However, to complete this lab, let's create our own .htaccess file. To do this, we first intercept a second POST request using our shell.php file as before:

Once intercepted, we modify three fields to create this .htaccess file:

  • filename

  • Content-Type

  • the actual content itself

For the filename, we simply change the name to ".htaccess". For the Content-Type, we need to change it to a simple text file using text/plain. Finally, for the content inside the file, we remove the PHP code and instead use "AddType application/x-httpd/php .shell":

What does the line do? The line AddType application/x-httpd-php .shell inside an Apache .htaccess file is used to associate the ".shell" file extension with the PHP scripting language.

This tells the Apache web server that any file with a ".shell" extension should be parsed as PHP code and executed by the PHP interpreter. In short, we create a custom file extension and map it to PHP, allowing us to bypass known PHP extensions but still get execution using a custom extension that is likely not filtered.

With this in mind, let's forward this request:

This time, it tells us that our .htaccess file was uploaded successfully. With that file now in place, Apache should understand any file that we send with the ".shell" extension and execute it as PHP. To test this, we can grab and upload the same shell.php file as before:

Then, we intercept it. Once intercepted, we can simply change the filename parameter from "shell.php" to "shell.shell" before forwarding it:

Once modified, we forward it on and can see that the file is uploaded successfully:

Since Apache should execute any file with the .shell extension as PHP, our code should get executed. We can test this by navigating to the file we just uploaded:

A blank white page! This is a good sign since our PHP code is likely being executed as it is not shown back to us. As before, to execute commands, we need to specify the e parameter in the URL and the command we want to execute like whoami:

We can see that we are the Carlos user. From here, we can grab the contents of the secret file:

Once we have the random string, we can submit the solution to this lab:

And, hopefully, we get the banner dropdown telling us that we have solved the lab:


Lab 5 - Web shell upload via obfuscated file extension

Once we start the lab, we can navigate to the home page and see the same blog as before:

Again, we login with the credentials provided and see the same upload functionality we have same many times before for choosing an avatar:

As always, we upload the dog image (R.jpg) and observe normal functionality. It may be the same as before (and it is), but it's always a good idea since there could be very small changes that could be exploited.

It uploads successfully and we get the same message as before:

Just to make sure, we check the source code to see if it shows the full directory path as before:

It does and we see the same /files/avatars directory. Now, let's upload the shell.php file and see if we get an error message:

As expected, our PHP file is not uploaded. However, we get a verbose message telling us that only JPG and PNG files are allowed - so now we know what type of files we can use!

To start playing around, we can check the POST request we just made while uploading our PHP file and we can see that the filename uses the .php file extension, which is likely being filtered out:

Whilst it is likely that this lab is only checking the file extension (as indicated by the lab name), it's a good idea to make sure it is only checking the file extension. To do this, we can simply change the extension from PHP to JPG and send it forward:

If it is only looking at the file extension, this request should go through successfully since we are using a whitelisted extension. However, if it still gets blocked, then it is also looking at things like the Content-Type or even the content of the file itself.

Let's send it forward and view the response:

As we can see above, when we change the extension, we get the message telling us that our file was uploaded. Seems good, right? Maybe our PHP code gets executed? Let's check it by navigating to the /files/avatars/shell.jpg file:

We get a black web page with what appears to be a single white square. If our PHP code is being executed, we should not see the PHP source code in the source of the web page. Let's check Burp Suite and see if that is the case:

Here, we can see that the source code of the page does include our PHP code, likely indicating that our code is not getting executed, but is simply getting displayed back to us. Although, the display back to us looks weird because we changed it to a JPEG format.

From here, there are many different methods we can try. The first one we can try is using a combination of uppercase and lowercase letters for the file extension. If the filter is poorly configured, it could only be looking for a lowercase string:

With our extension now being ".PhP", we can try sending it forward and viewing the response:

No luck! We get the same error telling us that our file was not uploaded.

The next thing we can try is using a different extension like we did previously such as php5:

Once we've changed the extension, we can send it forward:

Same thing. We get the same error message. No luck.

Ok, so next we can try using a double extension such as "shell.php.jpg" and maybe it strips the PHP part and keeps the JPG extension or some other behaviour:

Once modified, let's view the response:

It seems to work. It tells us that our file shell.php.jpg was uploaded successfully. Although, it's likely that we are not going to execute the code correctly since the extension is ending in JPG and not PHP but we can see anyways:

As expected, once we submit a GET request, we can see the PHP code is simply returned back to us and is not getting executed. We made progress, but enough to get code execution.

However, what happens if we do it the other way around such as "shell.jpg.php"?

Let's send it forward and see what happens:

Sadly not. We get the error message again. This is likely because it ends in PHP and so the web app basically tells us NO and disciplines us like a bad child.

Another thing we can try is using a semicolon after the PHP extension and then adding ".jpg" to the end:

By appending ";.jpg" to the end of the filename, the file could potentially be disguised as a JPEG image file, which may allow it to bypass security checks that are designed to detect potentially malicious files.

Let's send it forward and see what happens:

Again, we get some sort of luck telling us it was uploaded successfully. As we did before, let's check if our code is getting executed or simply returned to us:

Sadly, our code looks like it does not get executed as we can see our PHP code in the HTTP response likely meaning the server did not execute it.

One of the most common ways we can bypass filters like this is by using a null byte injection where we add "%00.jpg"" after the .php:

The null byte (%00) is a special character that is used to indicate the end of a string in certain programming languages, including C and C++. However, in some cases, null bytes may not be properly handled by web applications, which can lead to security vulnerabilities.

In the case of a file upload vulnerability, an attacker could use null byte injection to modify the file extension of an uploaded file, such as changing a PHP script to appear as a JPEG image file. By appending "%00.jpg" to the end of the filename, the null byte can be used to trick the server into thinking that the file has a different extension than it actually does.

Let's send it forward and see what happens:

Bingo! It looks like we have got an interesting response. Here, we can see that it tells us that our file shell.php was uploaded and it removed the .jpg and the null byte, likely because the web app saw the null byte and stopped after reaching it.

To make sure it's actually executing this PHP code, we can navigate to the page in our browser:

And we see a blank white page. This is great as we don't see our PHP code being displayed back to us. To check, we can look at this request in Burp Suite and see the same thing - our PHP code has disappeared and is likely being executed:

From here, we can do the same thing and execute commands using the e parameter in the URL:

Now that we have code execution, we can simply cat out the secret contents:

Once we have the random string, we can submit the solution:

Finally, once we submit the solution, we complete this lab and successfully bypassed the file extension filter in place:


Lab 6 - Remote code execution via polyglot web shell upload

Once the lab starts, we access it:

As we can see, it is the same old blog website we've seen before. And, as before, we login with the provided credentials:

Once we get logged in as the wiener user, we see the same avatar upload functionality as before. Now, we can simply try uploading our shell.php file and see what happens:

We get told that our file is not a valid image. Fair enough, so let's try uploading a normal image and see what occurs:

As expected, our JPG image of our cute puppy gets uploaded successfully. Let's check to see where it got uploaded to by looking at the source code:

The same old /files/avatars directory as expected. Now, since we know that it accepts image files, we can try using a Polyglot file if all other methods fail (and they do, apart from one method explained later). But, what exactly is a polyglot file? Well, there is a fantastic article written by Vickie Lie that explains it:

And, as she puts it best in her post, they are files that are a valid form of multiple different file types:

I highly suggest reading her blog post for much more information. But let's start playing around. With our image file, we can use a tool called exiftool to inspect the metadata of the file:

Here, we can see the file named is called R.jpg and it has a MIME type of image/jpeg - as expected since this is an image. However, an interesting part of exiftool is the ability to add comments to an image using the "-comment" parameter:

With this in mind, we can simply use exiftool to add a comment with the content of "complexsecurity":

This will duplicate the file and create an original alongside our new R.jpg. However, more importantly is we can now run exiftool on R.jpg once again and see that our comment is added to the image successfully:

Interesting, right? What if we add a different comment, but this time we submit some PHP code as the comment such as:

We are simply catting out the contents of a file but we are also adding in additional text like "FILE CONTENTS" and "END OF SECRET" so we can search for that string once this command gets executed and make it easier to find the contents of the file.

Once this command is ran, we can see that the comment inside this image file now contains the PHP code we just created:

It's important to note that nothing has changed - it's still a JPEG file. If we opened the file, the image would remain the same. From here, we can now make it a polyglot file by specifying an output file called "webshell.php":

This creates a new file that is pretty much the same as before, but has a new name. If we examine it, we can see the following:

It has a file extension of ".php" but the metadata still specifies it is actually a JPG image with the MIME type of image/jpeg. This is how we trick the sanitization as it checks if we provide an image file looking at the magic bytes or the MIME type, but in reality, it will still allowed us to upload a PHP file.

From here, we can choose to upload this new "webshell.php" file:

Once selected, we hit the Upload button:

As we can see, the PHP file uploads successfully. If we take a deeper look at Burp Suite, we can see the GET request:

What's interesting here is that the MIME type comes back as HTML, but the extension states it is PHP (highlighted in red above). However, we are more interested in the response so let's check it:

Looking at the response, we see that it looks like a regular JPEG file with all the random characters that make up the image. However, take a closer look below:

We see, at the very start, the string "FILE CONTENTS" followed by some random string and then the string "END OF SECRET". Look familiar? This is the comment we inserted into this file earlier, except the PHP code that grabbed the content of a file is replaced by a random string.

This random string is the content of that file. With this in mind, we can copy this string and submit it as our solution:

Once submitted, we should get the congratulatory banner popup and we complete the lab:

Now, as a side bonus, this was the intended way to complete this lab. However, there is another, potentially easier way to complete this lab that does not involve polyglot files.

In order to understand how this works, you need to understand what magic bytes are in files. MAgic bytes are essentially a file signature. It is data used to identify or verify the content of a file and they are located at the start of a file.

Magic byte is nothing but the first few bytes of a file which is used to recognize a file. It is not visible if you open the file but can be seen using some special tools.

For example, if you were to open up a PNG file in something like a hex editor, the first bytes would look something like this - 89 50 4E 47 0D 0A 1A 0A - which would help the OS, web app or any other device/system identify it as a PNG file.

This is how Linux works as well. Notice that Linux doesn't necessarily need a file extension, but can still identify what file type something is - it uses magic bytes.

Now that you understand that, we can trick the web app to think a file is a GIF file for example by adding these magic bytes to the file ourselves whilst keeping the PHP code and extension. For example, the magic bytes for a GIF file are GIF8.

Seen below, we simply modify the shell.php file to contain a line above the PHP code containing the string "GIF8":

What's interesting about this is that if we use the "file" command on this PHP file, the Linux file system actually returns and tells us that this PHP file is actually a GIF as it has identified those starting magic bytes we injected:

With this in mind, we can upload this shell.php file to the webapp:

Once uploaded, we can intercept the request and notice that the GIF8 string is at the top of the file, but we keep the PHP file extension and the Content-Type header stays as application/octet-stream:

Because of this, the web app thinks it is a GIF image and allows us to upload it:

Once uploaded, if we look at the source code, we can see our shell.php file has been uploaded successfully:

Now, if we navigate to shell.php in the browser, we can see the following:

Because we simply inputted a string "GIF8" at the start of the file, that will show as normal text. However, our PHP code does not show up, indicating it is likely getting executed on the back end. We can confirm this by using the "e" parameter to run a command like whoami:

And we can see it returns as Carlos. From here, you could run the same command to extract the secret contents and complete the lab:


Lab 7 - Web shell upload via race condition

Once the lab starts up, we can access it and see the following home page:

And, as always, let's login using the provided credentials (wiener:peter):

Woud you look at that? It's the same upload functionality we have seen many times before. So, we test its normal functionality by uploading a valid image:

As we can see, the image gets uploaded successfully to the avatars directory. To double check this, we can look at the source code on the My Account page:

Here, we see that the image appears on this page and that the link is the same as before - /files/avatars/R.jpg. Now, from here, we can create a PHP file that will execute a command to read from a certain file (/home/carlos/secret) which is what we need to complete the lab.

The code can be seen below:

With this PHP file created, we can try uploading it to the web app as we did with the image:

Sadly, it seems as though PHP files are blocked - good for them, bad for us. If we try navigating to the shell.php file, we get an error stating the requested URL was not found as it has likely been removed:

Let's take a closer look at this in Burp Suite. With a race condition, we want to issue requests as fast as possible. It is possible that this file is getting uploaded and for a split second - maybe even in milliseconds - that file exists on the web server before the validation process finishes and deletes it.

If that is the case, then we can use a variety of methods to upload the file and then quickly use a GET request to execute the PHP code and display the contents of the file we specified. The preferred way of doing this for me is using a free plugin called Turbo Intruder.

To use this, we simply right click on the POST request uploading our PHP shell and select Extensions --> Turbo Intruder --> Send to Turbo Intruder:

Once clicked, we the following pop up:

Now, as you can see above, we can use Python code to perform these requests. The code above is what will be used for this. What does this code do? Well, let's discuss.

In summary, this code uses Turbo Intruder to exploit a file upload vulnerability that has a race condition. It does this by queuing up multiple requests with the same gate tag and then using the engine.openGate() method to send the final byte of each request at the same time, causing a race condition to occur. The handleResponse function is used to analyze the responses from the web application.

However, in order for this to work, we need to fill out the two variables - request1 and request2. Request1 should contain the POST request uploading the shell2.php file and Request2 should contain the GET request for that shell2.php file:

Once filled in, we can run this script by hitting the Attack button and waiting:

As we can see, there are 4 requests that come back with a 200 OK response - this is interesting as we found a 404 Not Found error when we tried to access our PHP file. Let's take a look at this 200 OK request:

Interestingly, when we used the GET method to run this PHP script, the file seemed to still exist on the server and therefore, the PHP code was executed and displayed the contents of that file.

Now, to understand deeper, we can see that later on, a 403 Forbidden response is sent back to us telling us that the PHP file is not allowed:

Then, it seems as if that file gets deleted after displaying that error message as we then receive a 404 Not Found message - the same one we saw earlier:

With this in mind, we can grab that random string and submit it as our solution:

Once submitted, we successfully complete the lab:

Previous
Previous

PortSwigger: All Command Injection Labs Walkthrough

Next
Next

PortSwigger: Expert Level Cross Site Scripting Labs