Web Hacking — File Upload¶
File upload vulnerabilities happen when an application lets a user upload a file but doesn't properly restrict what kind of file gets accepted or where/how it can later be accessed — if an attacker can get server-side code (like a PHP script) onto the server and then get the server to execute it, that's remote code execution (RCE). This page covers both real practical examples of exploiting upload-triggered shells, and a hands-on progression through increasingly strict upload-validation code, showing exactly which checks a secure implementation needs and how each weaker version gets bypassed.
For the underlying shell theory (staged vs non-staged, reverse vs bind shell, payload generation), see Shells.
Reverse shell via file upload — practical examples¶
picoCTF file upload RCE¶
- Severity: critical (10/10).
- Target: picoCTF — challenge 482
- What this is: exploiting the file-upload mechanism on the site to upload malicious code, then execute it.
Chain overview:
flowchart TD
A["Upload unrestricted<br>.php file"] --> B["Locate uploaded file<br>on the server"]
B --> C["Execute commands<br>via the webshell"]
C --> D["sudo -l -> no restrictions"] --> E["Read /root -> flag"]
Security implication:
- Full access of the machine was taken.
- Data leak — the machine is within the attacker's hands.
Analysis:
- Why test the upload mechanism first: before anything else, check whether the upload field is sanitized at all — can any file type be inserted? This was tested by uploading a PHP file.
- It appeared that the mechanism accepted anything, so a malicious PHP webshell was uploaded (a minimal command-execution snippet that runs whatever is passed in the
cmdrequest parameter through the system shell). - Why locate the uploads folder next: the webshell is useless until we know the URL path it landed at — once uploaded, the next step was figuring out where the
uploads/folder actually is. - The uploaded file was found at
uploads/cmd.php. - Why check
sudo -lnext: now that command execution is possible via the webshell, the natural next question is what can this user run as another user/root —sudo -lis the fastest way to answer that.
- The machine had no restrictions on
sudo— this is a major win, so the root directory was accessed directly:
- The flag was found in the root directory and read with
cat.
Recommendations:
- Add sanitization to the upload mechanism (validate file type/extension server-side, not just by name).
- Restrict
sudoprivileges for all users on the system — this machine already had root-levelsudoaccess for the compromised account, which is what turned a webshell into full root.
Reverse shell via JetBrains CVE-2024-27198¶
- Task: reach the user's home folder and find
flag.txt, achievable only via a reverse shell — required enumeration and scanning first to figure out the path in.
Chain overview:
flowchart TD
A["Nmap enum<br>SSH, HTTP, IBM-DB2"] --> B["JetBrains login page<br>on IBM-DB2 port"]
B --> C["Old version identified<br>-> CVE-2024-27198"]
C --> D["Metasploit RCE exploit"] --> E["Reverse shell -> flag"]
- Enumeration phase — Nmap scan of the target found 3 open ports: SSH, HTTP, and IBM-DB2.
- HTTP showed a page "under maintenance" with nothing useful in the browser inspector;
gobusterwas run against it while pivoting to the other ports. - The IBM-DB2 port turned out to host a login page related to JetBrains — the version shown was old, which is the first thing worth checking for a matching exploit.
- Searching turned up an exact-version exploit: CVE-2024-27198, which allows RCE against the victim's machine.
- The exploit was run via Metasploit (options set, then executed) and returned a reverse shell.
- Flag:
THM{faa9bac345709b6620a6200b484c7594}
Reverse shell practical example (blind reverse shell via revshells.com)¶
Chain overview:
flowchart TD
A["Nmap scan"] --> B["Port 8000<br>needs 'basic connection'"]
B --> C["revshells.com<br>python payload"]
C --> D["nc listener<br>catches shell"]
D --> E[".git folder found<br>-> credentials"]
E --> F["SSH login<br>as found user"]
- Initial network scan with
nmapfound a service on port 8000 that responded with a hint about needing "a more basic connection" — not immediately clear what that meant, so external research (Google, then AI assistance) was used. - A blind reverse shell was attempted using Python, generated via revshells.com — the first payload option didn't work, but a second one did.
- Why set up the listener first: a reverse shell payload is useless without something on the attacker side ready to catch the incoming connection — the listener has to be running before the payload executes.
- After triggering the payload, the shell connected back to the listener.
- Why search the filesystem next: once inside, the goal shifts to finding anything of value — credentials, passwords, or the flag.
- A
.gitfolder was found inside adevdirectory, and searching through it revealed credentials for a user (found in the git history). - Why try those credentials over SSH: SSH was one of the open ports identified during recon, and reusing freshly-found credentials against every other open login service is standard practice.
- The same credentials worked over SSH, giving full access and the user flag.
File upload project — validation bypass walkthrough¶
This is a standalone, self-built file-upload testbed (not an HTB/THM machine) used to understand file-upload security from the defender's side: start from a completely open upload endpoint, add one security control at a time, and try to bypass each one. See Labs → File Upload Project (Udemy bug bounty) for the real-world bug-bounty case study that used the same bypass techniques against a live target.
First attempt to upload an RCE¶
- The first payload tried was a simple PHP command-execution webshell (a script that runs whatever is passed in the
cmdparameter throughsystem()). - It worked immediately — the upload endpoint had no validation at all. The vulnerable upload handler simply took the uploaded file and moved it straight into a public
uploads/directory usingmove_uploaded_file(), with no check on filename, extension, or content — any file type, including server-executable scripts, was accepted and stored where it could later be requested directly over HTTP.
Second attempt to upload an RCE¶
- The application was hardened to block PHP-executable extensions outright: the handler now lower-cased the file's extension and rejected it up front if it matched a blocklist of known PHP-executable extensions (
php,php3,php4,php5,php7,phtml,phps) before ever callingmove_uploaded_file(). - Why try
.php5next: the blocklist explicitly includesphp5, but the underlying question was whether the web server was even configured to execute that extension as PHP — a blocked-by-name extension that the server doesn't execute anyway isn't useful, so this needed testing regardless. - A
.php5file uploaded successfully (bypassing the blocklist check as coded), but opening it just showed the raw file — the file wasn't being executed as PHP by the server. - Why upload an
.htaccessfile next: since the server wasn't treating.php5as executable PHP by default, an Apache config override was needed to explicitly map that extension to the PHP handler.
- With the
.htaccessoverride in place, the.php5webshell executed successfully.
Third security modification¶
- The validation was tightened again to only accept
.jpg/.jpeg/.png, with several layers of checking: extension allowlist, a weak double-extension check, file size limit, client-supplied MIME type check, and a "magic bytes" check that only inspects the first byte of the file against a single expected value per type (0xFFfor JPEG,0x89for PNG). - This is more secure than the previous attempts, but checking only the first byte of the file signature is trivial to satisfy while still smuggling PHP code inside — a real image's first byte can be reused on a file that is mostly attacker-controlled content after that first byte.
- Why the file must be a real
.pngwith a real PNG header: since the check now validates the actual file signature (not just the extension), the malicious payload has to be embedded inside a file that still starts with valid PNG magic bytes — this was done by injecting PHP code into a PNG's EXIF comment field usingexiftool, writing the command-execution snippet into the image'sCommentmetadata field and saving the result with a double extension (exploit.png.php). - Once uploaded, the embedded PHP still executes when the file is requested with a
.phpextension — the double-extension trick means the server executes it as PHP (matching on the final.php), while the earlier bytes of the file remain valid PNG data, satisfying the magic-bytes check. - Confirmed via
curl, requesting the uploaded file with acmdquery parameter, that the injected command executed successfully — three separate validation layers (blocklist,.htaccess-gated extension check, and magic-bytes/MIME check) were each bypassed in turn.
Bug Bounty File Upload (Udemy)¶
- Target: Udemy's support-chat file-upload feature.
- Any file/image could be uploaded, but before testing payloads, the backend language needed to be identified — Wappalyzer indicated the backend might be Ruby (see Information Gathering → Website technologies).
- A
.phpfile with a reverse shell payload was tried first (exploit.php) — this didn't work. - A wide set of alternate PHP-executable extensions was tried next, none of which worked:
- Null-byte-style extension tricks were tried too, with an inconclusive result (neither blocked nor clearly uploaded):
- Why pivot to a Ruby payload: since Wappalyzer suggested a Ruby backend, a PHP payload was never going to execute server-side regardless of extension tricks — the payload language needs to match the backend language. A PNG was crafted with an embedded Ruby reverse-shell one-liner in its EXIF comment field (the same EXIF-injection idea as the earlier PNG bypass, but with a Ruby payload instead of PHP — using
exiftoolto write a Rubysocket-based shell spawn into theCommentmetadata of a real PNG, targeting the attacker's IP and port). - The file uploaded successfully, but the listener caught nothing.
- Curling the returned image URL showed the file was found — but examining the URL revealed the upload wasn't even served from Udemy's own domain, it was served from a separate third-party asset host (
p23.zdusercontent.com), which is why the injected code never got executed server-side on a host the attacker could reach.
Outcome
This particular target didn't yield code execution — uploaded content was offloaded to a separate static-asset domain rather than processed/served by the application itself, which is a mitigation in its own right (uploaded files never touch executable-capable infrastructure).
Source completeness
A small number of exact payload strings from the original notes (the raw PHP webshell one-liners and the Ruby EXIF reverse-shell command) are described here rather than quoted verbatim, since the exact literal text repeatedly triggered this machine's local antivirus/file-scanning to quarantine the page during writing. The underlying technique, tool, parameters, and logic in every case are reproduced faithfully from the source notes — see Shells for the general payload-generation approach (revshells.com) used elsewhere in these notes.