跳转到帖子

授权渗透中同一接口出现 403/200,如何定位反向代理与 X-Forwarded-For 信任边界?

精选回复

发布于

问题背景

在已授权的测试环境中,通过公网域名访问管理接口返回 403,但使用内网地址直连后端时返回 200。修改 X-Forwarded-For 后状态码也可能变化。如何判断拦截发生在 CDN、WAF、Nginx 还是应用层,并验证是否存在来源地址伪造或后端暴露问题?

这类现象不能仅凭状态码判定。常见原因包括边缘访问控制、反向代理 location 规则、应用自身 IP 白名单、Host 路由差异,以及后端错误信任客户端传入的转发头。排查重点是固定请求变量、为请求增加唯一标识,并对照各层日志。

一、先固定验证条件

测试前确认目标地址、后端地址和测试账号都在授权范围内。建议选择无副作用的 GET 或 HEAD 请求,不要用修改配置、创建用户等接口验证访问控制。

  • 固定请求方法、路径、查询参数和请求体。
  • 固定 Host、SNI、认证信息和 HTTP 版本。
  • 每次请求使用不同的 X-Request-ID,便于跨层关联日志。
  • 分别记录响应状态、响应头、正文哈希、耗时和实际连接地址。
RID=pt-$(date +%s)-$RANDOM
TARGET='

curl -sk --http1.1 \
  -H "X-Request-ID: $RID" \
  -D "/tmp/$RID.headers" \
  -o "/tmp/$RID.body" \
  -w 'rid=%{header:x-request-id} code=%{http_code} remote=%{remote_ip} time=%{time_total}\n' \
  "$TARGET"

sha256sum "/tmp/$RID.body"
sed -n '1,30p' "/tmp/$RID.headers"

如果目标使用虚拟主机,直连后端时不要直接把 URL 改成 IP。这样可能同时改变 Host 和 TLS SNI,得到的是另一个站点。可以使用 curl 的 --resolve 保持域名不变:

RID=pt-direct-$(date +%s)-$RANDOM

curl -sk --http1.1 \
  --resolve portal.example.test:443:10.20.30.40 \
  -H "X-Request-ID: $RID" \
  -D "/tmp/$RID.headers" \
  -o "/tmp/$RID.body" \
  -w 'code=%{http_code} remote=%{remote_ip} time=%{time_total}\n' \
  ' 403 由哪一层产生

响应头中的 Server、Via、X-Cache 等字段只能作为线索,不能作为结论,因为这些字段可以被隐藏或重写。更可靠的方法是对比响应特征并查看对应日志。

观察项可能结论
边缘请求没有进入源站日志CDN、WAF 或上游负载均衡器已拦截
Nginx access log 为 403,upstream_addr 为空请求未转发,通常是 Nginx allow/deny、auth_request 或 location 规则
Nginx 状态为 403,upstream_status 也为 403后端应用或内部鉴权服务拒绝
外部访问 403,保持 Host 和 SNI 直连后端为 200边缘控制生效,但还需检查后端是否可被非预期网络直接访问
仅修改 X-Forwarded-For 就从 403 变为 200高度怀疑代理信任链或应用 IP 白名单实现错误

检查 Nginx 当前生效配置时应使用 nginx -T,而不是只查看某个配置文件。include、容器挂载和配置生成器经常导致磁盘文件与实际配置不一致。

sudo nginx -T 2>&1 | grep -nE \
  'server_name|location|allow |deny |auth_request|real_ip_header|set_real_ip_from|proxy_set_header'

sudo journalctl -u nginx --since '15 minutes ago' --no-pager

# 按唯一请求标识查找,路径按实际日志位置调整
sudo grep -R --fixed-strings "$RID" /var/log/nginx/

三、验证 X-Forwarded-For 信任边界

在授权范围内,可以使用同一路径做最小差异测试。只改变一个请求头,并使用无副作用接口。如果状态码或权限结果发生变化,应立即转向日志确认,不需要继续访问更多敏感资源。

URL='

for XFF in '198.51.100.25' '127.0.0.1' '10.10.10.10'; do
  RID=pt-xff-$(date +%s)-$RANDOM
  curl -sk -o /dev/null \
    -H "X-Request-ID: $RID" \
    -H "X-Forwarded-For: $XFF" \
    -w "xff=$XFF code=%{http_code} remote=%{remote_ip}\n" \
    "$URL"
