June 22, 2026
I Hacked a Recruitment Portal From Zero to Root : Here’s Exactly How
A walkthrough of how four “small” vulnerabilities chained together to hand me a shell on a production-grade web app
Rue
2 min read
A walkthrough of how four "small" vulnerabilities chained together to hand me a shell on a production-grade web app
I want to preface this: everything in this post is from an authorised lab environment on TryHackMe. The target is a fictional recruitment application called RecruitX , built specifically to teach realistic web application penetration testing. No real systems were touched.
But here's what makes this room different from most CTF content I've worked through: it's not about finding one magic exploit. It's about connecting dots. And that's exactly what real pentesting looks like.
Let me walk you through it.
The Brief
The client (fictional) runs RecruitX — an internal recruitment portal. Hiring managers post roles, candidates apply, admins manage everything. They suspect security issues. Your job: find them.
No hints. No "check this endpoint." Just a machine IP and a browser.
Phase 1: Reconnaissance ! Don't Skip This !
Before touching a single page, I ran an Nmap scan:
nmap -sV -sC -p- 10.128.169.241nmap -sV -sC -p- 10.128.169.241Four open ports came back:
- 22 (SSH) — useful if we get credentials
- 80 (HTTP) — the target application, Apache 2.4.58
- 3306 (MySQL) — the backend database is exposed on the network
- 8080 (HTTP) — Apache default page, likely a misconfigured virtual host
That MySQL port matters. It tells you the app is almost certainly constructing SQL queries, which shapes how you think about input fields later.
I then checked the HTTP headers:
curl -I http://10.128.169.241curl -I http://10.128.169.241PHPSESSID in the response confirmed PHP session management. So we're dealing with a classic LAMP stack — Linux, Apache, MySQL, PHP. Good to know.
Next, directory enumeration with Gobuster:
gobuster dir -u http://10.128.169.241 \
-w /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt \
-x phpgobuster dir -u http://10.128.169.241 \
-w /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt \
-x phpWhat came back was telling:
PathStatusNotes/admin301Admin panel behind auth/api301API endpoint — no login required/reset.php200Password reset page/uploads301File upload directory/profile.php302Requires authentication
Four attack surfaces, before I've even logged in. This is why enumeration isn't optional — it's the whole game.
I also hit the API directly:
curl http://10.128.169.241/api/
# {"endpoints":["\/api\/user","\/api\/jobs","\/api\/applications"]}curl http://10.128.169.241/api/
# {"endpoints":["\/api\/user","\/api\/jobs","\/api\/applications"]}An unauthenticated user getting a full map of internal API routes is already an information disclosure finding. File it. Move on.
Phase 2: IDOR — The Vulnerability Everyone Forgets
After registering a test account and logging in, I noticed the profile URL:
http://10.128.169.241/profile.php?id=6http://10.128.169.241/profile.php?id=6A numeric ID in a URL is a flashing neon sign to any penetration tester. I changed it to 1.
The page returned Sarah Mitchell's full profile — the system administrator. No access control check. No error. Just her data, handed over because I asked for it.
But the API was worse:
curl -s "http://10.128.169.241/api/user?id=1"
{
"id": 1,
"name": "Sarah Mitchell",
"email": "s.mitchell@recruitx.thm",
"role": "administrator",
"created": "2026-03-24"
}curl -s "http://10.128.169.241/api/user?id=1"
{
"id": 1,
"name": "Sarah Mitchell",
"email": "s.mitchell@recruitx.thm",
"role": "administrator",
"created": "2026-03-24"
}No session cookie required. Just an integer in a URL parameter and you get back every user's name, email, role, and account creation date. I enumerated all five users in about 30 seconds.
This is IDOR (Insecure Direct Object Reference) in its purest form. The developer assumed users would only ever access their own profile. They didn't add a server-side check. And that assumption just gave me the admin's email address.
Phase 3: Weak Password Reset — Three Flaws in One Feature
I now had Sarah Mitchell's email. The goal: get into her account.
I went to /reset.php, entered her email, and submitted the form.
The token appeared directly in the HTTP response.
Not in an email. Not anywhere private. Right there on the page.
That alone is a critical finding. But I kept testing. I submitted my own account's reset form multiple times:
- Attempt 1:
784512 - Attempt 2:
291037 - Attempt 3:
503648
Six-digit numeric tokens. One million possible values. No rate limiting. No token expiry enforced in any meaningful way.
Let me be clear about what "three flaws in one feature" looks like:
- Token exposed in the response — should only ever be sent to the account owner's email
- Weak token space — six digits means a million possibilities; a cryptographically random 32-character token is the standard
- No rate limiting — nothing stops an automated brute-force of all one million values
I generated a token for Sarah Mitchell's account, used it to set a new password, and logged in.
We're now the administrator.
Phase 4: The Admin Panel and a File Upload Filter With a Hole In It
With admin credentials, I navigated to /admin/upload.php — the file upload function Gobuster had flagged earlier.
The form's accept attribute restricted uploads to PDFs, DOCX files, and images. Classic client-side restriction. Meaningless from a security perspective. I opened DevTools, deleted the accept attribute, and uploaded a plain .txt file.
Rejected by the server. Okay — there's server-side validation too.
I tried .php: