小德 发表于 2026-9-10 19:46:28

【BUG】宝塔 WAF 的 CDN 客户端地址获取设计缺陷

本帖最后由 小德 于 2026-9-10 19:57 编辑

无任何可信代理校验 → 直连源站任意伪造
只要站点 cdn=true,任何人只要能访问到源站 IP(源站暴露、IP 扫描、DNS 历史记录),即可完全控制 ngx.ctx.ip:

GET / HTTP/1.1
Host: victim.com
CF-Connecting-IP: 1.2.3.4
影响面(全部基于 ngx.ctx.ip):

IP 白名单绕过:waf.lua:140-143 命中后 return true,在 UA/URL/CC/POST/GET 全部检测之前直接放行。
CC 防护完全失效:cc.lua:83/115/168/233/307/309 以 ngx.ctx.ip 为计数键,每换一个头就是全新计数器。
地域/城市/IDC 封禁失效:drop_abroad、drop_china、reg_city、idc 全部读 ngx.ctx.iplong / ngx.ctx.country。
封禁第三方 IP(放大攻击):IpInfo.lan_ip() → ipinfo.bt_ip_filter(ip, 86400)(ipinfo.lua:160-195)会把伪造的 IP 写进 /dev/shm/.bt_ip_filter,即系统防火墙拉黑列表。攻击者只需故意触发一条规则,即可让任意第三方 IP 被服务器防火墙封禁最长 24 小时。
日志与统计污染:database.totla_log_insert、insert_ip_count、UV/UIP 快照、攻击溯源全部失真;Public.send_spider()(ipinfo.lua:597)还会把伪造 IP 当作蜘蛛候选上报。
remote-host 逻辑让伪造在"CDN 链路"下依然成立
ipinfo.lua:218-223:

                                                if request_header['remote-host'] and request_header['remote-host']~=nil then
                                                      if request_header['remote-host']==client_ip then
                                                                client_ip=tmpe
                                                                client_ip=ngx.re.gsub(client_ip," ","","jo")
                                                      end
                                                end
remote-host 不是任何 CDN 的标准头,完全由客户端控制并会被 CDN 透传。默认分支取 XFF 最后一个值(真实 IP),但攻击者只要同时把 remote-host 填成自己的真实 IP,就会被强制改判为 XFF 的第一个值:

X-Forwarded-For: 8.8.8.8          ← 攻击者伪造
remote-host: 203.0.113.9          ← 攻击者自己的真实 IP

Cloudflare 追加后 → X-Forwarded-For: 8.8.8.8, 203.0.113.9 → 取最后一个 = 203.0.113.9 → remote-host == client_ip 成立 → 改取 tmpe = 8.8.8.8

即:即使站点正确接入 Cloudflare、流量没有绕过 CDN,IP 依然可伪造。 这段代码等价于给了攻击者一个"取首/取尾"的开关。

cdn_baidu 模式取 XFF 首值 → 必然可伪造


ipinfo.lua:211-213。开启"百度 CDN"后直接取 tmpe,即客户端最先写入的值,任何场景下都可伪造。

is_localhost 覆盖不全,is_internal_ip 根本没被调用

public.luaL526-L544

function public.is_localhost(ip)
    if not ip then return false end
    if ip == "unknown" then return false end
    if public.find_str(ip, ":") then return false end   -- IPv6 直接放行
    local ip_nmber = public.ip2long(ip)
    if ip_nmber == 0 then return false end
    if ip_nmber >= 2130706433 and ip_nmber <= 2130706687 then   -- 仅 127.0.0.1 ~ 127.0.0.255
      return true
    end
    return false
end
回环段是 127.0.0.0/8,这里只覆盖 127.0.0.1–127.0.0.255 → 127.1.1.1、127.0.1.1 均可伪造成功。
含 : 直接 return false → ::1、0:0:0:0:0:0:0:1 等 IPv6 回环地址全部绕过检查。
私网段 10/8、172.16/12、192.168/16 完全未过滤:项目里已有 public.is_internal_ip()(public.lua:550-577),但在 get_client_ip_bylog 中从未调用(全仓库仅 database.lua:294 用了一次)。很多站点把内网 IP 或服务器 IP 加进白名单 → 直接全站 WAF 失效。

同名头只取第一条

ipinfo.lua:207-208:type(header_tmp)=="table" 时取 header_tmp。攻击者先发的那条头会被优先采用,CDN 后续追加/单独设置的头被忽略。正确做法应取最后一条,或按可信跳数从右往左回溯。

cdn=true 但 cdn_header 缺失 → 整个 WAF fail-open

ipinfo.lua:204 的 ipairs(Site_config['cdn_header']) 在 cdn_header 为 nil 时抛错。而 waf.lua:224-233 用 pcall 包住了整个 btwaf_run:

waf.luaL224-L233

local ok,error = pcall(function()
      return btwaf_run()
end)

if not ok then
   if not ngx.shared.spider:get("btwaf_access") then
      Public.logs(error)
      ngx.shared.spider:set("btwaf_access",1,180)
    end
end

IpInfo.get_client_ip_bylog() 在第 63 行,异常后 CC、URL、GET、POST、Cookie、UA、扫描器……全部跳过,请求正常放行,且错误信息 3 分钟才记一次日志。这比伪造 IP 更严重。

Public.get_client_ip() 未定义 → 自定义规则整条链路 fail-open

modules/customize.lua:938:

customize.luaL938-L940

                            local ip = Public.get_client_ip()
                            local cache_key ="customize_cc__"..v..ip
                            local safe_count = ngx.shared.drop_sum:incr(cache_key, 1)

全仓库只有这 5 处 get_client_ip 匹配,没有任何定义(public.lua 里不存在 public.get_client_ip,也没有 __index 元表兜底)。命中"自定义 CC 防御"规则时必然 attempt to call a nil value,同样被 waf.lua 的 pcall 吞掉 → 后续所有检测(args/post/URL 规则)被跳过。顺带一提,此处本应直接用 ngx.ctx.ip。



页: [1]
查看完整版本: 【BUG】宝塔 WAF 的 CDN 客户端地址获取设计缺陷