Secretos de CTF revelados: explicación del desafío TopLang de SnykCon 2021
Michael Aquilina
6 de enero de 2022
0 minutos de lecturaSi asististe a SnykCon 2021, quizá recuerdes nuestro primer CTF: Fetch the Flag. En este CTF, TopLang fue un desafío web de dificultad media que recibió muchos comentarios positivos. Así que, si te gustó, en este artículo explicamos cómo nuestro equipo abordó internamente el desafío y lo resolvió. Y, mejor aún, puedes probarlo por tu cuenta en https://ctf-2021.snyk.io/, en la sección Challenges.
Este desafío era un ejemplo bastante típico de lo que se conoce como un «ataque de oráculo» mediante inyección SQL ciega. Voy a explicar cómo abordamos este problema, suponiendo que quien lee no está familiarizado con los desafíos CTF. Los pasos se describen con bastante detalle, pero doy por hecho que tienes algunos conocimientos de Python y SQL.
La descripción del desafío era:
¿Cuál es tu lenguaje de programación favorito?
A veces, los desafíos dan pistas sobre cuál podría ser la solución. Pero parece que aquí no hay información que podamos aprovechar. ¡Así que vayamos directo a nuestras primeras investigaciones!
Investigación inicial
Al comenzar cualquier desafío web de CTF, conviene familiarizarse con las páginas web disponibles y su contenido antes de empezar a escribir código. Esta investigación inicial debería darte una idea de los posibles vectores de ataque y ayudarte a entender dónde es probable que esté almacenada la bandera del CTF. No debería llevarte más de 10 minutos: solo tienes que hacer clic en los distintos enlaces disponibles y tomar nota de todo lo que parezca útil.
Al abrir el enlace web de la descripción del desafío, aparece la siguiente página:

Podemos ver una tabla con los principales lenguajes de programación de 2020 y 2021, junto con algunos metadatos adicionales, como las calificaciones.
Las columnas parecen ordenarse al hacer clic en el encabezado. Y, lo que es más importante, al ordenar por columnas específicas, se agrega el parámetro de cadena de consulta sort a la URL. Por ejemplo, al ordenar por la columna «Jun 2021», obtenemos la ruta URL /?sort=jun2021
Cualquier tipo de entrada que podamos manipular es un posible vector de ataque que podemos aprovechar, así que parece una pista interesante que podemos investigar más adelante.

Otro lugar muy evidente para explorar es el enlace Admin panel en la parte inferior de la página. Al hacer clic, se nos redirige a /admin.php. No parece que haya mucho que hacer aquí si no iniciamos sesión.

Si vamos a la página de inicio de sesión, encontramos un formulario estándar:

