By @xbytemx
De esta manera, hemos pasado el filtro y
tendremos los puertos para hacer el port knocking!!!
Descripcion:
Ahora que ya sabes como utilizar el downloader cargad0r, descarga del
directorio raiz de dicho servidor web interno el binario de nombre bunkerClient
Este binario es un port knocker, esto es, manda una secuencia de puertos al
servidor en el Bunker para habilitar temporalmente (5 minutos) el acceso a 2
puertos restringidos. Tu tarea es hacerle ingenieria inversa a dicho binario y encontrar las llaves
que permiten descifrar la secuencia de puertos para habilitar MySQL y el
servicio ELITE respectivamente. TU FLAG sera la contatenacion de ambas llaves descifradas en este orden:
<llave_MySQL><llave ELITE> **** IMPORTANTE **** Al resolver este reto, debes seguir una de las 2 siguientes trayectorias,
ambas te llevaran a dentro del BUNKER por lo que no es necesario resolver las 2 Guardian (Pwn 400) o DrunkTillPwan (Web 400)
Recibimos un binario el cual tiene la
siguiente salida via file:
xbytemx@laptop:~/dev/reto300$ file bunkerClient
bunkerClient: ELF 64-bit
LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter
/lib64/ld-linux-x86-64.so.2,
BuildID[sha1]=3fd00d66ade317de7c674f5fc3f2da99f0c7fa04, for GNU/Linux 3.2.0,
not stripped
|
Observamos que se trata de un binario
ELF64, enlazado y not stripped. Si lo ejecutamos tenemos lo siguiente:
xbytemx@laptop:~/dev/reto300$ ./bunkerClient
Modo de uso: bunkerClient [MYSQL|ELITE]
<llave>
|
Nos indica que recibe argumentos, entonces
hacemos las siguientes pruebas:
xbytemx@laptop:~/dev/reto300$
./bunkerClient asdasd
El parametro de conexión es incorrecto
xbytemx@laptop:~/dev/reto300$
./bunkerClient asdasd asdasd
El parametro de conexión es incorrecto
xbytemx@laptop:~/dev/reto300$
./bunkerClient asdasd asdasd asdasd
El parametro de conexión es incorrecto
xbytemx@laptop:~/dev/reto300$
./bunkerClient asdasd asdasd asdasd asdasd
El parametro de conexión es incorrecto
xbytemx@laptop:~/dev/reto300$
./bunkerClient MYSQL asdasd
Llave incorrecta
xbytemx@laptop:~/dev/reto300$
./bunkerClient MYSQL asdasd asdasd
Llave incorrecta
xbytemx@laptop:~/dev/reto300$
./bunkerClient MYSQL asdasd asdasd asdasd
Llave incorrecta
|
Iniciamos con el análisis estático con un strings el cual nos
devuelve la siguiente información interesante:
u/UH
Llave inH
correctaH
[]A\A]A^A_
Modo de uso: bunkerClient [MYSQL|ELITE]
<llave>
localhost
Cannot resolve hostname
MYSQL
ELITE
El parametro de conexi
n es incorrecto
Cannot open socket
hitting tcp localhost:%d
Open Sesame?
;*3$"
|
Tenemos el mensaje de sin argumentos, el de
parámetros incorrectos y el de llave incorrecta. Adicionalmente vemos un hitting
tcp y un open sesame. Ninguno de flag o marca de flag.
Función main
Lo que vemos a continuación es como se
carga en dos partes del stack diferentes, dos arreglos de bytes.
En la primera parte tenemos desde RBP-0x80
hasta RBP-0x44, y en la segunda de RBP-0xc0 hasta RBP-0x84.
[0x000013d9]> pdf
┌ 889: int main (int
argc, char **argv, char **envp);
│
; var int32_t var_3eh @ rbp-0x3e
│ ; var int32_t var_3ch @ rbp-0x3c
│ ; var int32_t var_40h @ rbp-0x40
│ ; var int32_t var_2ch @ rbp-0x2c
│ ; var int32_t var_28h @ rbp-0x28
│ ; var int32_t var_22h @ rbp-0x22
│ ; var int32_t var_4h @ rbp-0x4
│ ; var int32_t var_10h @ rbp-0x10
│ ; var int32_t var_20h @ rbp-0x20
│ ; var int32_t var_84h @ rbp-0x84
│ ; var int32_t var_88h @ rbp-0x88
│ ; var int32_t var_8ch @ rbp-0x8c
│ ; var int32_t var_90h @ rbp-0x90
│ ; var int32_t var_94h @ rbp-0x94
│ ; var int32_t var_98h @ rbp-0x98
│ ; var int32_t var_9ch @ rbp-0x9c
│ ; var int32_t var_a0h @ rbp-0xa0
│ ; var int32_t var_a4h @ rbp-0xa4
│ ; var int32_t var_a8h @ rbp-0xa8
│ ; var int32_t var_ach @ rbp-0xac
│ ; var int32_t var_b0h @ rbp-0xb0
│ ; var int32_t var_b4h @ rbp-0xb4
│ ; var int32_t var_b8h @ rbp-0xb8
│ ; var int32_t var_bch @ rbp-0xbc
│ ; var int32_t var_c0h @ rbp-0xc0
│ ; var int32_t var_44h @ rbp-0x44
│ ; var int32_t var_48h @ rbp-0x48
│ ; var int32_t var_4ch @ rbp-0x4c
│ ; var int32_t var_50h @ rbp-0x50
│ ; var int32_t var_54h @ rbp-0x54
│ ; var int32_t var_58h @ rbp-0x58
│ ; var int32_t var_5ch @ rbp-0x5c
│ ; var int32_t var_60h @ rbp-0x60
│ ; var int32_t var_64h @ rbp-0x64
│ ; var int32_t var_68h @ rbp-0x68
│ ; var int32_t var_6ch @ rbp-0x6c
│ ; var int32_t var_70h @ rbp-0x70
│ ; var int32_t var_74h @ rbp-0x74
│ ; var int32_t var_78h @ rbp-0x78
│ ; var int32_t var_7ch @ rbp-0x7c
│ ; var int32_t var_80h @ rbp-0x80
│ ; var int32_t var_14h @ rbp-0x14
│
; var int32_t llave_insertada @ rbp-0xd0
│ ; var int32_t arg_c @ rbp-0xc4
│ ; arg signed int argc @ rdi
│ ; arg char **argv @ rsi
│ ; DATA XREF from entry0 @ 0x114d
│ 0x000013d9 55 push rbp
│
0x000013da 4889e5 mov rbp, rsp
│
0x000013dd
4881ecd00000. sub rsp, 0xd0
│
0x000013e4 89bd3cffffff mov dword [arg_c], edi ; argc
│
0x000013ea
4889b530ffff. mov qword
[llave_insertada], rsi ; argv
│
0x000013f1
c745ec050000. mov dword [var_14h], 5
│ 0x000013f8 c74580a00000. mov dword [var_80h], 0xa0
│ 0x000013ff c74584ac0000. mov dword [var_7ch], 0xac
│ 0x00001406 c74588ad0000. mov dword [var_78h], 0xad
│ 0x0000140d c7458c9d0000. mov dword [var_74h], 0x9d
│ 0x00001414 c74590ad0000. mov dword [var_70h], 0xad
│ 0x0000141b c74594a70000. mov dword [var_6ch], 0xa7
│ 0x00001422 c74598a30000. mov dword [var_68h], 0xa3
│ 0x00001429 c7459ca40000. mov dword [var_64h], 0xa4
│ 0x00001430 c745a09b0000. mov dword [var_60h], 0x9b
│ 0x00001437 c745a4a50000. mov dword [var_5ch], 0xa5
│ 0x0000143e c745a8a40000. mov dword [var_58h], 0xa4
│ 0x00001445 c745ac9b0000. mov dword [var_54h], 0x9b
│ 0x0000144c c745b0a20000. mov dword [var_50h], 0xa2
│ 0x00001453 c745b49f0000. mov dword [var_4ch], 0x9f
│ 0x0000145a c745b8b50000. mov dword [var_48h], 0xb5
│ 0x00001461 c745bcaa0000. mov dword [var_44h], 0xaa
│ 0x00001468 c78540ffffff. mov dword [var_c0h], 0xa9
│ 0x00001472 c78544ffffff. mov dword [var_bch], 0x98
│ 0x0000147c c78548ffffff. mov dword [var_b8h], 0xaa
│ 0x00001486 c7854cffffff. mov dword [var_b4h], 0x99
│ 0x00001490 c78550ffffff. mov dword [var_b0h], 0xb3
│ 0x0000149a c78554ffffff. mov dword [var_ach], 0x9f
│ 0x000014a4 c78558ffffff. mov dword [var_a8h], 0x9a
│ 0x000014ae c7855cffffff. mov dword [var_a4h], 0x9c
│ 0x000014b8 c78560ffffff. mov dword [var_a0h], 0x9b
│ 0x000014c2 c78564ffffff. mov dword [var_9ch], 0x9a
│ 0x000014cc c78568ffffff. mov dword [var_98h], 0x9d
│ 0x000014d6 c7856cffffff. mov dword [var_94h], 0xa7
│ 0x000014e0 c78570ffffff. mov dword [var_90h], 0xa4
│ 0x000014ea c78574ffffff. mov dword [var_8ch], 0x9f
│ 0x000014f4 c78578ffffff. mov dword [var_88h], 0xb0
│ 0x000014fe c7857cffffff. mov dword [var_84h], 0x99
│ 0x00001508 83bd3cffffff. cmp dword [arg_c], 1
|
¿Como sabemos que son dos arreglos
diferentes? Si buscamos referencias en la función a cualquier otra variable
inicial, solo encontraremos dos:
Renombraremos de ahora en adelante estas variables
a buf1 (var_80h) y buf2 (var_c0h).
Cuando vemos la siguiente sección, tenemos
un cmp de argc > 1, el cual si es verdadero realiza un jump (bloque
derecho), sino se va por el bloque izquierdo. Este bloque izquierdo, imprime el
texto de como usar el programa y termina con estado 1 el programa.
El bloque derecho manda a llamar a
gethostbyname con el argumento localhost. La salida de esta función es
almacenada en estado_gethostbyname.
Acto seguido, el valor almacenado en
estado_gethostbyname es comparado con 0, lo cual significa que trata de validar
que el resultado sea exitoso para continuar su viaje, teniendo el jump
condicional not equal 0. Si estado_gethostbyname fue 0, nos vamos
por el bloque izquierdo, el cual carga varios argumentos a fwrite, que como
podemos ver en radare, nos imprimira en pantalla que no pudo resolver el
hostname y terminara el programa con estatus 1.
Por otra parte si estado_gethostbyname fue
diferente de 0, tomaremos el vector de argumentos y sacaremos el elemento
argv[1] para mandarlo como argumento segundo argumento a la función strcmp. El
primero corresponde al string MYSQL, el cual evalúa el resultado en las
siguientes instrucciones para realizar un jump condicional:
Nuevamente de la siguiente evaluación, si
el resultado de strcmp fue 0, el camino elegido seria el bloque de la
izquierda, mientras que cualquier otro valor diferente el bloque de la derecha.
Si tomamos el camino de la izquierda,
estaríamos llamando a la función validar_llave con los argumentos de buf1 y
argv[2]. El resultado de dicha función lo estaríamos almacenando en puertos.
En caso de tomar el camino de la derecha,
realizaríamos algo muy parecido al bloque anterior, en donde tomamos el valor
de argv[1] y ELITE, y se lo mandamos para evaluar en strcmp.
Lo interesante es que este mismo camino
abre la posibilidad a dos caminos:
- strcmp devolvió 0, entonces mandamos a llamar a validar_llave con argumentos buf2 y argv[2], guardando el valor en puertos. Justo como con el bloque anterior.
- strcmp es diferente de 0, nos imprime en pantalla ‘El parámetro de conexión incorrecto’ y el programa termina con un estado 1.
Si ELITE o MYSQL fueron enviados como
argv[1], el programa continua:
Lo que tenemos a continuación, es un ciclo
for, el cual inicializa port_counter en 0 y después compara si port_counter es
menor o igual a 3. En caso de que sea menor o igual a 3, toma el camino de la izquierda,
caso contrario toma el camino de la derecha.
Empezare por la derecha ya que es la hoja
muerta. Básicamente imprime en pantalla “Open Sesame?” y termina el programa
retornando 0 para la función main.
Si tomamos el camino de la izquierda, es
decir, port_counter menor o igual a 3, tomaremos de puertos, el index de
counter y lo salvaremos en puerto (puerto = puertos[port_counter]).
Terminando estas instrucciones, lo que
continua es la creación de un socket, el cual se guarda en estado_socket. Si el
socket fue creado correctamente, continuaremos a la derecha (estado_socket !=
-1), mientras que si estado_socket fue igual a -1, tomaremos el camino de la
izquierda.
Como colocar las instrucciones es bastante
largo, se pegara como texto y se colocaran los comentarios al final de la
linea:
0x00001676 8b45d8 mov eax, dword [estado_socket]
0x00001679 ba00000000 mov edx, 0
0x0000167e be03000000 mov esi, 3 ; F_GETFL
0x00001683 89c7 mov edi, eax
0x00001685 b800000000 mov eax, 0
0x0000168a e8b1f9ffff call sym.imp.fcntl
0x0000168f 8945d4 mov dword [fd_flags], eax
Se manda a llamar a fcntl para validar a
estado_socket (que en realidad es un file descriptor) y guarda las flags en
fd_flags. Mas información vía
“man fcntl”
0x00001692 8b45d4 mov eax, dword [fd_flags]
0x00001695 80cc08 or ah, 8
0x00001698 89c2 mov edx, eax
0x0000169a 8b45d8 mov eax, dword [estado_socket]
0x0000169d be04000000 mov esi, 4 ; F_SETFL
0x000016a2 89c7 mov edi, eax
0x000016a4 b800000000 mov eax, 0
0x000016a9 e892f9ffff call sym.imp.fcntl
Se realiza un or entre las flags y 8, con la
finalidad de habilitar el modo non-blocking en el file descriptor vía SET
0x000016ae 488d45c0 lea rax, [addr_s]
0x000016b2 ba10000000 mov edx, 0x10 ; size_t n
0x000016b7 be00000000 mov esi, 0 ; int c
0x000016bc 4889c7 mov rdi, rax ; void *s
0x000016bf e8bcf9ffff call sym.imp.memset ; void *memset(void *s, int c,
size_t n)
Se llama memset para escribir en addr_s 16
veces 0. Limpieza manual.
0x000016c4 66c745c00200 mov word [addr_s], 2
0x000016ca 488b45e0 mov rax, qword [estado_gethostbyname]
0x000016ce 488b4018 mov rax, qword [rax + 0x18]
0x000016d2 488b00 mov rax, qword [rax]
0x000016d5 488b00 mov rax, qword [rax]
0x000016d8 8945c4 mov dword [addr_s_addr], eax
0x000016db
0fb745de movzx eax, word
[puerto]
0x000016df 89c7 mov edi, eax
0x000016e1 e87af9ffff call sym.imp.htons
0x000016e6 668945c2 mov word [addr_s_port], ax
Resulta que addr_s se trata de una estructura,
la cual recibe 2 en la primera ubicación, estado_gethostbyname (la resolución
a dirección) en la segunda ubicación y el puerto en la tercera ubicación.
0x000016ea
0fb745de movzx eax, word
[puerto]
0x000016ee
89c6 mov esi, eax
0x000016f0
488d3db50900. lea rdi, str.hitting_tcp_localhost:_d ;
0x20ac ; "hitting tcp localhost:%d\n" ; const char *format
0x000016f7 b800000000 mov eax, 0
0x000016fc e86ff9ffff call sym.imp.printf ; int printf(const char *format)
Mandamos a llamar a printf con el texto
“hitting tcp localhost:%d\n”, donde “%d” es el puerto al cual nos vamos a
tratar de conectar.
0x00001701 488d4dc0 lea rcx, [addr_s]
0x00001705 8b45d8 mov eax, dword [estado_socket]
0x00001708 ba10000000 mov edx, 0x10 ; size_t addrlen
0x0000170d 4889ce mov rsi, rcx ; void *addr
0x00001710 89c7 mov edi, eax ; int socket
0x00001712 e8c9f9ffff call sym.imp.connect ; ssize_t connect(int socket, void
*addr, size_t addrlen)
Llamamos a connect usando como argumentos, el
file descriptor de socket, la estructura addr_s y la longitud 16 (misma que
usamos en memset).
0x00001717 8b45d8 mov eax, dword [estado_socket]
0x0000171a 89c7 mov edi, eax ; int fildes
0x0000171c e86ff9ffff call sym.imp.close ; int close(int fildes)
Cerramos el fd una vez que connect termino,
con lo que cerramos la conexión.
0x00001721 8b45ec mov eax, dword [timer]
0x00001724
69c0e8030000 imul eax, eax,
0x3e8
0x0000172a 89c7 mov edi, eax ; int s
0x0000172c e8cff9ffff call sym.imp.usleep ; int usleep(int s)
Multiplicamos 1000 por timer y mandamos el
resultado a usleep. Timer se trata de los segundos del timer.
0x00001731 8345fc01 add dword [port_counter], 1
Incrementamos port_counter en 1 y seguimos con
el siguiente puerto.
|
Se realizar la conexión hacia el destino
localhost:port y finalmente tras terminar los 4 eventos, se imprime el mensaje
"Open Sesame?".
Concluimos que main recibe 2 argumentos,
uno para control y otro como llave. Se envía llave a la función validar_llave y
el resultado es un array de 4 elementos, los cuales son usados para realizar
una conexión TCP hacia localhost con un delay de 5ms.
Función validar_llave
En el bloque inicial, tenemos un stack
frame de 0xa0, después guardamos los argumentos en el stack, aquí las variables
usadas son buf para cualquiera de los dos buffers enviados y llave para el
valor de argv[2].
Se realiza un malloc de tamaño 16 bytes y
se guarda la dirección en puertos.
Inicializamos shift y letra_acomulada en 0,
y guardamos dos partes de un string en dos variables, s1 y s2. Se llama a strlen que recibe el argumento
llave y se compara el resultado con 16. Básicamente se valida si la llave es de
16 caracteres.
Si la llave es diferente de 16 caracteres
tomamos el camino de la derecha, el cual carga s1 y “%s”, mandando a llamar a
printf, el cual despliega que la contraseña es incorrecta. Como en otros casos,
el programa termina con un estatus 1.
Si la llave es de longitud 16, entonces
counter_caracter es igual a 0 y iniciamos un loop.
El loop compara si counter_caracter es
menor de 16. En caso de que si sea menor, se toma el camino de la izquierda. En
caso de que sea mayor o igual a 16, se toma el camino de la derecha donde
tenemos un otra inicialización de counter_caracter y un nuevo counter,
counter_splitter.
El siguiente bloque es donde se realiza en
si, la validación de la llave.
El pseudocodigo de instrucciones seria algo como:
letra = llave[counter_caracter]
letra_acomulada & 3
shift = letra_acomulada & 3
letra_acomulada += letra
(1 << shift)
(letra – (1<<shift) -1) ^ 0xc7
|
Iniciamos var_8 en 0 y aplicamos una
operación de protección que incluye un and contra 3, un shift left, un xor
contra 0xc7 y varias aplicaciones cdqe que guardamos en el stack (ojo con la conversión a byte). Al
final hacemos un cmp entre var_98 + counter y el resultado de las operaciones
anteriores.
Si es diferente, imprimimos el mensaje de
llave incorrecta. El programa continua en incrementando el counter.
Finalmente y después de varias
conversiones, comparamos el valor del buffer contra el valor que resulta de la
operación por cada carácter de la llave.
Si es verdadero, incrementamos el contador
y iniciamos nuevamente el loop (hasta cumplir las 16 iteraciones). Si es falso,
imprimimos el mensaje de llave incorrecta y terminamos el programa. Esto para
cada carácter.
Como se menciono anteriormente, después del
ciclo for inicial, iniciaba otro for que usaba a counter_caracter y a
counter_splitter.
En este caso de hace 4 iteraciones en lugar
de 16, como podemos ver por el primer bloque de 2 instrucciones.
Luego multiplicamos 100 por el valor del
arreglo con ubicación en counter_splitter, y salvamos en ECX. Seguimos iterando
ahora como counter_splitter +1 y guardamos en EAX.
Las siguientes instrucciones nos ayudaran a
sumar el contenido de ambas direcciones RBP-70[counter_splitter]*100 y
RBP-70[counter_splitter +1] para almacenarlo en puertos[counter_caracter].
Finalmente counter_caracter se incrementa 1
y counter_splitter se incrementa 2. Esto significa que guardamos 4 datos de
tamaño de qword*4, usando el resultado del anterior for.
for (i=0, j=0; i<4;
i++, i+=2) puertos[i] = valores[j]*100 + valores[j+1]
|
Solución
Tras googlear los strings, veremos que hace
una referencia al programa knock con el open sesame. Solo que en este caso los
puertos del port knocking se encuentran protegidos por una llave, por lo que
sera necesario realizar el proceso inverso de protección o bien generar un
bruteforcer que resuelva el proceso en una vía.
Se genera un solver con angr que ingresa todos los
caracteres validos a las operaciones realizadas en el array de bytes enviados
desde main a validar_llave.
#!/usr/bin/env python3 # -*- coding: utf-8 -*- ###################################################################### # Brute force al reto 300 de HackDef 2019 # ###################################################################### import angr def solver(buff): flag='' letra_acomulada = 0 # Valor de acomulación p = angr.Project("bunkerClient") # Creamos un nuevo proyecto en angr state = p.factory.entry_state() # Inicializamos el objeto de simulación for i in range(16): # Iteramos los 16 bytes dentro de cada buffer letra = state.solver.BVS("l", 8) # Definimos el objeto bitVec l de size de un byte. shift = letra_acomulada & 3 # Iniciamos aplicando & 3 # (posibles resultados del 0-3) alloc = 1 << shift # Luego generamos un valor usando el numero anterior # (posibles resultados 1,2,4,8) candidato = (letra - alloc -1) # Usamos el valor letra y le restamos la base y uno candidato ^= 0xc7 # Aplicamos la ultima protección, un xor con 0xc7 state.solver.add(candidato == buff[i]) # Agregamos al solver de z3 final try: valor = state.solver.eval(letra) # Realizamos los eval de candidatos a letra letra_acomulada += letra # En caso de que sea valido, acomulamos letra flag += chr(valor) # Salvamos la letra valida y continuamos con la sig except angr.errors.SimUnsatError: # En caso de fallo en la simulación (unsat) print("Fallo: " +hex(buff[i])) # Imprimimos el valor que fallo state.solver._stored_solver = None # Cerramos el solver al declararlo como no resuelto flag += '.' # Agregamos un . como valor no encontrado return flag def main(): pass_mysql = [160, 172, 173, 157, 173, 167, 163, 164, 155, 165, 164, 155, 162, 159, 181, 170] pass_pwn = [169, 152, 170, 153, 179, 159, 154, 156, 155, 154, 157, 167, 164, 159, 176, 153] print("MYSQL: {}, ELITE: {}\n".format(solver(pass_mysql), solver(pass_pwn))) if __name__== '__main__': main()
Después de ejecutar el programa,
encontraremos que los siguientes bytes son iguales a las siguientes llaves:
{160,172,173,157,173,167,163,164,155,165,164,155,162,159,181,170}
= "ins_meme_de_gato" para MYSQL
{169,152,170,153,179,159,154,156,155,154,157,167,164,159,176,153}
= "papaya_de_celaya" para ELITE
|
Comments
Post a Comment