Snyk Fetch the Flag CTF 2023: solución de Off the SETUID
Carlos Polop
Yago Gutiérrez
30 de noviembre de 2023
0 minutos de lectura¡Gracias por jugar Fetch con nosotros! Felicitaciones a los miles de jugadores que participaron en Fetch the Flag CTF. Si participaste en Fetch the Flag 2023 de Snyk y buscas la respuesta al desafío Off the SETUID, llegaste al lugar indicado. ¡Veamos la solución paso a paso!
Estás en un entorno desconocido. ¿Puedes arreglártelas con lo que tienes? O, ¿puedes simplemente vivir...
Obtén la bandera del directorio de inicio del usuario root.
Este es un caso extraño. ¿Es un desafío web o uno de explotación del kernel?
Tenemos un script de qemu, un kernel compilado (con su respectivo .diff) y una imagen initramfs (junto con el código del programa init). Para empezar, podemos descomprimir initramfs y echar un vistazo.
¿Con distribución o sin distribución?
Bien, sin distribución. No hay shell, ni utilidades comunes, ni nada. Evidentemente, el programa init está compilado (no hay shell, así que no puede ser un script), pero también tenemos su código fuente:
Hace lo típico que hace un script init: inicializa algunos dispositivos, monta procfs y sysfs (con noexec), vuelve a montar rootfs como solo lectura e inicializa la red. Luego inicia un servidor HTTP que escucha en el puerto 8080, con el directorio raíz en /var/run y como usuario con uid=100.
En /var/run, solo hay un index.php:
En resumen, tenemos un servidor HTTP claramente vulnerable a la inyección de código PHP, así que podemos usar algo como lo siguiente para obtener una shell interactiva inversa en PHP:
Pero no podemos leer la bandera porque solo root puede hacerlo. Por lo tanto, tenemos que encontrar una forma de escalar privilegios. Aquí entra fun_setuid.diff.
fun_setuid()
En el archivo .diff vemos que se agregó una nueva syscall al kernel:
En resumen, esta syscall permite que un proceso cambie su uid por otro, siempre y cuando sea mayor que el actual (o que el proceso tenga la capacidad CAP_SETUID).
La syscall usa una función auxiliar llamada prepare_user_creds(), que a su vez usa prepare_cred() para asignar un nuevo struct cred, luego lo modifica para que corresponda al nuevo usuario y lo devuelve. Si ocurre un error, devuelve el valor negativo de un código de error. Por eso, la función syscall verifica que el valor devuelto no sea negativo antes de confirmar las credenciales (consulta la Nota de diseño a continuación). También comprueba que el uid solicitado sea válido (es decir, que no sea igual a -1); si no lo es, devuelve NULL.
Nota de diseño
Este diseño también tiene dos problemas graves:
En C, comparar una dirección con cero no funciona. Simplemente no funciona. Las direcciones se tratan como tipos sin signo, así que verificar si una es negativa no tiene sentido. Por eso, el compilador elimina por completo el if.
Aunque el compilador no lo eliminara, el código seguiría sin funcionar porque en Linux para x86, las direcciones del kernel siempre son negativas. Así que, incluso cuando
prep_user_creds()no encuentra ningún problema, el punterostruct cred*que devuelve se interpreta como negativo y no se confirma.
Credencialismo
El problema es que la función syscall no verifica esta última posibilidad, así que podría confirmar un puntero NULL como puntero a nuestra estructura de credenciales. Después de modificar initramfs para agregar un busybox, podemos ejecutar sysctl vm.mmap_min_addr para ver la dirección mínima que un proceso puede asignar en este sistema. Si necesitamos depurar más, también tenemos /proc/config.gz.
Vemos que mmap_min_addr=0 y también que en el script run.sh la máquina virtual no tiene SMAP habilitado... ¡genial! Podemos asignar la dirección NULL, colocar allí una estructura de credenciales para root y llamar a fun_setuid(-1) para que nuestro puntero de credenciales apunte a NULL, es decir, a nuestras credenciales falsas.
En realidad no necesitamos cambiar las capacidades, pero ya que estamos aquí, podemos hacerlo para ganar puntos extra. Ten en cuenta que es muy importante establecer cred->usage=1 (que funciona como un contador de referencias) o el kernel creerá que se trata de un error UAF (BUG_ON(atomic_read(&new->usage) < 1)).
Tampoco podemos dejar que este proceso termine, o el kernel intentará liberar esta estructura, y eso no sería bueno. Por eso no podemos iniciar una shell (llamar a execve() implica, en última instancia, que el proceso actual termine); de ahí el pause() al final.
Pero todavía tenemos un problema: no podemos cargar el exploit en ningún lugar para ejecutarlo. Podemos convertirlo en shellcode y usar código PHP para escribir en el archivo mem de procfs del proceso PHP, y así lograr la ejecución arbitraria de código nativo. Desde allí, podemos ejecutar mmap() con NULL, colocar credenciales falsas y llamar a fun_setuid(-1), ya que no podemos hacer nada de esto con un script PHP…
Pero eso no es lo suficientemente genial.
Memexec
Hace unos meses, dimos una charla en DEFCON 31 sobre cómo evadir, en determinadas circunstancias, los entornos sin distribución. Para eso, yo (Yago Gutiérrez) desarrollé una herramienta llamada memexec que permite ejecutar cualquier programa que quieras, sin archivos, en PHP.
Solo tienes que pegar esta sesión interactiva de PHP. A partir de ahora, tendrás una función memexec() que acepta dos argumentos: el URI de un binario y un arreglo de argumentos para el programa. Por ejemplo, podemos configurar un servidor HTTP con busybox y ejecutar comandos en el sistema:
Importante: No hay bibliotecas SSL en la máquina virtual, así que asegúrate de no usar HTTPS.
Muy bien, explotemos este kernel de una vez por todas.
¡Gracias por hacer posible Fetch!
¡Muchísimas gracias a todos los equipos de Fetch the Flag 2023! Fue genial verlos a todos allí. Siempre pueden encontrarnos en @Arget (@arget13 en GH) y @carlospolop (@carlospop también en GH).
Aquí están las soluciones de los otros desafíos de 2023. ¡Échales un vistazo!