Podemos probar algunas combinaciones comunes, como «admin» / «admin», pero ninguna parece funcionar. Como se trata de un desafío web, podemos tener bastante confianza en que no necesitamos usar una herramienta para descifrar contraseñas, como THC Hydra, para forzar este inicio de sesión.
Parece probable que tengamos que extraer de alguna manera del servidor los valores de login y password y usarlos para iniciar sesión en este panel de administración.
Bien, ahora que conocemos las distintas páginas, profundicemos un poco más para ver si hay algo que parezca explotable.
Inyección SQL
Los datos sobre los principales lenguajes de programación se muestran en una tabla en la página web, que permite ordenar los datos de distintas maneras mediante el parámetro de cadena de consulta sort. Con esta información, es bastante probable que los datos se recuperen y consulten mediante una base de datos SQL en el backend.
Si efectivamente hay un backend SQL, podemos comprobar si la página es vulnerable a ataques de inyección SQL.
Nota: Podríamos probar una herramienta de inyección SQL como sqlmap para facilitar un poco este desafío CTF. De hecho, en muchos casos funcionaría perfectamente. Sin embargo, durante la competencia no tuve mucha suerte al intentar hacerla funcionar en este desafío. En lugar de dedicar mucho tiempo a averiguar qué opciones tenía que habilitar para que la herramienta funcionara bien, decidí escribir mi propio código de explotación. ¡Además, escribir tu propio código para resolver el desafío es mucho más divertido!
Ya vimos que el parámetro de cadena de consulta sort era un buen candidato a vector de ataque. El valor que se pasa al parámetro sort parece ser el nombre de una columna de una tabla de base de datos. Estos son los valores posibles que se obtienen al presionar los distintos encabezados de columna:
sort=jun2020sort=jun2021sort=ratingssort=change
Si este nombre de columna se pasa a una instrucción SQL ORDER BY mediante un formato de cadena inseguro, podríamos extraer datos con un ataque de inyección SQL ciega.
Una buena forma de comprobar si esto es vulnerable es ver si podemos cambiar el orden de los resultados usando una instrucción CASE con una condición booleana. En particular, ¿obtendremos distintos órdenes cuando la instrucción CASE se evalúe como True o False? Probemos los siguientes dos valores de sort y veamos qué sucede:
?sort=”(CASE WHEN 1=1 THEN jun2021 ELSE jun2020)”?sort=”(CASE WHEN 1=0 THEN jun2021 ELSE jun2020)”
En esta etapa del proceso, conviene empezar a escribir scripts para interactuar con la página web objetivo. Es probable que pronto necesitemos automatizar algunas tareas y, además, los navegadores como Firefox y Chrome suelen alterar y transformar las entradas complejas que contienen espacios en blanco y otros caracteres especiales.
Personalmente, conozco bastante bien Python y, en general, es un excelente lenguaje para los CTF porque permite poner en marcha una solución funcional con facilidad y con ayuda de algunos paquetes externos.
Si instalamos las bibliotecas requests y BeautifulSoup desde PyPi, podemos solicitar la página y analizar el HTML para detectar diferencias en los resultados. En particular, podemos detectar cambios comprobando si varía el orden de la columna de lenguajes en la página.
Podemos ver que «C» siempre es el lenguaje principal tanto en 2020 como en 2021. Sin embargo, «Go» fue el lenguaje menos favorito en 2021 y «Fortran» fue el menos favorito en 2020. Con esto en mente, podemos escribir código que compruebe cuál fue el lenguaje menos favorito para detectar diferencias.
Creemos una función que devuelva el último lenguaje de la página:
Si asociamos una condición True con Jun2021 y una condición False con Jun2020, deberíamos obtener «Go» si pasamos una instrucción True a get_data y «Fortran» si, en cambio, pasamos una instrucción False a get_data.
Una forma sencilla de pasar una instrucción True a SQL es «1=1», y «1=0» podría ser nuestra instrucción False.
¡Probémoslo con algo de código!
Esto nos da el siguiente resultado:
¡Éxito! ¡Logramos cambiar el orden mediante una condición booleana! Esta prueba es toda la evidencia que necesitamos para confirmar que el parámetro order es vulnerable a un ataque de inyección SQL ciega. Ahora solo tenemos que aprovechar esta vulnerabilidad.
Ataque de oráculo
Nuestro ataque de inyección SQL ciega es una forma de «ataque de oráculo». Un ataque de oráculo consiste en hacerle preguntas de «sí o no» al servidor, lo que nos indica qué tan cerca estamos del valor que buscamos.
En nuestro caso, parece probable que el objetivo sean los valores de login y password del formulario del panel de administración que vimos antes.
Con un ataque de oráculo, no podemos pedirle directamente al servidor que nos dé el login y la contraseña. Pero podemos seguir enviándole conjeturas sobre cuáles son esos valores y afinarlas poco a poco hasta obtener las respuestas correctas.
El truco habitual para aprovechar este ataque de oráculo consiste en hacer preguntas sobre subcadenas que se vuelven cada vez más específicas a medida que obtenemos respuestas afirmativas.
Este es un ejemplo de cómo podría ser una serie de preguntas al oráculo, sin usar código:
«¿El login empieza con a?» Servidor: No
«¿El login empieza con b?» Servidor: No
«¿El login empieza con c?» Servidor: Sí
«¿El login empieza con ca?» Servidor: No
«¿El login empieza con cb?» Servidor: No
«¿El login empieza con cc?» Servidor: No
«¿El login empieza con ce?» Servidor: Sí
Repetimos esta operación hasta extraer todo el contenido del login, carácter por carácter. Si suponemos que la contraseña también está en texto sin formato, podemos hacer lo mismo con ella.
Ahora traduzcamos esto a código. Podemos empezar escribiendo una función oracle que devuelva True si la respuesta a nuestra pregunta es «sí» y False si es «no». Si modificamos nuestra función get_data original, podemos comprobar cuál es el último lenguaje de la tabla HTML para determinar el resultado de nuestras preguntas de sí o no:
Cómo extraer la información de inicio de sesión
Sabemos que tenemos que recuperar de la base de datos el login y la password. Sin embargo, no conocemos el esquema SQL para escribir las consultas. Podríamos intentar adivinar los nombres de las tablas y las columnas (sabemos de algunos equipos que lo hicieron), pero nuestro equipo primero extrajo los metadatos de la base de datos para saber qué tablas y columnas consultar.
Al intentar varias consultas específicas para cada uno de los tipos de backend SQL más populares (SQLite, MySQL, PostgreSQL y SQL Server) y ver cuáles no hacían que la página web fallara, logramos descubrir que se trataba de un backend de base de datos SQLite.
Consultar las tablas y columnas disponibles en una base de datos SQLite es una operación sencilla. Empecemos por averiguar qué tablas tenemos disponibles para extraer datos.
La siguiente consulta nos daría una lista de todos los nombres de las tablas de una base de datos SQLite:
Sin embargo, no podemos pasar esto directamente a nuestra función de oráculo porque no es una pregunta de «sí o no». Para resolverlo, podemos concatenar todos los nombres de las tablas mediante la función GROUP_CONCAT y enviar conjeturas sobre los valores del resultado de esa concatenación usando la función substr.
Al combinar todo esto, obtenemos:
Observa que end y guess son parámetros de nuestra consulta. El parámetro guess sería simplemente nuestra conjetura actual y end es la longitud en caracteres de nuestra conjetura más uno.
Ahora solo tenemos que actualizar el código para enviarle conjeturas al servidor, carácter por carácter. Nuestro código debe seguir enviando estas solicitudes hasta que el oráculo responda True. Cuando recibimos una respuesta True, pasamos al siguiente carácter y repetimos el mismo proceso para seguir afinando nuestra conjetura.
Así se ve el código:
Al ejecutar este código, verás que se nos revelará el contenido de todos los nombres de las tablas, carácter por carácter:

