Challenge Analysis & Passive Reconnaissance

A careful reading of the challenge description already provides valuable information before even opening a browser. The challenge describes a recruitment web application with two distinct access levels: HR and Administrator.

None

With that in mind, I decided to start with passive reconnaissance, manually exploring the web interface before resorting to automated tools. This decision was intentional:

In a real environment with an active SOC, tools like nmap, nikto, or dirbuster generate easily detectable logs. The passive approach reduces noise and allows information gathering without triggering alerts.

API Enumeration — LFI Discovery

While browsing the application, I found a button linking to the API documentation — accessible without authentication. Public APIs with no access control are rich sources of information about the application's internal structure.

The documentation exposed a single endpoint:

None
GET /file.php?cv=<URL>

The cv parameter accepts a URL as input. This immediately raised two hypotheses:

  • RFI (Remote File Inclusion): the server makes a request to an external URL
  • LFI (Local File Inclusion): the server reads a local file using schemes like file://

🔍 RFI Test

To validate the RFI hypothesis, I sent an external URL pointing to a random image. The response was:

None
"Only Local Files are allowed"

RFI hypothesis discarded. However, the error message confirmed that the server processes the URL internally, indicating LFI.

Internal File Reading via LFI

With LFI confirmed, the natural first target would be /etc/passwd to enumerate system users. However, the response was:

None
"Access Denied"

The server has some access control blocking sensitive OS files. I redirected focus to application source files, which typically reside at /var/www/html/ on Linux servers running PHP.

🔍 Exploitation

GET /file.php?cv=file:///var/www/html/config.php

The file was returned successfully, exposing hardcoded credentials in the source code:

None
$HR_PASSWORD = 'hrpassword123';

⚠️ Storing credentials in plaintext inside configuration files is a critical security violation. The correct approach is to use environment variables or secret managers such as HashiCorp Vault or AWS Secrets Manager.

HR Panel Access

With the password in hand, I still needed a username. The configuration file didn't expose one. The options were:

  1. Brute force on the login page — discarded due to the noise it would generate in a real environment with active monitoring
  2. Contextual inference — since this is an HR portal, I tried obvious credentials based on the application's context

I tested the username hr combined with the discovered password.

Access granted.

None

🚩 Flag 1 obtained.

SQL Injection on the Dashboard

Authenticated as HR, I mapped the available features. The dashboard displayed a candidates table with a search bar — search fields are classic SQL Injection targets, as they frequently build dynamic queries by directly concatenating user input into the SQL statement without proper sanitization.

🔍 SQLi Confirmation

I inserted a single quote to break the SQL syntax:

None
GET /dashboard.php?search='

The application returned a verbose MySQL error:

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

This confirmed three critical pieces of information:

  • ✅ SQL Injection present
  • ✅ Database engine: MySQL
  • ✅ Verbose errors exposed in production

🔍 Manual Enumeration — Database Name

Observing the dashboard table with 4 visible columns, I built the appropriate UNION SELECT payload:

None
' UNION SELECT database(),NULL,NULL,NULL-- -

Result: recruit_db

🔍 Automation with sqlmap

With the database identified, I used sqlmap to automate table and data extraction:

None
# List tables
sqlmap -u "http://TARGET/dashboard.php?search=test" \
  --cookie="PHPSESSID=<session>" \
  -D recruit_db --tables --batch
# Dump users table
sqlmap -u "http://TARGET/dashboard.php?search=test" \
  --cookie="PHPSESSID=<session>" \
  -D recruit_db -T users --dump --batch

The users table returned credentials stored in plaintext, including the administrator's credentials.

⚠️ Passwords should never be stored in plaintext. The correct approach is to use secure hashing algorithms such as bcrypt or Argon2.

Admin Access

With the administrator credentials extracted from the database, I accessed the application's admin panel.

Logged in as administrator.

🚩 Flag 2 obtained.

None

Vulnerability Impact Mitigation LFI via URL parameter Internal server file read Validate and sanitize input; block file:// and php:// schemes Unauthenticated public API Attack surface exposure Enforce authentication on all sensitive documentation Hardcoded credentials Plaintext password exposure Use environment variables or secret managers Verbose errors in production Database information disclosure Disable display_errors in production environments SQL Injection Full database dump Use Prepared Statements and parameterized queries Plaintext passwords in database Full account compromise Hash passwords with bcrypt or Argon2

References