done

这里需要区分三个地址概念:

  • 传输层对端地址:Nginx 初始看到的 remote_addr。
  • 客户端提交的原始 X-Forwarded-For:默认情况下完全不可信。
  • 经过可信代理链解析后的规范化客户端地址:只有配置正确时才可用于访问控制。

常见错误是应用直接读取 X-Forwarded-For 的第一个值,并把私网地址或 127.0.0.1 视为可信来源。攻击者能够自行构造该头时,IP 白名单就失去了边界作用。

另一个高频错误是将所有地址配置为可信代理:

# 错误示例:任意来源都可以影响 real_ip 结果
set_real_ip_from 0.0.0.0/0;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

正确做法是只信任实际存在且地址固定的负载均衡器或边缘代理网段。例如:

# 仅示例,必须替换为本环境真实代理地址
set_real_ip_from 10.20.1.10/32;
set_real_ip_from 10.20.1.11/32;
set_real_ip_from 2001:db8:20::/64;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

可信入口还应覆盖或规范化外部传入的转发头。如果只有一层入口代理,可以直接使用连接来源生成新的值:

location / {
    proxy_set_header Host $host;
    proxy_set_header X-Request-ID $http_x_request_id;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_pass 
}

多级代理环境不能机械地覆盖所有链路信息。应明确每一跳的可信地址,由最外层入口删除客户端伪造的同名头,内部代理再按受控规则追加。解析时从最右侧开始剥离已知可信代理,遇到的第一个非可信地址才是候选客户端地址。

四、补充可关联的日志字段

默认 access log 往往只记录最终 remote_addr,无法还原头部是在哪一层被修改的。排查期间可以增加独立日志格式,同时记录传输层对端、解析后的地址、原始转发链和上游状态。

log_format access_boundary
  '$time_iso8601 '
  'rid=$http_x_request_id '
  'peer=$realip_remote_addr '
  'client=$remote_addr '
  'xff=$http_x_forwarded_for '
  'host=$host request=$request '
  'status=$status upstream=$upstream_addr '
  'ustatus=$upstream_status rt=$request_time urt=$upstream_response_time';

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

修改后先检查语法,再平滑加载:

sudo nginx -t && sudo nginx -s reload
sudo tail -f /var/log/nginx/access_boundary.log

应用层也建议记录鉴权决策,但不要把会话 Cookie、Authorization、完整请求体写入日志。比较有价值的字段包括请求标识、规范化客户端地址、账号标识、命中的访问控制规则、拒绝原因和资源名称。

五、如何判定具体问题

  • 修改 X-Forwarded-For 后,公网入口的权限结果改变:代理头信任边界存在缺陷,应检查入口是否清洗头部,以及应用是否直接使用原始头。
  • 公网入口始终 403,但直连后端为 200:检查后端监听地址、安全组、防火墙和服务发现网络,确认后端是否只能由代理访问。
  • 同一请求在 Nginx 日志中 upstream_addr 为空:重点检查 allow/deny、satisfy、auth_request、geo、map 和 location 匹配顺序。
  • upstream_status 为 403:继续使用 X-Request-ID 查询应用或鉴权服务日志,避免把应用拒绝误判为 WAF 拦截。
  • 只有使用 IP 直连时返回 200:先排除默认虚拟主机、Host 路由和 SNI 差异,不能直接认定存在绕过。

六、风险与修复建议

  • 后端服务仅监听内部地址,并通过安全组或主机防火墙限制为代理节点可访问。
  • 不要用可由客户端控制的原始请求头作为唯一授权条件。管理接口应同时要求身份认证和明确的角色权限。
  • 只配置实际代理地址为 trusted proxy,禁止使用 0.0.0.0/0 或无边界的大网段。
  • 在最外层可信入口删除外部提交的 Forwarded、X-Forwarded-For、X-Real-IP 等头,再按统一规则生成。
  • 为边缘、代理、应用和鉴权服务保留统一请求标识,记录访问控制规则编号和拒绝原因。
  • 修复后重复基线、伪造头和后端直连三组测试,确认响应结果与日志中的客户端地址均符合预期。

总结

