TryHackMe Web Linux Medium

Recruit Portal

Exposed mail log leaks an internal email. SSRF via file:// reads credentials from config.php. Authenticated dashboard has raw SQLi — dumped with sqlmap for full admin compromise.

PlatformTryHackMe
DifficultyMedium
OSLinux (Ubuntu)
StackApache · PHP · MySQL
00

Kill Chain

Phase 1 — Recon
Nmap ffuf directory enum /mail/ directory found mail.log — internal email
Phase 2 — SSRF → Credential Extraction
API docs — /file.php?cv= file:// URI — no restriction config.php read hr:$PASS
Phase 3 — SQLi → Admin
Login as HR dashboard.php?search= SQLi sqlmap — recruit_db dump admin:$PASS Admin flag
Recon Credentials Access Exploitation
01

Recon & Enumeration

Port scan

shell
sudo nmap -sS -sC -sV -p- -T4 10.113.159.163

Three open ports: 22 (SSH — OpenSSH 8.2p1), 53 (DNS — ISC BIND 9.16.1), 80 (HTTP — Apache 2.4.41). The web server is the only real attack surface here — SSH needs credentials and DNS isn't usually a starting point on a web challenge.

Directory enumeration

shell
ffuf -w /usr/share/wordlists/seclists/Discovery/Web-Content/big.txt \ -u http://10.113.159.163/FUZZ \ -e .php

Several interesting hits come back immediately:

200
api.php — API documentation page. Worth reading carefully.
301
mail/ — directory listing or log files. Not linked from anywhere on the site.
302
dashboard.php — redirects to login. Exists but requires auth.
200
config.php — returns empty body (0 bytes). File exists server-side but outputs nothing directly — PHP is executing it, not serving it as text.
200
file.php — returns a short message. Combined with the API docs, this becomes the SSRF vector.
config.php returning 0 bytes is a common indicator of a PHP config file — it's being executed by the interpreter which produces no output, but the file clearly exists. This means we can't read it directly through the browser. We'll need another way to retrieve its raw contents.

Vhost enumeration

shell
gobuster vhost -u http://recruit.thm \ -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt \ --append-domain \ --exclude-length 291,292,293,294,295,296,297,298,299,300,302 \ --exclude-status 302

No useful vhosts found — all responses are 400. The attack stays on the main domain.

02

Log Analysis — Intelligence Gathering

The /mail/ directory found during enumeration is accessible without authentication. Inside: mail.log — a Postfix mail server log file left in the web root.

browser/mail/mail.log
mail.log exposed at /mail/mail.log showing internal HR email with credentials location hint
Internal email from HR to IT Support visible in the Postfix log. The message body contains two critical pieces of intelligence.

The email body reveals two things directly:

Intel from the log:
① HR credentials (username: hr) are stored in config.php — "for ease of access during the initial rollout phase"
② Admin credentials are not in config files — they're in the backend database

This tells us the exact path of the next two steps: read config.php to get HR access, then hit the database for admin creds.
Why log files leak: Postfix logs to /var/log/mail.log by default. Someone configured the web root to include the mail/ directory — or symlinked the log there — and Apache serves it as a static file. This is a misconfigured web root combined with overly verbose logging that includes email body content.
03

SSRF → Reading config.php

API documentation

browserapi.php — endpoint documentation
Recruit API FAQ page showing /file.php?cv=URL endpoint for fetching candidate CVs
The API documentation openly documents the /file.php?cv=<URL> endpoint. Crucially, it doesn't mention any protocol restrictions.

The API docs document an endpoint designed to fetch candidate CV files by URL: /file.php?cv=<URL>. The intended use is fetching remote HTTP URLs. The vulnerability: the server makes this request itself — and doesn't restrict the file:// protocol, meaning we can read files from the server's own filesystem.

SSRF explained: Server-Side Request Forgery occurs when an application fetches a URL on behalf of the user without restricting what URLs are allowed. When file:// URIs are permitted, the server reads local filesystem paths and returns their contents — turning SSRF into arbitrary local file read. The request comes from the server itself (127.0.0.1), bypassing any network-level restrictions.

Exploitation

shell
curl -s "http://recruit.thm/file.php?cv=file:///var/www/html/config.php"
browserconfig.php contents via SSRF
config.php raw PHP source exposed via SSRF showing HR_PASSWORD = $PASS
config.php source returned in full. HR_PASSWORD = '$PASS' — exactly where the log email said it would be.

The raw PHP source of config.php is returned. The relevant section:

