跳转到帖子

渗透攻防实战:反向代理客户端 IP 信任错误导致管理接口越权

精选回复

发布于

背景

在一次授权渗透测试中,目标架构为“公网负载均衡—Nginx—业务应用”。管理接口与普通业务共用域名,应用通过客户端 IP 判断请求是否来自内网:内网地址允许访问管理功能,公网地址返回 403。

这类设计的风险点不在管理接口本身,而在客户端 IP 的获取链路。如果应用直接信任用户可控的 X-Forwarded-For、X-Real-IP 或 Forwarded,攻击者可能伪造内网地址绕过权限边界。

以下验证应限定在授权目标和无副作用接口,例如健康检查、版本信息或只读状态页。不要使用删除、发布、重置密码等管理操作证明漏洞。

问题定位

测试时发现,正常访问 /admin/health 返回 403,但加入特定代理头后返回 200。响应差异表明管理接口的访问控制依赖了请求头中的地址,而不是经过可信代理校验的连接源地址。

常见缺陷代码如下:

from ipaddress import ip_address

xff = request.headers.get('X-Forwarded-For')
client_ip = xff.split(',')[0].strip() if xff else request.remote_addr

if ip_address(client_ip).is_private or ip_address(client_ip).is_loopback:
    allow_admin_request()
else:
    deny_request()

这里存在三个问题:

  • X-Forwarded-For 可以由公网客户端直接提交。
  • 代码固定取逗号分隔后的第一个地址,没有结合可信代理数量反向解析。
  • 使用 IP 地址代替身份认证,一旦代理配置或解析逻辑变化,权限边界随之失效。

授权验证步骤

先建立基线,只记录状态码、响应长度和跳转位置,避免拉取敏感内容:

export TARGET='

curl -sk -o /dev/null -D - \
  "$TARGET/admin/health"

curl -sk -o /dev/null -D - \
  -H 'X-Forwarded-For: 127.0.0.1' \
  "$TARGET/admin/health"

curl -sk -o /dev/null -D - \
  -H 'X-Forwarded-For: 10.10.10.10' \
  "$TARGET/admin/health"

curl -sk -o /dev/null -D - \
  -H 'X-Real-IP: 127.0.0.1' \
  "$TARGET/admin/health"

curl -sk -o /dev/null -D - \
  -H 'Forwarded: for=127.0.0.1;proto=https' \
  "$TARGET/admin/health"

如果基线为 403,而伪造某个头后稳定变为 200,需要继续确认究竟是哪一层解析了该头。不要仅凭一次响应下结论,CDN 缓存、WAF 策略和灰度节点也可能造成差异。

可以使用不同的地址顺序判断应用如何解析代理链:

for value in \
  '127.0.0.1, 198.51.100.20' \
  '198.51.100.20, 127.0.0.1' \
  '10.0.0.8, 198.51.100.20' \
  '198.51.100.20, 10.0.0.8'
do
  echo "--- X-Forwarded-For: $value"
  curl -sk -o /dev/null \
    -w 'status=%{http_code} size=%{size_download} redirect=%{redirect_url}\n' \
    -H "X-Forwarded-For: $value" \
    "$TARGET/admin/health"
done

判断方式如下:

  • 只有第一个地址为内网时成功:应用大概率直接取左侧第一个地址。
  • 只有最后一个地址为内网时成功:应用可能取最右侧地址,但没有剥离可信代理。
  • 单独设置 X-Real-IP 生效:可能是 Nginx、网关或应用直接读取该字段。
  • 不同节点结果不一致:重点检查负载均衡后的配置漂移和不同版本中间件。

代理配置排查

Nginx 中较常见的问题是直接保留客户端提交的代理头:

location / {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Real-IP $http_x_real_ip;
    proxy_pass 
}

$proxy_add_x_forwarded_for 会在原始 X-Forwarded-For 后追加连接地址。如果应用取最左侧地址,攻击者提交的内容仍然位于最前面。第二行的问题更直接:它把用户提交的 X-Real-IP 原样传给后端。

排查时需要同时确认以下信息:

  • 公网入口是否覆盖客户端提交的代理头。
  • 后端是否可以绕过入口代理直接访问。
  • Nginx set_real_ip_from 是否设置成了过大的网段。
  • 应用框架配置的可信代理数量是否与实际拓扑一致。
  • IPv6、IPv4 映射地址和多个同名请求头是否采用一致的解析规则。

尤其要避免下面这种配置:

set_real_ip_from 0.0.0.0/0;
set_real_ip_from ::/0;
real_ip_header X-Forwarded-For;

它等于允许任意连接方声明自己的真实地址。即使当前服务位于负载均衡后,也应填写负载均衡或入口代理的精确地址段,而不是全网段。

日志取证与链路确认

只记录最终解析出的客户端 IP 不足以复盘此类问题。建议同时保留 TCP 对端地址、原始代理头、解析后地址、上游节点和请求标识:

log_format boundary '$time_iso8601 '
                    'peer=$remote_addr '
                    'original=$realip_remote_addr '
                    'xff="$http_x_forwarded_for" '
                    'xreal="$http_x_real_ip" '
                    'request="$request" '
                    'status=$status '
                    'upstream=$upstream_addr '
                    'request_id=$request_id';

access_log /var/log/nginx/access_boundary.log boundary;

针对管理路径进行初步检索时,可以先关注公网连接来源与内网声明地址同时出现的记录:

grep 'request="GET /admin/' /var/log/nginx/access_boundary.log \
  | grep -E 'xff="(127\.0\.0\.1|10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)'

该命令只能用于缩小范围,不能直接认定为攻击。企业代理、服务网格和健康检查也可能产生内网地址。最终需要结合 peer、入口负载均衡日志、请求标识及认证日志进行关联。

