发布于4小时前4小时 背景与测试边界在外网入口到内网服务的渗透测试中,一类容易漏掉的问题是:接口确实配置了内网 IP 白名单,但用于判断来源的地址却能被请求头改写。最终表现为外部请求携带一个内网地址,就获得了原本只面向内网的访问权限。本文使用一个经过授权的测试场景:边缘代理 E → 业务 Nginx B → 本机健康检查服务。业务域名为 app.lab.example,B 的测试地址为 192.0.2.20,E 在后端网络中的地址为 172.20.0.10。受限资源仅为 /internal/health,返回固定健康状态,不涉及管理操作和业务数据。示例地址应替换为授权范围内的实际地址。问题定位:白名单判断的到底是谁先检查实际生效的配置,不要只看某个站点文件。重点寻找真实 IP 模块、信任来源及访问控制指令:sudo nginx -T 2>&1 | grep -nE 'configuration file|listen|server_name|set_real_ip_from|real_ip_header|real_ip_recursive|allow|deny|proxy_pass' nginx -V 2>&1 | grep -- --with-http_realip_module下面是业务 Nginx B 上的危险配置示例,相关指令位于同一个 server 中:set_real_ip_from 0.0.0.0/0; set_real_ip_from ::/0; real_ip_header X-Forwarded-For; real_ip_recursive on; location = /internal/health { allow 10.0.0.0/8; deny all; proxy_pass }这里的问题不是 allow 语法,而是所有连接来源都被当成可信代理。客户端发送 X-Forwarded-For: 10.23.45.67 后,Nginx 可能将 $remote_addr 改为该地址,随后访问控制模块按改写后的地址放行。不要用同一个 location 中的 return 200 模拟受保护资源:return 在重写阶段执行,可能先于访问控制阶段结束请求,导致实验结论失真。这里使用一个已经运行、只返回固定健康状态的本机上游服务。技术分析:三个地址不能混为一谈$realip_remote_addr:真实 IP 模块改写前的连接对端地址,用于确认请求是否经过预期代理。$remote_addr:Nginx 当前用于处理请求的客户端地址,可能已被真实 IP 模块改写。$http_x_forwarded_for:请求收到的原始头部内容,本身不具备可信性。real_ip_recursive on 并不等于安全。它只影响可信代理链中地址的选择方式;如果信任范围写成全网,递归处理也无法补救信任边界错误。操作步骤:用最小请求建立证据链先在 B 的 http 上下文添加专项日志格式,再在目标 server 中启用。配置变更应走测试环境或约定的变更窗口。# http 上下文 log_format ip_boundary escape=json '{"time":"$time_iso8601","rid":"$request_id",' '"peer":"$realip_remote_addr","client":"$remote_addr",' '"xff":"$http_x_forwarded_for","uri":"$uri",' '"status":$status,"upstream":"$upstream_status"}'; # 目标 server 上下文 access_log /var/log/nginx/ip-boundary.log ip_boundary;测试源地址应在允许网段之外。以下命令直接访问 B,绕过边缘代理,仅用于确认 B 自身的信任边界;如果 B 不允许直连,不要为了验证而临时开放,应改用隔离环境复现。# 基线:预期被业务 Nginx 拒绝 curl --noproxy '*' --resolve app.lab.example:80:192.0.2.20 \ -i --max-time 5 # 单变量验证:只增加一个来源地址头 curl --noproxy '*' --resolve app.lab.example:80:192.0.2.20 \ -i --max-time 5 \ -H 'X-Forwarded-For: 10.23.45.67' # 在服务器侧核对相同时间窗口的日志 sudo tail -n 20 /var/log/nginx/ip-boundary.log若第一条返回 403,第二条返回上游健康响应,同时第二条日志显示 peer 仍为测试机地址、client 变为 10.23.45.67,才能较明确地归因于来源地址信任错误。成功请求的 upstream 通常为 200;被 Nginx 本地拒绝的请求通常没有上游状态。仅有状态码差异还不够。403 可能来自边缘 WAF、上游应用或另一个虚拟主机;200 也可能只是统一错误页。需要同时核对目标 server、响应正文和上游日志,确认请求确实进入受限处理路径。修复:同时收紧代理信任和网络入口如果 E 是直接面对客户端的第一跳代理,应覆盖客户端提交的 X-Forwarded-For,而不是保留其中的任意内容。E 转发到 B 的配置示例:# 边缘代理 E 的转发 location 内 proxy_set_header Host $host; proxy_set_header X-Forwarded-For $remote_addr; proxy_pass Nginx B 只信任 E 的准确地址,删除原有的全网信任项:# 业务 Nginx B 的 server 内 set_real_ip_from 172.20.0.10/32; real_ip_header X-Forwarded-For; real_ip_recursive on; location = /internal/health { allow 10.0.0.0/8; deny all; proxy_pass }上述覆盖策略仅适用于 E 是客户端第一跳、且 $remote_addr 未被不可信头部提前改写的情况。如果前面还有 CDN 或负载均衡,应逐跳明确可信来源和地址传递规则,不能照搬后丢失真实客户端地址。通过安全组、主机防火墙或独立监听地址,限制 B 的业务端口仅允许 E 访问。准确配置真实 IP 信任,不等于已经阻止绕过 E 直连。不需要真实客户端 IP 的内部接口,应优先移到独立管理入口,而不是继续与公网业务共享入口。管理操作仍需身份认证和权限校验。内网地址只适合作为附加条件,不能代替授权。记录 peer、client、请求标识及上游状态,但不要将 Cookie、Authorization 等凭据写入专项日志。回归验证与交付证据执行 nginx -t 通过后,再按变更流程重载。至少复测三条路径:外部客户端经 E 携带伪造头仍被拒绝;授权内网客户端经 E 正常访问;外部客户端直连 B 被网络策略阻断。多级代理环境还应验证带多个地址的请求头,防止不同组件对地址链的理解不一致。报告中保留脱敏后的生效配置、两组最小请求、对应日志和修复后的回归结果即可。不要为了证明影响继续访问真实管理接口或导出数据;固定健康接口已经足以证明来源边界可被突破。总结这类问题的核心是把客户端可控数据提升成了授权依据。渗透验证应沿着“连接来自谁、头部由谁写入、访问控制使用哪个地址”逐层核对。修复则需要同时覆盖代理头部处理、后端信任列表、网络可达性和应用授权,单独增加一条 deny 往往解决不了根因。 网络请求、日志与边界流量分析示意 服务器、云资产与权限边界梳理示意
3小时前3小时 这类问题建议把“谁写入真实 IP、谁消费真实 IP、信任链覆盖到哪里”分开验证。重点不是单纯伪造 X-Forwarded-For,而是确认外部请求头是否能穿过代理链并最终影响应用的内网白名单判断。 可以先做一组最小对照测试,分别观察接口状态码和后端访问日志: curl -i curl -i \ -H 'X-Forwarded-For: 127.0.0.1' curl -i \ -H 'X-Forwarded-For: 10.0.0.10' curl -i \ -H 'X-Real-IP: 127.0.0.1' curl -i \ -H 'Forwarded: for=127.0.0.1;proto=https' 日志最好同时记录原始来源和解析后的地址,否则只看应用里的 remote_addr 很容易误判: log_format ip_debug '$time_iso8601 ' 'peer=$remote_addr ' 'realip_remote=$realip_remote_addr ' 'xff="$http_x_forwarded_for" ' 'xri="$http_x_real_ip" ' 'forwarded="$http_forwarded" ' 'request="$request" status=$status'; 如果是多级代理,例如 CDN → SLB → Nginx → 应用,要逐层确认请求头的变化。常见错误是: set_real_ip_from 0.0.0.0/0 或信任了过大的网段; 边缘代理使用追加方式保留了客户端自带的 X-Forwarded-For,内层又取最左侧地址; real_ip_recursive on 开启后,可信代理列表不准确,导致继续向头部回溯到攻击者可控值; 应用框架启用了类似 trust proxy = true 的全量信任; 仅凭 RFC1918 地址、回环地址判断“内网身份”,没有独立认证。 Nginx 侧应只信任实际会连接到本机的代理出口地址,并在最外层覆盖客户端提供的相关头: # 仅填写真实负载均衡器或入口代理网段 set_real_ip_from 10.20.0.0/16; set_real_ip_from 192.0.2.10/32; real_ip_header X-Forwarded-For; real_ip_recursive on; location / { # $remote_addr 应在完成 realip 处理后使用 proxy_set_header X-Real-IP $remote_addr; # 不直接透传外部传入值,由当前代理重新生成 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_pass } 不过要注意:只有在当前 Nginx 确实是公网第一跳时,才适合直接覆盖头。如果前面还有 CDN 或 SLB,应由最外层入口清理外部同名头,再按固定规则逐跳追加;内层代理只信任明确的上游地址。否则简单覆盖可能丢失真实客户端地址。 应用层也建议同步收紧。例如 Express 不要直接使用布尔值全信任: app.set('trust proxy', ['loopback', '10.20.0.0/16', '192.0.2.10/32']); 修复验证时,除了确认伪造头不再改变授权结果,还要覆盖 IPv6 回环地址 ::1、IPv4 映射地址 ::ffff:127.0.0.1、多值头和重复头。部分代理与框架对重复头的合并规则不同,可以额外检查: curl -i \ -H 'X-Forwarded-For: 127.0.0.1, 203.0.113.20' curl -i \ -H 'X-Forwarded-For: 203.0.113.20' \ -H 'X-Forwarded-For: 127.0.0.1' 最终整改不应只停留在“正确取 IP”。管理接口最好使用 mTLS、独立鉴权或网关身份令牌,网络来源只能作为附加限制,不能作为唯一授权条件。这样即使后续代理链调整或可信网段配置漂移,也不会再次直接形成越权。
创建帐户或登录后发表意见