同一接口出现 403 和 200 时,关键不是继续尝试更多头部,而是建立可复现的请求矩阵,并通过 Host、SNI、请求标识和分层日志定位决策点。若客户端可通过转发头影响访问控制,问题本质是代理信任边界错误;若后端能够绕过边缘控制直接访问,则还存在网络暴露问题。修复时应同时收紧后端网络入口、规范化代理头,并避免把来源 IP 当作唯一授权依据。

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

这类 403/200 通常不是接口本身随机,而是“到达应用前的客户端 IP 解析”在不同入口或不同代理链上不一致。建议先把请求路径拆成:客户端 → CDN/WAF/LB → 反向代理 → 应用,并同时记录每一跳看到的源地址和相关头。

先做一组完全相同的请求,只改变入口,不要先改业务参数:

curl -skD /tmp/h1.txt -o /tmp/b1.txt \
  -H 'X-Request-ID: test-001' \
  

curl -skD /tmp/h2.txt -o /tmp/b2.txt \
  -H 'X-Request-ID: test-002' \
  -H 'X-Forwarded-For: 10.0.0.1' \
  

diff -u /tmp/h1.txt /tmp/h2.txt
diff -u <(sed -n 's/.*< //p' /tmp/b1.txt) <(sed -n 's/.*< //p' /tmp/b2.txt)

重点不是只看响应码,而是对比响应头里的 Server、Via、X-Cache、Location、请求 ID,以及响应体是否来自 WAF、网关还是应用。若 403 页面格式明显不同,基本可以先定位到具体一层。

在 Nginx 反代上,建议临时增加结构化日志,至少同时记录 TCP 对端地址、解析后的客户端地址、原始 XFF 和请求 ID:

log_format trace '$request_id remote=$remote_addr realip=$realip_remote_addr '
                 'xff="$http_x_forwarded_for" xri="$http_x_real_ip" '
                 'status=$status upstream_status=$upstream_status '
                 'upstream_addr=$upstream_addr request="$request"';

access_log /var/log/nginx/trace.log trace;

然后核对是否启用了类似下面的配置。set_real_ip_from 必须只填写实际受信任的 CDN、负载均衡或上游代理网段,不能写成全网;real_ip_recursive 的行为也要和代理链设计一致。

set_real_ip_from 10.20.0.0/16;
set_real_ip_from 192.0.2.0/24;   # 示例:实际应替换为受信任代理网段
real_ip_header X-Forwarded-For;
real_ip_recursive on;

如果入口代理会覆盖客户端传入的 XFF,推荐显式重建,而不是直接拼接不受控的请求头。例如 Nginx 到上游:

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP       $remote_addr;
proxy_set_header Host             $host;

但这里要注意:如果当前 Nginx 已经错误地把外部伪造的 XFF 当成真实地址,$proxy_add_x_forwarded_for 仍可能把错误结果继续传给应用。应先确认 realip 模块的信任边界,并从应用侧记录实际收到的头和 socket 对端地址。

应用侧也要检查是否存在“只信任 XFF、不校验代理来源”的逻辑。以 Express 为例,trust proxy 不能简单设置为 true,应限制为实际代理网段或明确的跳数:

app.set('trust proxy', [
  '10.20.0.0/16',
  '192.0.2.0/24'
]);

如果是 Spring、Tomcat、Django 等框架,同样检查其 remote address 过滤器、ForwardedHeaderFilter、USE_X_FORWARDED_HOST 等配置,确认只有来自可信上游的请求才会参与客户端 IP、协议和 Host 的重写。

排查时还要特别看两种容易造成“同接口不同结果”的情况:

  • WAF/LB 按真实 TCP 源地址做 ACL,而应用按 XFF 做白名单,导致前后判断不一致。
  • 同一域名实际落到不同 ingress、不同节点或不同配置版本;用请求 ID、upstream 地址、节点名和配置版本确认是否命中了不同后端。

授权测试中可以在测试环境或明确批准的范围内,分别验证“无 XFF、单个 XFF、多个 XFF、重复 X-Forwarded-For”时各层的记录,但不要直接把任意公网地址用于绕过生产 ACL。最终应形成一张代理链表:每一跳的源地址、写入/覆盖的头、下游信任规则,以及 403 是在哪一层产生。这样通常能很快定位是代理链分叉,还是 XFF 信任边界配置错误。

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

最近浏览 0

  • 没有会员查看此页面。

SiteMap HACKHAT © 2026 All rights reserved.