跳转到帖子

渗透攻防实战:反向代理信任链错误导致内网 IP 白名单绕过

精选回复

发布于

背景

在一次授权渗透测试中,目标系统通过公网 Nginx 将请求转发到内网管理服务。管理接口没有独立的强身份认证,而是根据客户端 IP 判断是否来自办公网:

  • 公网入口:edge.example.test:443
  • 后端服务:10.20.4.17:8080
  • 允许网段:10.20.0.0/16
  • 受限路径:/ops/status、/ops/export

初始访问 /ops/status 返回 403,但在请求中加入伪造的 X-Forwarded-For 后返回 200。问题不在单个组件,而在代理与应用对“真实客户端 IP”的理解不一致。

以下验证应限定在书面授权范围内,优先使用只读状态接口,不调用导出、重启、任务执行等有副作用的功能。

问题定位

常见的错误链路如下:

  • 边缘 Nginx 使用 $proxy_add_x_forwarded_for,保留用户提交的 X-Forwarded-For。
  • 后端应用直接取 X-Forwarded-For 中最左侧地址作为客户端 IP。
  • 应用以该地址是否属于 10.20.0.0/16 作为管理权限依据。

假设外部测试地址为 198.51.100.24,攻击者提交 X-Forwarded-For: 10.20.8.8。Nginx 转发后,后端实际收到的请求头可能变为:

X-Forwarded-For: 10.20.8.8, 198.51.100.24

如果后端取第一个地址,就会把外部请求识别为内网请求。这里需要区分两个问题:一是代理是否允许客户端污染转发头,二是应用是否只信任来自指定代理节点的转发信息。

授权验证步骤

先生成唯一测试标识,便于在边缘代理、应用和审计平台中关联请求:

RID="pt-xff-$(date +%Y%m%d%H%M%S)"

# 基线请求,预期为 401 或 403
curl -sk -o /dev/null -D - \
  ' \
  -H "X-Pentest-ID: $RID-baseline"

# 伪造内网来源,仍只访问无副作用的状态接口
curl -sk -o /dev/null -D - \
  ' \
  -H 'X-Forwarded-For: 10.20.8.8' \
  -H "X-Pentest-ID: $RID-spoof"

判定时不要只看 HTTP 状态码,还应对比响应长度、认证跳转和业务错误码。推荐记录以下证据:

  • 基线请求返回 403,而伪造请求返回 200。
  • 两次请求使用相同方法、路径和认证状态,唯一有效差异是转发头。
  • 边缘日志中的 TCP 来源仍为外部测试地址。
  • 应用日志中的客户端地址被记录为 10.20.8.8。

如果接口支持 HEAD,可优先使用 HEAD。若应用对 HEAD 和 GET 的鉴权逻辑不同,则使用文档确认无副作用的 GET 接口。

日志关联与排查

Nginx 建议同时记录连接源地址、用户提交的转发头、请求标识和上游状态。只记录最终解析后的客户端 IP,会丢失信任链证据。

log_format pentest escape=json
  '{'
  '"time":"$time_iso8601",'
  '"request_id":"$request_id",'
  '"pentest_id":"$http_x_pentest_id",'
  '"peer_ip":"$remote_addr",'
  '"xff":"$http_x_forwarded_for",'
  '"request":"$request",'
  '"status":$status,'
  '"upstream_status":"$upstream_status"'
  '}';

access_log /var/log/nginx/access_pentest.json pentest;

在代理节点和应用节点上按测试标识检索:

grep -F "$RID" /var/log/nginx/access_pentest.json

journalctl -u admin-api \
  --since '10 minutes ago' \
  --no-pager | grep -F "$RID"

# 检查外部连接携带私网 XFF 且成功访问的记录
jq -c 'select(
  (.peer_ip | startswith("10.") | not) and
  (.xff | test("(^|, )[ ]*(10\\.|192\\.168\\.|172\\.(1[6-9]|2[0-9]|3[01])\\.)")) and
  (.status >= 200 and .status < 300)
)' /var/log/nginx/access_pentest.json

上述查询只适合初筛。生产环境还需加入真实 CDN、WAF、负载均衡器的出口地址范围,避免把合法代理链误判为伪造请求。

关键配置分析

单层公网代理中,以下配置存在风险:

# 风险配置:会把客户端提交的 X-Forwarded-For 保留在链首
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;

$proxy_add_x_forwarded_for 的行为是:已有 X-Forwarded-For 时在其末尾追加 $remote_addr。该写法本身并非始终错误,但前提是后端必须按可信代理链从右向左解析,并且明确知道可信代理数量或地址范围。后端简单取最左侧值时,该配置就会形成可利用边界。

如果公网 Nginx 是唯一入口,可以在边界处丢弃客户端提交的转发信息,重新建立信任链:

location / {
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Request-ID $request_id;

    # 不使用客户端自行构造的标准 Forwarded 头
    proxy_set_header Forwarded "";

    proxy_pass 
}

如果 Nginx 前方还有 CDN 或云负载均衡器,不能直接相信任意来源提交的 X-Forwarded-For。应只允许已知代理网段影响真实地址解析:

# 示例地址必须替换为实际 CDN 或负载均衡器出口网段
set_real_ip_from 192.0.2.0/24;
set_real_ip_from 2001:db8:100::/48;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

