403 绕过不是玄学:从访问控制链路看常见测试思路

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/admin
/admin/
//admin//
/./admin
/admin/.
/admin/./
/admin/../
/;/admin
/.;/admin
/admin;
/admin;/
/%2e/admin
/%252e/admin
/admin/%2f
/admin/%20
/admin/%09

这些变体本身没有神奇效果,关键看目标链路。Nginx、Apache、IIS、Tomcat、Spring、Node、PHP、Rails 对分号、点号、重复斜杠、编码斜杠的处理并不总是一致。有的配置在代理层匹配原始 URI、后端用归一化后的 path,也有的反过来,导致安全规则和业务路由错位。

后缀也是常见入口。有些访问控制只匹配精确路径 /admin,但框架仍能把 /admin.json、/admin.css、/admin/ 路由到同一个控制器:

1
2
3
4
5
6
7
8
9
10
/admin.json
/admin.css
/admin.html
/admin/
/admin/?
/admin??
/admin?id=1
/admin~
/admin.old
/admin.bak

测试时要避免被默认首页或自定义错误页误导。命中后应对比页面标题、业务字段、接口 JSON 结构和响应头,而不是只看 200。

三、Header、方法与来源信任

另一类高频问题来自「信任了不该由客户端控制的信息」。后端若把用户传入的代理头当作真实来源 IP,或允许 Header 覆盖真实路径,就可能绕过前置访问控制。

来源 IP 相关 Header 常见于内网后台、只允许公司出口 IP、只允许 CDN 回源的场景:

1
2
3
4
5
6
7
8
9
10
X-Forwarded-For: 127.0.0.1
X-Forwarded-For: 10.0.0.1
X-Real-IP: 127.0.0.1
X-Client-IP: 127.0.0.1
Client-IP: 127.0.0.1
True-Client-IP: 127.0.0.1
X-Originating-IP: 127.0.0.1
X-Remote-IP: 127.0.0.1
X-Remote-Addr: 127.0.0.1
Forwarded: for=127.0.0.1

更危险的是路径重写类 Header。错误配置下,前置组件检查的是请求行里的 /,后端应用却读取 Header 里的 /admin:

1
2
3
4
5
X-Original-URL: /admin
X-Rewrite-URL: /admin
X-Original-URI: /admin
X-Forwarded-Path: /admin
X-Real-URL: /admin

这类测试一般不是直接请求 /admin,而是请求一个允许访问的路径,再把敏感路径放进 Header:

1
2
3
GET / HTTP/1.1
Host: example.test
X-Original-URL: /admin

HTTP 方法也值得单独验证。有些 ACL 或 WAF 规则只覆盖 GET 和 POST,但后端路由、框架自动处理或中间件仍会响应其他方法:

1
2
3
4
5
6
7
HEAD
OPTIONS
POST
PUT
PATCH
DELETE
TRACE

同时检查方法覆盖 Header:

1
2
3
4
X-HTTP-Method-Override: GET
X-HTTP-Method-Override: POST
X-Method-Override: GET
X-Original-Method: GET

这里的命中不一定是直接拿到敏感数据。OPTIONS 暴露了 Allow、HEAD 返回了不同的认证行为、POST 从 403 变成 405,都说明访问控制可能只覆盖部分方法,需要继续手工确认。

四、编码、参数与业务授权

编码绕过的核心是解码次数和解码时机不同。WAF 可能只解一次,后端框架可能解两次;网关按原始字符串判断,业务代码拿到的却是规范化后的值。

常见变体:

1
2
3
4
5
6
7
/%61dmin
/ad%6din
/%2e/admin
/%252e/admin
/admin/%2f
/admin/%252f
/admin%00

大小写、Unicode、宽字符和零宽字符也可能影响某些 WAF 或过滤器,但这类技巧依赖具体解析链路,不能脱离环境下结论。文章或报告中应明确区分「理论可能」「代码分析推测」和「动态验证成功」。

对 API 来说,403 往往不是路径问题,而是业务对象授权问题。这时应把注意力放到参数和身份边界:

1
2
3
4
5
6
7
8
9
10
?id=1001
?id=1002
?user_id=当前用户
?user_id=其他用户
?tenant_id=当前租户
?tenant_id=其他租户
?role=user
?role=admin
?id=1001&id=1002
?admin=false&admin=true

还要检查同一个对象 ID 是否同时出现在 path、query、JSON body、Header 或 Cookie 中:

1
2
3
4
5
6
7
PUT /api/users/1001 HTTP/1.1
Content-Type: application/json

{
"id": 1002,
"role": "admin"
}

如果网关按 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 → 空 body POST)。
  • bypass-403 — 基于 curl 的命令行 403 绕过脚本,对单个 URL 注入路径前缀/后缀、来源 IP 与自定义 Header、HTTP 方法替换,适合脱离 Burp 在终端批量验证。

插件结果要按「差异」排序,而不是只看 200。某个 payload 让响应从 403 变成 405,说明请求可能已绕过前置拒绝、只是后端方法不允许,这类请求通常值得继续调整方法、Content-Type 或参数。以 /admin 为例,下面两类响应变化值得单独验证:

1
2
3
GET /admin/. HTTP/1.1
Host: example.test
Cookie: session=low_privilege_user
1
2
3
4
GET / HTTP/1.1
Host: example.test
X-Original-URL: /admin
Cookie: session=low_privilege_user

第一种偏路径归一化差异,第二种偏路径重写 Header 差异,风险点和成因层都不同。

归根到底,403 绕过的主线只有一条:让拦截层和执行层看到不一样的请求。路径归一化、Header 信任、HTTP 方法、编码与对象授权,本质上都是在链路的缝隙里找不一致。理解这条主线,工具和 payload 才有用武之地;脱离链路谈 payload,就只是碰运气。

上一篇
下一篇