A CV Upload to a Web Shell: Unauthenticated Remote Code Execution on a Small Agency's Careers Page
Redacted (Creative / Marketing Agency, ~15 employees)
4 days
Critical
SeverityCV Upload
Entry PointRemote Code Execution
Access Gained9.8
CVSSv3 ScoreOn This Page
Key outcome
Unauthenticated file-upload leading to remote code execution identified and contained at a single benign proof file. Upload validation rebuilt around an allow-list, uploads moved off the web root, and execution disabled in the upload directory.
Table of Contents
14
Executive Summary
A small creative and marketing agency, referred to throughout this document only as Redacted, asked XHack to test a newly rebuilt company website before promoting it. The site is mostly informational, but it includes a careers page with a "Upload your CV" form open to the public, with no login required.
XHack found that the upload form's file-type validation blocked the obvious dangerous extension, .php, but not the equally executable .phtml. The uploaded files were stored in a directory served directly by the web server, and that server was configured to execute .phtml files as PHP. By uploading a minimal, harmless proof file, XHack confirmed that arbitrary code supplied through the public form ran on the hosting server — unauthenticated remote code execution reachable by anyone on the internet.
XHack contained the test to a single benign proof file, which printed a fixed marker and the current user, captured the evidence, and deleted the file immediately afterwards. No data was accessed or removed, and no persistent access was established. The agency's developer patched the form within two days.
Client Background
The client is a fifteen-person agency. The website had been built by a small external development team and handed over shortly before the engagement. Like many small-business sites, it ran on a shared PHP hosting stack, and the same server account that served the website also held the site's database credentials and content.
The client cannot be identified beyond this description for commercial reasons.
Scope
- In scope: the production website, including all public forms and file-upload functionality
- Out of scope: the hosting provider's control panel, email, and any third-party services linked from the site
- Approach: black-box, from the position of any anonymous internet visitor, with explicit written authorisation to attempt file upload and, if successful, to confirm code execution with a single non-destructive proof
Discovery
The site's attack surface was small, so XHack examined each form carefully. The careers page accepted a CV upload:
POST /careers/apply HTTP/1.1
Host: www.redacted.example
Content-Type: multipart/form-data; boundary=----xhack
------xhack
Content-Disposition: form-data; name="cv"; filename="resume.pdf"
Content-Type: application/pdf
%PDF-1.4 ...
------xhack--
A normal PDF upload succeeded and the site returned a "thank you for applying" message. The response and subsequent page source revealed that uploaded files were stored under a predictable, web-accessible path:
https://www.redacted.example/uploads/cv/resume.pdf
That combination — a public upload plus a web-accessible storage directory — is the precondition for a file-upload-to-code-execution issue, so XHack tested how strictly the file type was enforced.
Probing the Validation
XHack worked through the usual bypasses in order, keeping every payload benign:
- Direct
.phpupload — rejected. The server returned a validation error, showing an extension check was present. - Content-Type spoofing (a
.phpfile sent asapplication/pdf) — still rejected, indicating the check looked at the filename extension, not only the declared type. - Double extension (
resume.pdf.php) — rejected, so the check inspected the final extension. - Alternate PHP extension (
resume.phtml) — accepted.
The validation was a deny-list: it blocked .php but not the other extensions that many web servers also execute as PHP, including .phtml. The upload directory's server configuration mapped .phtml to the PHP handler.
Confirming Execution (Contained)
XHack uploaded a single minimal proof file, deliberately harmless, that printed a fixed marker string and the identity of the web-server user and did nothing else:
<?php echo "XHACK-POC-9F42"; echo " | "; echo shell_exec("id"); ?>
Requesting the stored file executed it:
GET /uploads/cv/xhack_poc.phtml HTTP/1.1
Host: www.redacted.example
HTTP/1.1 200 OK
Content-Type: text/html
XHACK-POC-9F42 | uid=1002(webuser) gid=1002(webuser) groups=1002(webuser)
The fixed marker and the id output confirmed that code from the public form was executing on the server as the web user. XHack captured the request and response as evidence, deleted the proof file, confirmed it was gone (a follow-up request returned 404), and stopped. No web shell was left in place, no files were read or written beyond the single proof, and no attempt was made to escalate or pivot.
Vulnerability Properties
| Property | Detail |
|---|---|
| Vulnerability Class | Unrestricted Upload of File with Dangerous Type → Remote Code Execution |
| OWASP Top 10 | A05:2021 Security Misconfiguration (executable upload directory); outcome is remote code execution |
| CWE | CWE-434 Unrestricted Upload of File with Dangerous Type |
| CVSSv3 Score | 9.8 (Critical) |
| Attack Vector | Network |
| Authentication Required | No |
| Privilege Required | None |
| User Interaction | None |
| Impact | Arbitrary code execution on the hosting server as the web user |
Why It Happened
Two ordinary decisions combined into a critical flaw. First, the upload validation used a deny-list — "block the bad extensions" — which is impossible to get right, because the set of extensions a web server will execute is larger and more surprising than most developers expect, and .phtml is a common omission. Second, the uploaded files were stored inside the web root and served by a server configured to execute scripts there, so an executable file did not merely land on disk — it became a reachable URL that ran on request.
Either decision alone would have been much less dangerous. An allow-list that only permitted document types would have stopped the upload. Storing files outside the web root, or disabling script execution in the upload directory, would have made a slipped-through file inert. Neither safeguard was present, so the deny-list gap became direct code execution.
Impact Assessment
Remote code execution on the hosting server is close to the worst outcome a website can have, and on shared hosting the blast radius extends past the site itself:
- Full control of the website. An attacker could deface the site, modify pages, or plant persistent malware served to every visitor.
- Database and credential exposure. The web user could read the site's configuration files, which held the database credentials, giving an attacker access to any personal data the site stored, including applicant details submitted through the very same careers form.
- Malware and phishing hosting. A compromised, legitimate business site is valuable to attackers as a host for malware or phishing pages that inherit the domain's reputation.
- Lateral movement on shared hosting. Depending on the hosting configuration, code execution as the web user can expose other content or sites in the same account, and provides a foothold to probe for privilege escalation on the host.
- Reputational damage disproportionate to the company's size. For a small agency whose product is, in part, its own credibility, a visibly hacked website is a direct commercial harm.
Because the entry point was unauthenticated and internet-facing, exploitation required no account, no social engineering, and no user interaction. Any automated scanner probing public upload forms could have found it.
Remediation
Immediate (Within Two Days)
- Replace the deny-list with a strict allow-list. The form now accepts only document types appropriate for a CV (
.pdf,.doc,.docx), validated by both extension and verified file content, and rejects everything else. - Move uploaded files off the web root. CVs are now stored in a directory the web server does not serve, and are retrieved only through an authenticated internal handler, so an uploaded file is never a directly reachable URL.
- Disable script execution in the upload directory as defence in depth, so any file that ever does land there cannot be executed by the web server.
Short-Term (Within the Week)
- Randomise stored filenames and strip the original extension, removing the attacker's ability to predict or control the stored path.
- Add an upload size limit and basic content validation to reject files that do not actually parse as the document type they claim to be.
- Review the hosting configuration with the provider to confirm the site runs with the least privilege necessary and cannot reach neighbouring content on the shared account.
Longer-Term
- Adopt secure-upload defaults for any future form: allow-list validation, storage outside the web root, no execution in upload paths, and randomised names — as a standard the development team applies by default.
- Re-test after any change to the site's forms, since upload handling is a frequent source of regressions.
- Consider a web application firewall in front of the site to add a layer of detection for upload-based attacks, without relying on it as the primary control.
Outcome
The developer rebuilt the upload validation around an allow-list, moved CV storage off the web root, and disabled execution in the old upload directory, all within two days of disclosure. A re-test by XHack confirmed that .phtml, .php, and other executable extensions were rejected, that uploaded files were no longer reachable as URLs, and that the previously exploitable path returned nothing executable.
The engagement touched exactly one benign proof file, which was removed immediately after confirming execution. No applicant data was accessed, and the site's logs showed no prior exploitation before the fix.
This is a small website, and that is precisely the point. Unauthenticated file upload leading to remote code execution is not an exotic, enterprise-only problem — it is one of the most reliable ways attackers turn an ordinary public form into a foothold, and it shows up most often on the small, quickly-built sites that assume they are too modest to be a target. Automated attackers do not make that distinction. A public upload form deserves the same care whether it sits on a Fortune 500 portal or a fifteen-person agency's careers page.
Engagement details
Redacted (Creative / Marketing Agency, ~15 employees)
Marketing / Creative Services
Web Application VAPT
4 days
2026
Start your engagement
Get a Web App VAPT
XHack delivers the same rigorous methodology behind every case study. Let us pressure-test your defences.
Get a Web App VAPT