config.php — extracted
/* HR Credentials (Temporary — Initial Rollout Phase) */ /* NOTE: stored here temporarily, will be moved to database in a future release */ $HR_PASSWORD = '$PASS';
Credentials: username: hr · password: $PASS — exactly as hinted in the log email.
04

SQL Injection — Dumping Admin Credentials

Finding the injection point

Logging in as hr:$PASS gives access to the candidate dashboard at /dashboard.php. The dashboard has a search field — a classic target for SQL injection testing. Sending a single quote ' as the search query through Burp Suite produces a raw MySQL error in the response.

burp suitedashboard.php?search=' — SQL error
Burp Suite showing GET request with search=' and SQL error response: You have an error in your SQL syntax near '%'
Single quote breaks the query. MySQL error returned verbatim — error-based injection confirmed. The HR flag is also visible on this page.

The error message is returned directly to the client — this is error-based SQL injection, the most straightforward type to exploit. The application is passing the search parameter directly into a SQL query without parameterisation.

Why this happens: Instead of using prepared statements, the application likely builds the query like:
SELECT * FROM applicants WHERE name LIKE '%{search}%'

When search = ', the query becomes malformed SQL and MySQL returns a syntax error — which the application helpfully displays to the user. This both confirms the injection and tells us the database type and version constraints.

Automated exploitation with sqlmap

With the injection confirmed and a valid session cookie (PHPSESSID), sqlmap can automate the full extraction. The --cookie flag is essential — without it, sqlmap hits the login redirect and never reaches the vulnerable endpoint.

shell — step 1: enumerate databases
sqlmap -u "http://recruit.thm/dashboard.php?search=test" \ --cookie="PHPSESSID=ltjjo4vl5revk9ln8h97i1o1gi" \ --dbs --batch --delay=1 # Found: recruit_db
shell — step 2: enumerate tables
sqlmap -u "http://recruit.thm/dashboard.php?search=test" \ --cookie="PHPSESSID=ltjjo4vl5revk9ln8h97i1o1gi" \ -D recruit_db --tables --batch --delay=1 # Found: users
shell — step 3: dump credentials
sqlmap -u "http://recruit.thm/dashboard.php?search=test" \ --cookie="PHPSESSID=ltjjo4vl5revk9ln8h97i1o1gi" \ -D recruit_db -T users \ -C username,password \ --dump --batch --delay=1
terminalsqlmap — dump output
sqlmap output showing boolean-based blind, error-based, time-based blind and UNION query injection types confirmed, then dumping admin:admin@001admin from recruit_db.users
sqlmap identifies four injection types and dumps the users table. One entry: admin / $PASS.

sqlmap identifies and uses four injection techniques simultaneously — boolean-based blind, error-based, time-based blind, and UNION query. The users table contains one entry:

Admin credentials: username: admin · password: $PASS
05

Flags

Logging in as hr reveals the HR flag directly on the dashboard. Logging in as admin with the dumped credentials shows the Admin flag.

browseradmin dashboard — admin flag
Recruit admin dashboard showing ADMIN Flag: THM{FLAG} and full candidate applications table
Admin dashboard. THM{FLAG} — full compromise confirmed. The candidate table shows the full application data.
flags
HR Flag : THM{FLAG} Admin Flag : THM{FLAG}
Chain complete. Log exposure → SSRF file read → HR credentials → authenticated SQLi → sqlmap dump → admin credentials → full compromise. ✓ HR flag ✓ Admin flag
06

Key Takeaways

01
Log files in the web root are a critical misconfiguration. /mail/mail.log was accessible without auth and contained an internal email with a direct credential hint. Log files belong in /var/log/, not served by Apache. Check your web root for anything that shouldn't be there.
02
SSRF without protocol restriction = local file read. The file:// URI scheme lets the server read its own filesystem. Any endpoint that fetches user-supplied URLs must have an explicit allowlist of protocols (HTTP/HTTPS only) and domains. A denylist will always be bypassed.
03
Context from reconnaissance shapes the attack. The log didn't give the password — it gave the file path. That path guided the SSRF payload. Reading every exposed file carefully before moving to exploitation often saves hours of blind fuzzing.
04
Error messages are attacker intelligence. The raw MySQL error in the dashboard response confirmed injection, identified the database type, and revealed query structure. Applications should never display database errors to users — catch exceptions server-side and return a generic message.
05
Always pass --cookie to sqlmap for authenticated endpoints. Without the session cookie, sqlmap hits the login redirect (302) and reports no injection — a false negative. Capture the session in Burp first, then pass it to sqlmap.
06
Use prepared statements. The entire SQLi chain collapses if the search parameter uses PDO::prepare() or mysqli_prepare(). Parameterised queries make this category of vulnerability structurally impossible regardless of what the user submits.