Si esperamos a que termine el script, el resultado final será «users:languages». Esto significa que tenemos una tabla users y una tabla languages que podemos investigar más a fondo. Como nuestro objetivo es iniciar sesión en el panel de administración, es razonable suponer que la tabla «users» es la que queremos explorar.
El siguiente paso es averiguar qué columnas están disponibles en la tabla users. SQLite tiene una función PRAGMA_TABLE_INFO(table_name) que permite consultar los nombres de las columnas de una tabla específica. En nuestro caso, la consulta sería algo como:
De nuevo, como solo queremos enviarle al servidor preguntas del tipo oráculo, podemos convertir esto mediante las funciones GROUP_CONCAT y substr para obtener este payload SQL:
Solo tenemos que modificar el script anterior para actualizar la variable command con esta nueva consulta.
Al volver a ejecutarlo, finalmente obtendríamos la respuesta:
Parece que encontramos las columnas objetivo: login y password. Lo único que queda es enviar payloads SQL para extraer toda la información de esas columnas:
Este es el payload SQL en formato de oráculo que extraería los datos de login:
Y aquí está la carga útil de SQL en formato Oracle para extraer los passworddatos:
Puedes ejecutar ambos casos reemplazando el valor de command en el mismo script, tal como hicimos antes. Al ejecutar estos ataques, se filtrarán correctamente dos conjuntos de combinaciones de usuario y contraseña.
No quiero arruinar la diversión, así que dejaré que averigües las respuestas por tu cuenta.
Inicio de sesión del panel de administración
Con la información de inicio de sesión que acabamos de extraer, podemos acceder correctamente al panel de administración en /admin.php.
Sin embargo, parece que aún tenemos que superar un último desafío, ya que nos encontramos con esta página web:

Por suerte, resolver este último paso es bastante fácil. Siempre es buena idea revisar qué cookies se guardan en el navegador para ver si podemos modificarlas a nuestro favor. En la pestaña Storage de Firefox, podemos ver lo siguiente:

En particular, el valor analizado nos muestra que la cookie almacena un valor isAdmin y que actualmente tiene el valor 0. Veamos qué pasa si lo cambiamos por 1.
Si copiamos el valor completo de la cookie, tenemos:
Sin necesidad de entender en qué consistía esta codificación, nuestro equipo decidió que lo más fácil era cambiar el “0” hacia el final de la cadena por un “1” y reemplazar el valor de la cookie en el navegador:
Al investigar esto una vez terminado el CTF, quedó claro que se trataba simplemente de un objeto serializado de PHP. Sin embargo, creo que vale la pena mostrar que tomar atajos como el que usamos es perfectamente aceptable en un desafío CTF, donde ahorrar tiempo puede marcar la diferencia en tu posición final en la tabla de clasificación.
Si pegas esto en la pestaña Value (haz doble clic y luego pega la cookie que acabas de editar) y actualizas la página, aparecerá la codiciada bandera del CTF SNYK{...}. Una vez más, eliminé el resultado real de la pantalla para que puedas probarlo por tu cuenta:

Para terminar… ¡por ahora!
En resumen, estos son los pasos que seguimos:
Investigamos las páginas disponibles y descubrimos una cadena de consulta order, además de una página de inicio de sesión de administrador
Comprobamos correctamente que la cadena de consulta order era vulnerable a ataques de inyección SQL ciega
Extrajimos los nombres de las tablas y las columnas, así como los datos de inicio de sesión de la página de administración.
Modificamos los datos de las cookies del navegador para engañar al servidor y hacerle creer que habíamos iniciado sesión como administradores; por último, recuperamos la bandera del Snyk CTF
¡Espero que hayas disfrutado este informe del CTF y que incluso hayas aprendido algo nuevo! Los desafíos CTF son una excelente forma de aprender sobre vulnerabilidades reales y, por lo tanto, de aumentar tus posibilidades de defenderte contra ellas en tus propios sistemas. Publicaremos más informes en el futuro, así que mantente al tanto para ver más guías detalladas.
Por último, John Hammond (investigador sénior de seguridad en Huntress) creó explicaciones detalladas de algunos de los otros CTF de Fetch the Flag de SnykCon 2021. Te recomiendo que las veas:
Si te interesa unirte al equipo de investigación de seguridad de Snyk, que crea estos CTF (y muchas otras cosas), ¡consulta nuestras vacantes!