修复方案

修复应从边界入口、代理链和应用鉴权三层同时处理。

如果公网入口是唯一可信边界,可以在入口处覆盖外部提交的地址头:

location / {
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header Forwarded '';
    proxy_pass 
}

后续代理仅信任入口代理的精确地址,并将校验后的地址传给应用:

set_real_ip_from 10.20.1.10/32;
set_real_ip_from 10.20.1.11/32;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

location / {
    proxy_set_header X-Verified-Client-IP $remote_addr;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_pass 
}

这套配置成立的前提是应用端口只能被该代理访问。否则攻击者绕过代理直连应用时,仍然可以伪造 X-Verified-Client-IP。应通过安全组、防火墙、Kubernetes NetworkPolicy 或监听地址限制后端入口。

应用层不应仅依靠内网 IP 授权。管理接口至少需要具备以下控制:

  • 独立身份认证,例如短时令牌、OIDC 或双向 TLS。
  • 基于角色和具体操作的授权,避免“来自内网即管理员”。
  • 对高风险操作增加重新认证、审批或双人复核。
  • 记录主体、来源、操作对象、结果和请求标识,避免只记录客户端 IP。
  • 管理平面与公网业务入口分离,无法分离时至少采用独立路由和明确的鉴权中间件。

回归验证

修复后应从公网入口、可信代理和后端直连三个位置分别测试:

  • 公网请求伪造任意代理头,结果均应保持未授权。
  • 合法用户通过入口访问时,审计日志应记录正确来源地址。
  • 非代理节点访问应用端口应在网络层被拒绝。
  • 管理接口在没有有效身份凭据时,即使来源为内网也不应获得管理权限。
  • IPv4、IPv6、多值头和重复头的处理结果应保持一致。

建议把代理头伪造用例加入发布后的自动化安全回归,尤其是在更换 CDN、负载均衡、服务网格或应用框架后重新验证。此类漏洞往往不是单点代码错误,而是代理拓扑变化后,原有可信边界假设失效。

总结

客户端 IP 本质上是经过代理链计算出的属性,而不是可以直接信任的请求字段。渗透验证的重点应放在地址头覆盖规则、可信代理范围、应用解析顺序、后端可达性和实际鉴权机制上。修复时仅修改一行取 IP 代码通常不够,必须同时收紧网络入口、统一代理链解析规则,并让管理权限回归到可审计的身份认证与授权体系。

网络流量与边界分析示意图
网络请求、日志与边界流量分析示意
代码审计与漏洞定位示意图
代码审计、调用链与关键函数定位示意

这类问题的根因通常不是“拿到了客户端 IP”,而是应用把可伪造的请求头当成了授权依据。尤其要注意:只限制管理接口来源 IP,但没有同时限定可信代理链路时,X-Forwarded-For、X-Real-IP 都可能被客户端直接注入。

建议按下面几处排查:

  1. 先确认应用实际读取哪个字段,以及是否无条件启用了 Proxy Trust:

    curl -i  \
      -H 'X-Forwarded-For: 127.0.0.1' \
      -H 'X-Real-IP: 127.0.0.1' \
      -H 'Forwarded: for=127.0.0.1'

    同时在应用临时记录连接源地址、原始转发头和框架解析后的客户端地址。重点区分:TCP 对端地址是否确实是反向代理,以及框架最终选择了转发链中的哪一项。

  2. 在最外层代理覆盖客户端提交的头,不要继续透传同名值。Nginx 单层代理可以这样处理:

    location / {
        proxy_set_header X-Real-IP        $remote_addr;
        proxy_set_header X-Forwarded-For  $remote_addr;
        proxy_set_header Forwarded        "";
        proxy_pass 
    }

    如果确实存在多层代理,应该只信任明确的负载均衡器网段,再由 Real IP 模块重建地址:

    set_real_ip_from 10.20.0.0/16;
    set_real_ip_from 192.0.2.10;
    real_ip_header X-Forwarded-For;
    real_ip_recursive on;
    
    proxy_set_header X-Real-IP       $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    set_real_ip_from 0.0.0.0/0 或信任整个内网大段都比较危险,容器网络、SSRF 跳板和旁路入口经常会让这个边界失效。

  3. 应用侧不要用“取 XFF 第一项”这种固定规则。正确做法是从连接源地址开始,按反向代理链由右向左剥离已知可信代理;遇到第一个非可信地址即停止。以 Express 为例,不要直接设置:

    app.set('trust proxy', true);

    应限定实际代理地址或跳数,地址白名单通常比固定跳数更稳:

    const proxyaddr = require('proxy-addr');
    
    const trust = proxyaddr.compile([
      'loopback',
      '10.20.0.0/16',
      '192.0.2.10/32'
    ]);
    
    app.set('trust proxy', addr => trust(addr));
  4. 补测容易漏掉的地址表示法:::1、::ffff:127.0.0.1、IPv6 压缩形式,以及带空格、多值、多请求头的 XFF。地址应先用标准 IP 库解析和规范化,再做 CIDR 判断,避免字符串前缀匹配:

    # 不安全示例
    client_ip.startswith("127.")
    client_ip == "10.0.0.1"
    
    # 应使用标准 IP/CIDR 解析库判断网段归属

最后,管理接口不建议只依赖来源 IP 做授权。至少再加一层独立认证,并在网络层限制应用端口只能被反向代理访问;否则攻击者如果能绕过代理直连后端,即使转发头处理正确,也可能命中应用对 remote_addr 的错误信任。修复后应分别从公网入口、可信代理、后端直连路径执行回归测试,并确认伪造头不会影响最终授权结果。

创建帐户或登录后发表意见

最近浏览 0

  • 没有会员查看此页面。