SPACE 01 / 技术 · 2026.11.01

什么都看不见的盲注:用耗时写第三个 PoC

前两个 PoC 还能靠页面内容或差异判断,这个连长度都一样。唯一能拿来做判据的,是响应慢了几秒。

前两个 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())

三个要点:

  1. time.Now() 必须在发请求之前,写后面永远是 0
  2. time.Since(start) 在请求返回之后
  3. 每个请求独立取一次 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,排查时能把人带沟里。这是排查信息本身在撒谎的典型。

一个能复用的调试习惯

写这类脚本,我的顺序固定是:

  1. 先只发一个请求,把耗时打印出来 —— 跑通再加第二个
  2. 先打印中间值,再加判定 —— 能看见数据才谈得上判断
  3. 最后加结论分支 —— HIT / MISS

反过来的写法(一次写完再跑)出错时,你不知道错在请求、在计时、还是在判定。

三个 PoC 的判据对照

PoC 目标 判据 关键点
① cmdi-echo DVWA 命令注入 响应含 administrator 得区分“被踢回登录页”和“命令没执行”
② sqli-bool Less-8 布尔盲注 真分支有回显、假分支没有 不能用长度,模板会干扰
③ sqli-time Less-9 时间盲注 绝对 + 相对双条件 阈值要给抖动留余量

三个加起来不到 200 行,骨架几乎一样,只有判定段不同。

写在最后

这三个 PoC 教会我的不是“怎么发请求”——那是查文档的事。真正值钱的是判据设计:

  • 凭什么说这次命中了
  • 这个判据会不会被无关因素影响
  • 差异出现时,能不能解释它从哪来

面试官问“你怎么确认盲注存在”,能答出这三层的人,和只会说“我跑了脚本,它说 HIT”的人,不是一个水平。