HTTP 403 Forbidden 表示服务器理解了请求,但拒绝执行。对渗透测试来说,403 往往是个提示:目标路径、接口或对象真实存在,只是当前请求没通过访问控制。真正有价值的测试,不是把 payload 列表机械地打一遍,而是回答一个更基础的问题——是哪一层认为这个请求不该放行,后端又会如何理解同一个请求?
很多 403 绕过问题都来自链路差异:一个请求从浏览器到业务代码,中间要经过 CDN、WAF、反向代理、网关、容器、框架等多道处理,而它们对路径归一化、编码解码、HTTP 方法、代理头和参数取值的规则并不一致。只要负责拦截的一层和真正执行业务的一层看到的请求不一致,就可能出现绕过。
本文只讨论授权环境下的验证思路,目标是证明访问控制是否正确,而非追求对真实系统的破坏性访问。
一、先判断 403 出现在哪一层
403 不是凭空返回的,它由链路中某个组件的访问控制逻辑触发。常见的 403 来源:
- 反向代理 / CDN / WAF:按路径、IP、UA、地理位置、来源 Header、速率做黑名单或 ACL,命中即拦截,常返回 403 或自定义拦截页,依据是请求的字面特征。
- Web 服务器 / 容器:Nginx 的
deny与location限制、Apache<Location>+Require、Tomcat<security-constraint>、IIS 授权规则,在请求进入应用前完成路径与角色校验。 - 应用框架鉴权中间件:Spring Security、Apache Shiro、Django decorator、Express 中间件、Laravel Gate 等,未登录或无权限返回 403,依据是认证态与角色/权限。
- 网关 / BFF:按 API、租户、配额、订阅级别做 ACL。
- 业务代码:控制器或方法内的
if user.role != 'admin'、if resource.owner_id != current_user.id返回 403,依据是对象归属和业务字段。
关键在于不同层的判断依据不同——路径字符串、IP、Cookie/Token、角色字段、对象归属,各看各的。拦截层和执行层依据一旦不一致,缝隙里的差异就是绕过的立足点。
看到 403,第一步不是立刻 fuzz,而是建立基线响应,至少记录这些信息:
- 状态码、响应长度、响应标题、响应体关键词。
Server、Via、X-Cache、CF-*、X-*等响应头。- 是否有跳转、Set-Cookie、登录页、统一错误页或 WAF 拦截页。
- 同路径的
GET、HEAD、OPTIONS响应是否不同。 - 改动 Cookie、Authorization 或登录状态后响应是否变化。
来源层决定测试方向。403 页面明显来自 CDN 或 WAF,优先测路径、编码、Header 和方法差异;来自业务 API,优先测对象 ID、角色、租户、参数污染和服务端授权逻辑;与登录状态强相关,则重点是认证后越权,而非未授权路径绕过。
实用原则是「不要只看状态码」。403 -> 200 最明显,但 403 -> 302、403 -> 405、403 -> 415、响应长度大幅变化、错误码从网关错误变成业务错误,都可能说明请求已穿过原来的拦截层。
二、路径与解析差异
路径绕过是 403 测试中最常见的一类。原因很直接:反向代理通常按路径拦截,后端框架则可能在路由前再次归一化路径。两边规则不同,就可能出现「前端判断不是受保护路径,后端却路由到受保护资源」。
常见测试方向:
1 | /admin |
这些变体本身没有神奇效果,关键看目标链路。Nginx、Apache、IIS、Tomcat、Spring、Node、PHP、Rails 对分号、点号、重复斜杠、编码斜杠的处理并不总是一致。有的配置在代理层匹配原始 URI、后端用归一化后的 path,也有的反过来,导致安全规则和业务路由错位。
后缀也是常见入口。有些访问控制只匹配精确路径 /admin,但框架仍能把 /admin.json、/admin.css、/admin/ 路由到同一个控制器:
1 | /admin.json |
测试时要避免被默认首页或自定义错误页误导。命中后应对比页面标题、业务字段、接口 JSON 结构和响应头,而不是只看 200。
三、Header、方法与来源信任
另一类高频问题来自「信任了不该由客户端控制的信息」。后端若把用户传入的代理头当作真实来源 IP,或允许 Header 覆盖真实路径,就可能绕过前置访问控制。
来源 IP 相关 Header 常见于内网后台、只允许公司出口 IP、只允许 CDN 回源的场景:
1 | X-Forwarded-For: 127.0.0.1 |
更危险的是路径重写类 Header。错误配置下,前置组件检查的是请求行里的 /,后端应用却读取 Header 里的 /admin:
1 | X-Original-URL: /admin |
这类测试一般不是直接请求 /admin,而是请求一个允许访问的路径,再把敏感路径放进 Header:
1 | GET / |
HTTP 方法也值得单独验证。有些 ACL 或 WAF 规则只覆盖 GET 和 POST,但后端路由、框架自动处理或中间件仍会响应其他方法:
1 | HEAD |
同时检查方法覆盖 Header:
1 | X-HTTP-Method-Override: GET |
这里的命中不一定是直接拿到敏感数据。OPTIONS 暴露了 Allow、HEAD 返回了不同的认证行为、POST 从 403 变成 405,都说明访问控制可能只覆盖部分方法,需要继续手工确认。
四、编码、参数与业务授权
编码绕过的核心是解码次数和解码时机不同。WAF 可能只解一次,后端框架可能解两次;网关按原始字符串判断,业务代码拿到的却是规范化后的值。
常见变体:
1 | /%61dmin |
大小写、Unicode、宽字符和零宽字符也可能影响某些 WAF 或过滤器,但这类技巧依赖具体解析链路,不能脱离环境下结论。文章或报告中应明确区分「理论可能」「代码分析推测」和「动态验证成功」。
对 API 来说,403 往往不是路径问题,而是业务对象授权问题。这时应把注意力放到参数和身份边界:
1 | ?id=1001 |
还要检查同一个对象 ID 是否同时出现在 path、query、JSON body、Header 或 Cookie 中:
1 | PUT /api/users/1001 |
如果网关按 path 中的 1001 授权,而业务代码最终更新 body 中的 1002,就形成典型越权。重复参数也类似,不同语言和框架对 id=1&id=2 的取值规则不同:有的取第一个,有的取最后一个,有的变成数组。前后端或网关取值不一致时,权限判断和业务执行对象就可能错位。
五、实战流程与专项工具
实际测试可以拆成三步:建立基线、用工具枚举变体、再看哪些请求真正穿过了拦截层。工具负责扩大覆盖面,结论仍要靠响应差异和业务行为确认。
这类工具适合在单个 403 请求上快速跑组合变体,把拦截到的 403 交给工具,自动尝试路径前后缀、分号点号、重复斜杠、编码斜杠、来源 IP 与路径重写 Header、HTTP 方法替换:
- BypassPro — 面向 401/403、权限与访问控制绕过的规则化插件,按
suffix/prefix/boundary_insert/headers规则自动生成变体,规则可定制。 - BurpSuite_403Bypasser — PassiveScan 默认开启,对每个 403 自动重放常见绕过 payload。
- 403 Bypasser — PortSwigger 官方 BApp,基于 Orange Tsai《Breaking Parser Logic》,做查询型 payload 全排列和方法降级(如
GET→ 空 bodyPOST)。 - bypass-403 — 基于 curl 的命令行 403 绕过脚本,对单个 URL 注入路径前缀/后缀、来源 IP 与自定义 Header、HTTP 方法替换,适合脱离 Burp 在终端批量验证。
插件结果要按「差异」排序,而不是只看 200。某个 payload 让响应从 403 变成 405,说明请求可能已绕过前置拒绝、只是后端方法不允许,这类请求通常值得继续调整方法、Content-Type 或参数。以 /admin 为例,下面两类响应变化值得单独验证:
1 | GET /admin/. |
1 | GET / |
第一种偏路径归一化差异,第二种偏路径重写 Header 差异,风险点和成因层都不同。
归根到底,403 绕过的主线只有一条:让拦截层和执行层看到不一样的请求。路径归一化、Header 信任、HTTP 方法、编码与对象授权,本质上都是在链路的缝隙里找不一致。理解这条主线,工具和 payload 才有用武之地;脱离链路谈 payload,就只是碰运气。