
Este es un reto CTF de tipo pwn que escribí en C y que requiere que el usuario explote la vulnerabilidad CVE-2021-4034. Los jugadores reciben 2 binarios en el directorio challenge de este repositorio. El binario chal implementa el reto CTF y shelly.so es un binario auxiliar.
Al momento de escribir este writeup, el Dockerfile aún no está completo. El Dockerfile es necesario para desplegar este reto durante un CTF en vivo, pero no localmente: aún puedes emular este reto localmente configurando los permisos de usuario e instalando los paquetes vulnerables de la siguiente manera:
libpolkit-gobject-1-0=0.105-26ubuntu1 libpolkit-agent-1-0=0.105-26ubuntu1 policykit-1=0.105-26ubuntu1.flag.txt propiedad de root:root en el directorio actual.challenge al directorio actual.chal como el usuario sin privilegios.Ejecutar el binario chal nos da una idea vaga de lo que hace este binario:```
WELCOME TO THE HUB CTRL+ALT+DELICIOUS
We're not just a sandwich hub. We are the beacon of flavors, serving a symphony in every byte
Jugar con el tamaño de entrada en la función `Add` y con los índices de las funciones `Cancel` no nos da nada especial (sin desbordamiento ni fallo de segmentación). Sin embargo, algunas cosas interesantes:
- Parece que los pedidos se colocan en una lista (quizás una lista enlazada) y ¿están indexados desde 0?
- Hay este `Order number: 0x7ffde93681f0` que parece imprimir alguna ubicación en la pila?
- Además, el programa crea un directorio con nuestro nombre de entrada junto con 3 ejecutables, uno de los cuales es el archivo binario auxiliar que se nos proporciona:```sh
peasant@Tin-VM:~/Desktop$ ls
chal chal.c Dockerfile shelly.so solve.py tin
peasant@Tin-VM:~/Desktop$ ls -l tin/
total 28
-rwxrwx--- 1 peasant vboxsf 5 Feb 4 14:33 notes
-rwxrwx--- 1 peasant vboxsf 20 Feb 4 14:33 recipe
-rwxr-x--- 1 peasant vboxsf 16488 Feb 4 14:33 shelly.so
peasant@Tin-VM:~/Desktop$ cat tin/notes
0000
peasant@Tin-VM:~/Desktop$ cat tin/recipe
aaaa
bbbb
cccc
dddc
Los ejecutables contienen nuestra entrada.
Puedes jugar con las demás funciones y esperar a toparte con algunos bugs (lo cual es probable), pero voy a ir al grano y abrir el programa en Ghidra.
Comparando las cadenas que aparecen al ejecutar el programa con las cadenas presentes en Ghidra, podemos renombrar algunas de las funciones FUN_* con nombres familiares:```C
undefined8 main(void)
{ int iVar1; size_t sVar2; undefined2 *puVar3; long in_FS_OFFSET; int opt; int local_1c; char *local_18; long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28); local_18 = "/recipe"; print("WELCOME TO THE HUB CTRL+ALT+DELICIOUS\n"); print( "We're not just a sandwich hub. We are the beacon of flavors, serving a symphony in every by te\n\n" ); while( true ) { print("1. ENTER THE HUB\n"); print("2. QUIT\n"); __isoc99_scanf(&DAT_001030c5,&opt); getc(stdin); if (opt != 1) break; printf("Order number: %p\n",&local_18); print("Enter your name: "); __isoc99_scanf(&DAT_0010334f,&DAT_00105120); sVar2 = strlen(&DAT_00105120); puVar3 = (undefined2 *)malloc(sVar2 + 2); DAT_00105100 = puVar3; *puVar3 = 0x2f2e; *(undefined *)(puVar3 + 1) = 0; strcpy((char *)(DAT_00105100 + 1),&DAT_00105120); iVar1 = FUN_001022f0(DAT_00105100,&DAT_00105060); if (iVar1 == -1) { mkdir((char *)DAT_00105100,0x1c0); } DAT_00105140 = 0; order_cnt = 0; for (local_1c = 0; local_1c < 10; local_1c = local_1c + 1) { *(undefined8 *)(&ptr_array + (long)local_1c * 8) = 0; } main_menu(); } print("Come again :)\n");
The `printf("Order number: %p\n",&local_18);` **imprime la ubicación de una variable local en la pila.**
Una verificación rápida en gdb nos muestra que la dirección filtrada es la dirección del puntero a la cadena constante `/recipe`:```gdb
...
Order number: 0x7fffffffdfc0
...
gef➤ x/gx 0x7fffffffdfc0
0x7fffffffdfc0: 0x0000555555559020
gef➤ x/s 0x0000555555559020
0x555555559020: "/recipe"
Vemos una llamada a mkdir, que crea un directorio con nuestro nombre de entrada en el directorio actual.
Estos son consistentes con nuestra observación cuando ejecutamos el programa. Luego inicializa alguna variable antes de llamar a la función main_menu, que se ve algo así:```C
while( true ) {
while( true ) {
while( true ) {
while( true ) {
while( true ) {
while( true ) {
print("1. ADD NEW ORDER\n");
print("2. EDIT ORDER\n");
print("3. SHOW ORDER\n");
print("4. CANCEL ORDER\n");
print("5. CHECKOUT\n");
print("6. DONE\n");
__isoc99_scanf(&DAT_001030c5,&local_40);
getc(stdin);
if (local_40 != 1) break;
add_order();
}
if (local_40 != 2) break;
edit_order();
}
if (local_40 != 3) break;
show_order();
}
if (local_40 != 4) break;
cancel_order();
}
if (local_40 != 5) break;
checkout();
}
if (local_40 == 6) break;
if (local_40 == 0x539) {
print(
"\nGORDON RAMSAY: Finally, a worthy opponent, our battle will be legendary! I BET YOU CAN 'T GUESS THE SECRET RECIPE.\n"
);
fgets(inp,0x20,stdin);
getrandom(random-bytes,0x10,0);
for (local_3c = 0; local_3c < 0x10; local_3c = local_3c + 1) {
if (inp[local_3c] != random-bytes[local_3c]) {
print("...Nuh Uh!...\n");
/* WARNING: Subroutine does not return */
exit(0);
}
print("...Ooh Yes.. sCruMpTioUs...");
}
print("Fine... I'll give you a taste.\n");
FUN_00101504();
}
Si no estás familiarizado con REV, esta es la descompilación de la sentencia `switch` en C. Hay una opción interesante, también conocida como `0x539`. Permite a los jugadores adivinar `0x10` bytes aleatorios. Si todos los bytes son iguales, entonces llama a `FUN_00101504();`, que llama a `system("cat flag.txt");`. De lo contrario, el programa sale.
Sin embargo, intentar adivinar por fuerza bruta 16 bytes aleatorios equivale a probar todas las 256**16 = 340282366920938463463374607431768211456 posibilidades. Buena suerte con esto lol.
Incluso si pruebas todas las posibilidades, el programa sigue sin privilegios, así que no puede leer la flag. Esta sentencia `cat flag.txt` es intencional, no solo para engañar a los jugadores sin experiencia y hacer que elijan esta opción del menú `0x539`, sino también para evitar que los jugadores simplemente llamen a esta función para leer la flag, cuestión en la que profundizaremos más adelante.
***
## Agregar```C
int iVar1;
undefined8 *puVar2;
void *pvVar3;
undefined8 *ptr2;
if (order_cnt < 10) {
puVar2 = (undefined8 *)malloc(0x30);
print("Pick your bread: ");
readline(puVar2 + 1,8);
print("Select your spread: ");
readline(puVar2 + 2,8);
print("Choose your veg: ");
readline(puVar2 + 3,8);
print("Slam your meat & egg: ");
readline(puVar2 + 4,9);
iVar1 = order_cnt;
pvVar3 = malloc(0x30);
*(void **)(&ptr_array + (long)iVar1 * 8) = pvVar3;
*puVar2 = *(undefined8 *)(&ptr_array + (long)order_cnt * 8);
print("Any side notes for the cook? ");
readline(*puVar2,0x30);
puVar2[5] = 0;
if (DAT_00105140 != (undefined8 *)0x0) {
for (ptr2 = DAT_00105140; ptr2[5] != 0; ptr2 = (undefined8 *)ptr2[5]) {
}
ptr2[5] = puVar2;
puVar2 = DAT_00105140;
}
DAT_00105140 = puVar2;
order_cnt = order_cnt + 1;
}
...
Si algunas de las variables no te resultan fáciles de leer de esta forma, es porque me tomé un tiempo para renombrarlas así, algo que deberías hacer mientras haces ingeniería inversa para mantener el registro de las cosas. Bien, vamos a los puntos principales:
puVar2 es un chunk de malloc que tiene un tamaño de 0x30.puVar3, que tiene 8 bytes de largo y apunta a los notes del chunk actual.puVar2[5] = 0; sobrescribe de todos modos nuestro desbordamiento de 1 byte con NULL.DAT_00105140 == 0, simplemente la establecemos al nuevo chunk, así que podría ser un puntero head de la lista.... print("Enter order index: "); __isoc99_scanf(&DAT_001030c5,&local_20); getc(stdin); if ((local_20 < 0) || (order_cnt <= local_20)) { print("Invalid index!\n"); } else { local_18 = head; for (local_1c = 0; local_1c != local_20; local_1c = local_1c + 1) { local_18 = (undefined8 *)local_18[5]; } print("Pick your bread: "); readline(local_18 + 1,8); print("Select your spread: "); readline(local_18 + 2,8); print("Choose your veg: "); readline(local_18 + 3,8);WE print("Slam your meat & egg: "); readline(local_18 + 4,9); print("Any side notes for the cook? "); readline(*local_18,0x30); } ...
Hay una comprobación para nuestro índice de entrada, así que no podemos editar ubicaciones arbitrarias.
**Sin embargo, la lectura de 9 bytes en el 4.º campo, que se desborda 1 byte hacia el 5.º campo, sigue ahí!!! Podemos sobrescribir 1 byte en el puntero `nxt`**
***
## Show```C
...
if (local_18 != (undefined8 *)0x0) {
printf("%s, %s, %s, %s, %s\n",local_18 + 1,local_18 + 2,local_18 + 3,local_18 + 4,*local_18);
}
...
%s imprimirá hasta el carácter NULL, así que si nuestro 4.º campo tiene 8 bytes de longitud, entonces el 4.º %s imprimirá el 4.º campo de nuestro chunk + el valor del puntero nxt en el 5.º campo.
=> ¡Conseguimos una fuga de heap!!!*
Nada interesante aquí.
strcpy(local_d8,dir_name); sVar2 = strlen(dir_name); strcpy(local_d8 + sVar2,"/recipe"); creat(local_d8,0x1c0); iVar1 = open(local_d8,2); if (iVar1 == -1) { print("Error opening file f.\n"); /* WARNING: Subroutine does not return / exit(0); } chmod(local_d8,0x1f8); strcpy(local_98,dir_name); sVar2 = strlen(dir_name); strcpy(local_98 + sVar2,"/notes"); printf("%s %s\n","/notes",local_98); creat(local_98,0x1c0); __fd = open(local_98,2); if (__fd == -1) { print("Error opening file f_notes.\n"); / WARNING: Subroutine does not return */ exit(0); } chmod(local_98,0x1f8);
Así que crea los archivos `recipe` y `notes` en el directorio con nuestro nombre de entrada. Las instrucciones `chmod` establecen estos archivos en modo ejecutable: `0x1f8` y `0x1c0` son `0700` y `0770` en base octal, respectivamente.```C
...
for (local_ec = 0; (local_e0 != (char **)0x0 && (local_ec < order_cnt)); local_ec = local_ec + 1)
{
sVar2 = strlen((char *)(local_e0 + 1));
write(iVar1,local_e0 + 1,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen((char *)(local_e0 + 2));
write(iVar1,local_e0 + 2,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen((char *)(local_e0 + 3));
write(iVar1,local_e0 + 3,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen((char *)(local_e0 + 1));
write(iVar1,local_e0 + 4,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen(*local_e0);
write(__fd,*local_e0,sVar2);
write(__fd,&DAT_00103127,1);
local_e0 = (char **)local_e0[5];
}
iVar1 = close(iVar1);
if (-1 < iVar1) {
iVar1 = close(__fd);
if (-1 < iVar1) {
local_58 = 0x2f706d742f207063;
local_50 = 0x732e796c6c656873;
local_48 = 0x206f;
local_40 = 0;
local_38 = 0;
local_30 = 0;
local_28 = 0;
local_20 = 0;
strcpy((char *)((long)&local_48 + 2),dir_name);
system((char *)&local_58);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
}
...
Este enorme y desordenado bloque de código básicamente escribe el contenido de nuestros fragmentos en estos archivos, y luego ejecuta el comando cp /tmp/shelly.so dir_name, donde dir_name es el nombre que le damos al programa al principio.
void FUN_00101e84(void)
{ long in_FS_OFFSET; char *local_18; long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28); puts("HOLD UP, LET HIM COOK."); local_18 = (char *)0x0; execve("/usr/bin/pkexec",&local_18,(char **)&ptr_array); if (local_10 != *(long )(in_FS_OFFSET + 0x28)) { / WARNING: Subroutine does not return */ __stack_chk_fail(); } return; }
Esta es una función interesante. Llama a `pkexec` con un `NULL argv` y el puntero del array `notes` como variables de entorno, que es el comando vulnerable en nuestro CVE-2021-4034. ¿Esto sugiere que tenemos que manipular nuestros `notes` y redirigir la ejecución a esta función de alguna manera?
***
## shelly.so
Abrirlo en Ghidra nos dirá que es obviamente una biblioteca `set-UID-root`, similar a las utilizadas en la explotación del CVE.
***
# Resumen
Algunos puntos importantes:
1. Tenemos una fuga de pila al comienzo del programa (número de pedido) que contiene la dirección de la cadena constante `/recipe` utilizada para crear el archivo `recipe`.
2. El campo 0 de un chunk es su puntero `notes`.
3. El campo 5 de un chunk es el puntero `nxt` al siguiente chunk en la lista enlazada.
4. `Show` puede filtrar una dirección del heap que es el campo 5 de un chunk.
5. `Edit` puede sobrescribir 1 byte en el campo 5. **¡¡ESTE ES EL ÚNICO BUG DE ESCRITURA ARBITRARIA!!**
6. `FUN_00101e84` llama a `pkexec` con argv NULL y las variables de entorno que controlamos (array `notes`).
***
# Explotación
## Disposición de los chunks del heap
Una inspección rápida en gdb puede decirnos el desplazamiento entre los distintos chunks de nuestro programa:```gdb
...
Pick your bread: aaaa
Select your spread: a
Choose your veg: a
Slam your meat & egg: a
Any side notes for the cook? 0000
1. ADD NEW ORDER
2. EDIT ORDER
3. SHOW ORDER
4. CANCEL ORDER
5. CHECKOUT
6. DONE
1
Pick your bread: bbbb
Select your spread: b
Choose your veg: b
Slam your meat & egg: b
Any side notes for the cook? 1111
...
gef➤ search-pattern aaaa
[+] Searching 'aaaa' in memory
[+] In '[heap]'(0x55555555a000-0x55555557b000), permission=rw-
0x55555555aae8 - 0x55555555aaec → "aaaa"
gef➤ x/30gx 0x55555555aae8-0x18
0x55555555aad0: 0x0000000000000000 0x0000000000000041
0x55555555aae0: 0x000055555555ab20 0x0000000061616161
0x55555555aaf0: 0x0000000000000061 0x0000000000000061
0x55555555ab00: 0x0000000000000061 0x000055555555ab60
0x55555555ab10: 0x0000000000000000 0x0000000000000041
0x55555555ab20: 0x0000000030303030 0x0000000000000000
0x55555555ab30: 0x0000000000000000 0x0000000000000000
0x55555555ab40: 0x0000000000000000 0x0000000000000000
0x55555555ab50: 0x0000000000000000 0x0000000000000041
0x55555555ab60: 0x000055555555aba0 0x0000000062626262
0x55555555ab70: 0x0000000000000062 0x0000000000000062
0x55555555ab80: 0x0000000000000062 0x0000000000000000
0x55555555ab90: 0x0000000000000000 0x0000000000000041
0x55555555aba0: 0x0000000031313131 0x0000000000000000
0x55555555abb0: 0x0000000000000000 0x0000000000000000
Entonces los chunks son de tamaño 0x40 (incluyendo 0x10 bytes de metadatos), y están separados (desde el inicio del chunk actual hasta el inicio del siguiente chunk) por un offset de 0x40+0x40 = 0x80. Esto se debe a que los notes se asignan cada vez que asignamos un chunk, por lo que siempre quedan entre chunks consecutivos.
Antes de saltar al exploit, ¿cómo abusamos de los bugs de desbordamiento de 1 byte para escribir en una dirección arbitraria? Podemos usar la siguiente estrategia:
nxt del 2º chunk (2º punto en la sesión Summary).
C.B = C-0x80.x el byte MENOS significativo de B.x + 8 en el 5º campo del chunk actual. El puntero nxt resultante será B + 8.
A = 0x55555555aae0, entonces la dirección filtrada sería B = 0x55555555ab60 y x = 0xe8.A en 0x55555555ab08 con , el puntero resultante será , que no es lo que queríamos.Hay una forma de saltar a esta función, que no voy a comentar. Pero antes de intentarlo, echa un vistazo al Dockerfile. ¿Notas algo sobre flag.txt?```
...
chown root:root /home/ctf/flag.txt
...
USER peasant
CMD ["/home/ctf/start.sh"]
***ES PROPIEDAD DE ROOT, MIENTRAS QUE EL PROGRAMA SE EJECUTA Y ES PROPIEDAD DE UN USUARIO SIN PRIVILEGIOS***.
Así que aunque ejecutes "cat flag.txt", no va a imprimir la flag porque el programa no tiene permiso para acceder al archivo. Esto implica que necesitamos escalar a root para poder leer la flag.
## 2.ª forma - CVE-2021-4034
Para explotar CVE-2021-4034, necesitamos la siguiente configuración:
1. Un directorio con el nombre `GCONV_PATH=.`
2. En este directorio, necesitamos un archivo ejecutable que sea el nombre del directorio donde estará nuestro archivo de configuración `gconv-modules`. Que sea `recipe` en nuestro reto.
3. En el directorio `recipe`, necesitamos un archivo llamado `gconv-modules` con el contenido que controlemos, y una biblioteca dinámica que nos lance una shell (¿quizás es el binario proporcionado `shelly.so`)?
4. Después, en nuestro `gconv-modules`, necesitamos tener el mismo CHARSET que en el paso 5 de abajo, y el nombre de nuestra biblioteca dinámica `shelly.so` así: `module UTF-8// SHELLY// shelly 2`.
5. Después de todo esto, llamamos a `pkexec` con argv NULL y el array de variables de entorno manipulado = `{"recipe", "PATH=GCONV_PATH=.", "CHARSET=SHELLY", "SHELL=shelly", NULL}`.
Ahora, explotamos este reto para conseguir la configuración anterior.
***
### Objetivo 1-2
Los pasos 1-2 son fáciles: solo tenemos que iniciar sesión con un nombre de usuario `GCONV_PATH=.` para crear este directorio. Luego hacemos un `checkout` para crear el ejecutable `recipe` en este directorio. Después volvemos al primer menú.
***
### Objetivo 3-4
Iniciamos sesión con el nombre de usuario `recipe` para crear este directorio. Ahora, *necesitamos* crear un archivo llamado `gconv-modules`, pero `checkout` solo crea los archivos `recipe` y `notes`.
¿Y si sobrescribimos la ubicación de memoria para la cadena `/notes` y la convertimos en `/gconv-modules`? Esto podría funcionar, pero requiere que esa memoria sea escribible. Vamos a comprobarlo en gdb:```gdb
gef➤ search-pattern /notes
[+] Searching '/notes' in memory
[+] In '/home/peasant/Desktop/chal'(0x555555559000-0x55555555a000), permission=rw-
0x555555559010 - 0x555555559016 → "/notes"
¡Y es escribible!
¿Cómo realizamos la escritura?
/recipe./recipe hasta /notes es 0x10. Resta esto de la dirección de /recipe en el paso 1 para obtener la dirección de /notes./notes con /gconv-modules.En cuanto a escribir el contenido correcto en el archivo gconv-modules, sabemos que en checkout, el contenido de notes de los chunks se escribirá en /notes, que ahora es /gconv-modules. Para asegurarnos, podemos simplemente proporcionar la cadena module UTF-8// SHELLY// shelly 2 a cada notes.
Así que ahora, cada vez que llamemos a checkout, creará 2 archivos recipe y gconv-modules y escribirá algunas líneas module UTF-8// SHELLY// shelly 2 (usamos shelly porque ese es el nombre de la biblioteca .so set-uid-root proporcionada) en el archivo gconv-modules. También copiará un shelly.so al mismo directorio.
notes son las variables de entorno correspondientes. El array de punteros notes tendrá 4 punteros a estos 4 notes y un puntero NULL final, que es exactamente lo que queríamos.RIP de la función main con nuestra función secreta FUN_00101e84.
RIP? ¿Recuerdas nuestra dirección de pila filtrada al comienzo del programa? Entra en gdb para averiguar su desplazamiento con respecto al RIP, luego suma ese desplazamiento a la dirección filtrada real para obtener el RIP.main? ¿Recuerdas nuestra dirección filtrada de /recipe del paso 2 del Objetivo 3-4? ¡Haz lo mismo aquí!El script de resolución con comentarios se proporciona en este repositorio. No dudes en contactarme si tienes alguna pregunta :)
xnxt0x55555555**ab**e8 != 0x55555555**aa**e8B + 8 en lugar de C. Edita el chunk B para que su 1er campo (bread) contenga la dirección target a la que queremos escribir.B + 8. ¡El puntero notes de este 3er chunk es el target! Así que todo lo que escribamos en notes se escribirá en target.