前两个 PoC 都有东西可看:一个靠页面里的关键字,一个靠真分支有回显、假分支没有。
但真实的盲注往往什么都不给——页面一样长、一样白、状态码一样 200。唯一泄露信息的,是它多花了三秒。
先看目标
sqli-labs Less-9,注入点 ?id=1' and sleep(3) -- 。
不带注入时:
1耗时 0.020s
1状态码: 200 | 1响应长度: 707
带上 sleep(3):
2耗时 3.015s
2状态码: 200 | 2响应长度: 744
注意长度也有差(707 / 744),但这个差不该当判据——上一个 PoC 已经吃过亏:模板里一段空的错误框就能制造长度差。时间盲注要看的就是时间。
计时:Go 不给你,自己掐表
Go 的标准库不会返回“响应耗时”,得自己记两个时间点:
start := time.Now()
resp, err := http.Get(target)
if err != nil {
fmt.Println("请求失败:", err)
return
}
defer resp.Body.Close()
elapsed := time.Since(start)
fmt.Printf("耗时 %.3fs\n", elapsed.Seconds())
三个要点:
time.Now()必须在发请求之前,写后面永远是 0time.Since(start)在请求返回之后- 每个请求独立取一次
start,不能共用——共用的话第二次的耗时是累加的
sleep(3) 发生在服务端执行 SQL 时,服务器必须等它跑完才返回响应头。所以 http.Get 这一行阻塞了 3 秒,取到响应头就够,不用等读完 body。
判定:为什么是两个条件
只用绝对判断(“耗时超过 3 秒就是盲注”),会被“这个站本来就慢”骗。 只用相对判断(“比基准慢 2.5 秒”),会被网络抖动骗。
所以两个都要:
diff := elapsed2 - elapsed1
if elapsed2 > 3*time.Second && diff > 2500*time.Millisecond {
fmt.Println("[HIT] 时间盲注成立:sleep 被数据库执行了")
} else {
fmt.Println("[MISS] 耗时差不足")
}
- 绝对:注入请求本身超过 3 秒 —— sleep 真的执行了
- 相对:比基准慢 2.5 秒以上 —— 不是“服务器一直慢”
阈值为什么是 2.5 不是 3:
- 设 3.0 秒,网络抖动会让
sleep(3)测出来 2.98 秒,漏判 - 设 1.0 秒,正常页面偶尔慢 1 秒,误判
- 2.5 秒留了 0.5 秒余量,同时和“没睡”拉开了数量级的距离
真实工具里更稳的做法是测三次取中位数,靶场一次就够。
三个抄代码抄出来的坑
① 方法抄错了
从第一个 PoC(POST)复制过来,忘了 Less-9 是 GET。因为 query 里的 id=... 不管什么方法都会发出去,碰巧还能跑——但换个只认 GET 的接口就废了。
「能跑」和「对」是两件事。
② defer 关错对象
defer resp.Body.Close() // 第一个:对
...
defer resp.Body.Close() // 第二个:又关了 resp,resp2 没人关
第二个应该是 resp2.Body.Close()。现在这样 resp2 的连接不会释放,跑几十次可能耗尽连接池。
这种错误不会报错、不会崩、单次跑看不出来——只有跑多了才暴露。面试问“你的脚本能跑多久不挂”,这就是那个答案。
③ 状态码打印错对象
fmt.Println("2状态码:", resp.StatusCode, ...) // 打印的是第一个响应
两个都 200 的时候看不出来;如果第二个被 WAF 拦成 403,这行会骗你说 200,排查时能把人带沟里。这是排查信息本身在撒谎的典型。
一个能复用的调试习惯
写这类脚本,我的顺序固定是:
- 先只发一个请求,把耗时打印出来 —— 跑通再加第二个
- 先打印中间值,再加判定 —— 能看见数据才谈得上判断
- 最后加结论分支 —— HIT / MISS
反过来的写法(一次写完再跑)出错时,你不知道错在请求、在计时、还是在判定。
三个 PoC 的判据对照
| PoC | 目标 | 判据 | 关键点 |
|---|---|---|---|
| ① cmdi-echo | DVWA 命令注入 | 响应含 administrator |
得区分“被踢回登录页”和“命令没执行” |
| ② sqli-bool | Less-8 布尔盲注 | 真分支有回显、假分支没有 | 不能用长度,模板会干扰 |
| ③ sqli-time | Less-9 时间盲注 | 绝对 + 相对双条件 | 阈值要给抖动留余量 |
三个加起来不到 200 行,骨架几乎一样,只有判定段不同。
写在最后
这三个 PoC 教会我的不是“怎么发请求”——那是查文档的事。真正值钱的是判据设计:
- 凭什么说这次命中了
- 这个判据会不会被无关因素影响
- 差异出现时,能不能解释它从哪来
面试官问“你怎么确认盲注存在”,能答出这三层的人,和只会说“我跑了脚本,它说 HIT”的人,不是一个水平。