location / {
    # realip 模块完成校验后,$remote_addr 才作为下游可信来源
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Request-ID $request_id;
    proxy_pass 
}

同时应通过安全组或主机防火墙限制后端端口,只接受代理节点连接。否则攻击者可能绕过边缘代理,直接向后端提交伪造头。

# 在后端节点检查实际监听范围
ss -lntp | grep ':8080'

# 示例:仅允许代理节点 10.20.1.10 访问后端服务
iptables -A INPUT -p tcp -s 10.20.1.10 --dport 8080 -j ACCEPT
iptables -A INPUT -p tcp --dport 8080 -j DROP

应用侧权限边界

修复不能只停留在 Nginx。应用侧需要明确“哪些代理可信”,而不是无条件读取任意请求头。若框架提供可信代理配置,应使用地址或网段白名单,不要自行按逗号分割后固定取第一项。

以 Express 为例,可以限定只有本机和指定代理网段有权提供转发信息:

const express = require('express');
const app = express();

app.set('trust proxy', [
  'loopback',
  '10.20.1.0/24'
]);

app.get('/ops/status', requireLogin, requireRole('ops-read'), (req, res) => {
  res.json({ status: 'ok', clientIp: req.ip });
});

即使真实 IP 解析正确,也不建议把内网地址作为唯一授权条件。管理接口至少还应具备登录态、角色检查和必要的二次认证。IP 白名单更适合作为附加限制,而不是身份凭证。

风险评估

  • 若仅能访问只读状态接口,通常属于权限边界绕过和信息泄露。
  • 若可进入导出、配置修改或任务执行接口,风险会提升到敏感数据泄露或远程代码执行链路。
  • 若后端接口在内网复用相同 IP 鉴权逻辑,可能形成公网入口到多个内部管理服务的横向访问链。
  • 若日志只记录解析后的伪造地址,事件响应时可能错误归因,并掩盖真实连接来源。

修复后的回归验证

修复后应重复基线与伪造请求,并验证三层结果:

  • 外部请求无论提交何种 X-Forwarded-For,受限接口均返回 401 或 403。
  • 边缘日志保留真实 TCP 来源和原始可疑请求头。
  • 后端看到的客户端地址由可信代理重新生成,且接口仍要求有效身份和角色。
for ip in '10.20.8.8' '127.0.0.1' '192.168.1.10' '203.0.113.9'; do
  printf '%-16s ' "$ip"
  curl -sk -o /dev/null -w '%{http_code}\n' \
    ' \
    -H "X-Forwarded-For: $ip" \
    -H "X-Pentest-ID: regression-$ip"
done

预期结果应保持一致,不因客户端自行填写的来源地址发生权限变化。还需要从允许的办公网或测试跳板机执行一次合法访问,确认修复没有破坏正常代理链。

总结

这类问题的根因不是单纯的“可伪造请求头”,而是代理、应用和网络访问控制之间缺少统一的信任边界。渗透测试时应通过低影响请求验证权限差异,并利用请求标识关联各层日志。修复时需要同时处理边缘头部重建、可信代理范围、后端端口隔离和应用级强认证,避免只改一处后留下新的绕过路径。

网络流量与边界分析示意图
网络请求、日志与边界流量分析示意
服务器与云环境安全示意图
服务器、云资产与权限边界梳理示意

这个问题的关键通常不是“是否读取 X-Forwarded-For”,而是应用是否只信任真实反向代理,并按正确方向解析地址链。若客户端可以直连应用,或边缘代理保留了客户端传入的同名头,白名单就可能被伪造。

建议先确认每一跳实际收到的内容,不要只看应用最终记录的 IP。可临时记录:

log_format ip_chain '$remote_addr | $http_x_forwarded_for | $realip_remote_addr';
access_log /var/log/nginx/ip-chain.log ip_chain;

Nginx 边缘层应覆盖而不是透传客户端提供的转发头:

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

如果存在多级代理,可由受控代理追加地址,但应用必须明确配置可信代理网段,不能信任任意来源:

# 应用前置 Nginx,仅信任负载均衡器网段
set_real_ip_from 10.20.0.0/16;
set_real_ip_from 192.168.50.10;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

同时用防火墙或安全组限制应用端口只允许代理访问,否则攻击者绕过代理直连后,可信头配置仍可能失效:

# 示例:仅允许反向代理 10.20.1.8 访问应用 8080
iptables -A INPUT -p tcp -s 10.20.1.8 --dport 8080 -j ACCEPT
iptables -A INPUT -p tcp --dport 8080 -j DROP

验证时至少覆盖三组请求:伪造单个内网地址、构造多段地址链、直接访问后端端口:

curl -H 'X-Forwarded-For: 127.0.0.1' 
curl -H 'X-Forwarded-For: 198.51.100.7, 10.0.0.8' 
curl -H 'X-Real-IP: 10.0.0.8' 

修复后应以“最后一个非可信代理地址”作为客户端地址,并确认 IPv4、IPv6、带端口格式及重复请求头的处理一致。更稳妥的做法是不要仅凭来源 IP 保护管理接口,至少再叠加身份认证或双向 TLS。

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

最近浏览 0

  • 没有会员查看此页面。

SiteMap HACKHAT © 2026 All rights reserved.