兰 亭 墨 苑
期货 · 量化 · AI · 终身学习
首页
归档
编辑文章
标题 *
URL 别名 *
内容 *
(支持 Markdown 格式)
攻击手法解析:DNS Rebinding 我用跑通的实验来解释。先看问题本身。 ## 问题:一次检查,两次解析 `mediaIngest` 的职责是「用户给一个 URL,服务端下载它」。但如果不管,用户可以让服务端去访问**内网地址**: ```json {"msgtype":"image","media_url":"http://127.0.0.1:9096/health"} ``` 这就是 SSRF(服务端请求伪造)——拿你的服务器当跳板去打内网。所以要有 `guardSSRF` 检查。 **问题在于:检查用的解析,和连接用的解析,是两次独立的 DNS 查询。** ``` ① guardSSRF(u.Hostname()) ← 解析一次,得到公网 IP,检查通过 ⋮ 时间窗口(毫秒级) ② http.Client.Do(req) ← Transport 内部又解析一次,这次拿到内网 IP ↓ 连到 127.0.0.1 —— 检查形同虚设 ``` 刚才的实验就是在演示这两步返回不同 IP: ``` ① 检查阶段 → 93.184.216.34(公网),guardIP 通过 ✅ ② 连接阶段 → 127.0.0.1(内网) 若此刻也检查 → 会被拦:拒绝内网/本机地址: 127.0.0.1 ``` ## 攻击手法:DNS Rebinding 攻击者控制一个域名(比如 `evil.com`)的 DNS,把它设成 **TTL=0**,然后让它这样回答: | 第几次查询 | 返回 | |---|---| | 第 1 次(`guardSSRF` 查的) | 公网 IP,比如 `1.2.3.4` → 检查通过 | | 第 2 次(Transport 查的) | `127.0.0.1` → 连到内网 | 因为 TTL=0,中间那几毫秒 DNS 缓存立即失效,第二次查询会拿到新答案。**检查通过了,但连的是内网。** 这种「检查时和使用时状态不一致」的漏洞模式就叫 **TOCTOU**(Time-Of-Check to Time-Of-Use,检查时刻 ≠ 使用时刻)。 ## 修复:把检查搬到「连接的那一瞬间」 关键机制是 `http.Transport` 提供了一个钩子: ```go Transport: &http.Transport{ DialContext: dialGuarded, // ← 每次建 TCP 连接前,都会调用它 } ``` `DialContext` 的职责本来是「给我主机名和端口,你负责建立 TCP 连接」。**Transport 不再自己解析 DNS,而是把这件事交给你的函数。**于是 `dialGuarded` 拿到控制权: ```go func dialGuarded(ctx, network, addr string) (net.Conn, error) { host, port, _ := net.SplitHostPort(addr) // addr = "evil.com:80" ips, _ := net.DefaultResolver.LookupIPAddr(ctx, host) // ① 唯一的解析 for _, ip := range ips { if err := guardIP(ip.IP); err != nil { // ② 检查紧挨着 continue } // ③ 直接连这个已经检查过的 IP,而不是再传主机名 conn, err := d.DialContext(ctx, network, net.JoinHostPort(ip.IP.String(), port)) if err == nil { return conn, nil } } } ``` 三个要点: **1. 只剩一次解析。** 检查用的 `ips` 和连接用的 IP 是同一份数据,中间没有第二次查询——**窗口消失了**。这不是"把窗口缩小",是**从结构上不存在了**。 **2. 连接时传 IP,不传主机名。** 注意第 ③ 步: ```go d.DialContext(ctx, network, net.JoinHostPort(ip.IP.String(), port)) // ^^^^^^^^^^^^^ 是 IP,不是 host ``` 如果这里传 `evil.com:80`,`Dialer` 会**再解析一次**,窗口又回来了。传 IP 就绕过了任何后续解析。 **3. 逐个 IP 检查后再连接。** 域名可能解析出多个 IP(CDN 常见)。任何一个 IP 是内网就跳过它;全部是内网则整个请求失败。 ## 一句话对比 | | 检查时机 | 结果 | |---|---|---| | 修复前 | 发请求**前**解析一次 | 检查的是 A,连的是 B → 可绕过 | | 修复后 | 建连接**时**解析,同一份数据既检查又连接 | 检查的和连的是同一个 IP → 无法绕过 | 用类比说:原来的做法像**看身份证放行,但进门时不再核对**——你可以在查验之后换个人进去。修复后是**查验和进门是同一个人、同一个瞬间**。 ## 为什么这个修复值得单独说 因为「检查了内网 IP」这个动作**看起来已经做了防护**,很容易认为安全了。但它的正确性依赖于一个隐含假设:**解析结果在检查和连接之间不会变**。而 DNS 是个外部可变系统,这个假设不成立。 这也是为什么我在 devlog 里把这类问题归为「声称做到 vs 真的做到」——`guardSSRF` 确实写了、确实在检查内网、确实能拦住直接写 IP 的攻击。**只有想到 DNS 可以中途换答案的人,才会发现它还差一步。** 顺带一提:这个漏洞是外部评审指出来的,我第一版实现就是那个"只查一次"的版本。
配图 (可多选)
选择新图片文件或拖拽到此处
标签
更新文章
删除文章