3rsh1
/single dog/college student/ctfer
3rsh1's Blog

javascript原型链污染

prototype和__proto__

js在定义一个类的时候需要以定义“构造函数”的形式进行:

function Foo() {
    this.bar = 1;
}//Foo类,其内的一个属性为bar
new Foo();

同样作为一个类也不可避免的有一些方法,这些方法也是定义在“构造函数的内部”:

function Foo(){
    this.bar=1;
    this.show=function(){
        console.log(this.bar);
    }
new Foo();
}

这样的方法是和类的对象绑定的,即每当初始化一个对象就会调用一次function函数给show赋值。如果我们只希望是在类创建的时候调用一次函数,有没有简便一点的方法呢?这可能就需要用到prototype(原型)了。

function Foo() {
    this.bar = 1;
}
Foo.prototype.show = function show() {
    console.log(this.bar);
}
let foo = new Foo();
foo.show();//这里的分号加不加都可以的

我们可以认为prototype是类的一个默认的属性,而因为js内万物皆对象,这个对象里面又有一些属性和方法。而Foo类会继承prototype属性内的所有内容。我们也称prototype是类的原型。这里的访问形式也是不同的:

Foo.prototype   //类的访问方式
foo.__proto__   //对象的访问方式
两者是等价的。

所以,总结一下:
1. prototype是一个类的属性,所有类对象在实例化的时候将会拥有prototype中的属性和方法
2. 一个对象的proto属性,指向这个对象所在的类的prototype属性

js内的继承链

所有类的实例化对象都会有prototype原型对象内的类和属性,那么这就变相的构成了一个继承的关系,我们可以通过指定类的prototype对象来改变类的继承关系。

function Father() {
    this.first_name = 'Donald'
    this.last_name = 'Trump'
}
Father.prototype.show=123;
function Son() {
    this.first_name = 'Melania'
}
Son.prototype = new Father()
let son = new Son()
console.log(`Name: {son.first_name}{son.last_name}`)
https://www.3rsh1.cool/wp-content/uploads/2020/06/wp_editor_md_c8027cf95b37a2647c7081e65709afca.jpg

基于上面的继承关系我们来分析一下继承关系:
son.__proto__指向的是Son.prototype也就是father类的一个对象。因为son.__proto__指向的也是一个对象,因此son.__proto__.__proto__则是指向的是Father.prototype即Father类的原型对象,即对象内只有一个属性show的对象。那么再继续寻找呢,son.__proto__.__proto__.__proto__,因为Father的原型对象指向的就是最原始的类的一个对象,原始类的原型对象(最原始的类的原型)的原型肯定是不存在的即为null了。

console.log(son.__proto__.__proto__.__proto__.__proto__)    //null
console.log(son.__proto__.__proto__.__proto__)    //{}
console.log(son.__proto__.__proto__)    //Father { show: 123 }
console.log(son.__proto__)  //Father { first_name: 'Donald', last_name: 'Trump' }

那么这些原型对象是怎么起作用的呢?

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

原链污染

继续第一个例子:因为Foo类的对象包含Foo的原型对象的所有属性和方法,那么如果我们修改Foo.__proto__的内容就可以间接的修改Foo类的内容。

// foo是一个简单的JavaScript对象
let foo = {bar: 1}

// foo.bar 此时为1
console.log(foo.bar)    //1

// 修改foo的原型(即Object)
foo.__proto__.bar = 2

// 由于查找顺序的原因,foo.bar仍然是1
console.log(foo.bar)    //1

// 此时再用Object创建一个空的zoo对象
let zoo = {}

// 查看zoo.bar
console.log(zoo.bar)    //2

zoo明明是个空对象但是,他的bar属性却有内容为2,这也说明了我们可以通过修改类的原型来控制其父类和子类的内容。那么为什么会这样呢?foo是Object类的一个实例,foo.__proto__表示的是Object类的一个原型对象,所以相当于修改了Object类的内容,我们建立的zoo是Object类的另外一个实例对象,包含了其原型的所有内容自然也包括bar=2,因此会有输出内容。
那么,在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染。

深入理解原链污染:

因为js语言内万物皆是对象,那么有对象就会有结构体或者类这种东西。

var a='heiheihei';
var b='123';
var c=123;
a.__proto__.__proto__.test=123;
console.log(a.__proto__.__proto__); //{ test: 123 }
console.log(b.test);    //123
console.log(c.test) //123

直接上例子,a是一个字符串变量,即为一个字符串对象那么a.__proto__对应的就是字符串类的prototype对象,这个对象应该是和Object类的对象一个层次的,或者说就是Object类的一个对象。那么我们就可以设想,int类型的对象也会有这样的一个原型对象,这个原型对象和字符串类的原型对象并不完全一样。那么你污染了字符串类的原型对象的话,对于int类的对象是不起作用的。
再向上想,Object类的原型对象是对Object类的所有对象都起作用的,只要我们污染了Object类的原型对象就对所有Object类的对象都起作用,那么就会对所有比Object类低一层次的类的对象都会起作用在例子中对int类和string类的对象起作用。
这也变相的说明了,int类的原型对象和string类的原型对象都是Object类的一个对象。

如何利用?

我们找到可以修改对象(数组)的键名的操作即可:
– 对象merge
– 对象clone(其实内核就是将待操作的对象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]
        }
    }
}

如果key值为__proto__,那么在target[key] = source[key]赋值的时候,后者的__proto__被解析成键名,前者的__proto__被解释成原型对象,那么这个赋值的操作会不会修改原型呢?
实验代码:

let o1 = {}
let o2 = {a: 1, "__proto__": {b: 2}}
console.log(o2)//{ a: 1 }
merge(o1, o2)
console.log(o1.a, o1.b) //1 2
o3 = {}
console.log(o3.b)   //undefined

我们发现这个操作似乎并未影响到Object类,原因是因为在解释o1变量的时候,__proto__已经被解释成原型而不是键值,在merge之前已经完成了对Object类的原型对象的处理,故__proto__并不会被当做键值赋给对象o2。

这是因为,我们用JavaScript创建o2的过程(let o2 = {a: 1, “_ proto__”: {b: 2}})中, proto 已经代表o2的原型了,此时遍历o2的所有键名,你拿到的是[a, b], proto _并不是一个key,自然也不会修改Object的原型。

https://www.3rsh1.cool/wp-content/uploads/2020/06/wp_editor_md_7aed3a3273650831825d9da712b06653.jpg

如果换成下面的实验代码:

let o1 = {}
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}') //{ a: 1, __proto__: { b: 2 } }
merge(o1, o2)
console.log(o1.a, o1.b) //1 2

o3 = {}
console.log(o3.b)   //2

//JSON.parse() 方法用于将一个 JSON 字符串转换为对象。

JSON.parse函数将o2的内容看作了一个字符串,而那么__proto__项也是字符串的一部分,经过函数的处理,字符串变为对象,而__proto__理所应当的变成了一个键值。

https://www.3rsh1.cool/wp-content/uploads/2020/06/wp_editor_md_6de28a9c83d8f15b609f9a559bf683ed.jpg

我们可以看到这里的,__proto__项和原型对象是做了区分的了。因此这样的原型链污染是会生效的,因为等号前面的target[__proto__]会被解释成原型对象,而赋值也是对原型对象赋值,原链污染成功。

在javascript中一切皆对象,因为所有的变量,函数,数组,对象 都始于object的原型即object.prototype。同时,在js中只有类才有prototype属性,而对象却没有,对象有的是__proto__和类的prototype对应。且二者是等价的

p神博客上的某道题目

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')   //设置模板引擎,render engine

app.all('/', (req, res) => {
    let data = req.session.data || {language: [], category: []}
    if (req.method === 'POST') {
        data = lodash.merge(data, req.body) //将body的内容复制到data内,然后将结果赋值给data,session.data给
        req.session.data = data
    }

    res.render('index', {
        language: data.language,
        category: data.category
    })
})

app.listen(3000, () => console.log(`Example app listening on port 3000!`))

这里的lodash.template函数和lodash.merge(data, req.body)函数是突破点,首先merge函数可以污染原型,template函数可以调用污染的原型。

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

options是一个对象,sourceURL取到了其options.sourceURL属性。这个属性原本是没有赋值的,默认取空字符串。
但因为原型链污染,我们可以给所有Object对象中都插入一个sourceURL属性。最后,这个sourceURL被拼接进new Function的第二个参数中,造成任意代码执行漏洞。
我将带有proto的Payload以json的形式发送给后端,因为express框架支持根据Content-Type来解析请求Body,这里给我们注入原型提供了很大方便:

题目还是看不太懂先放个解在这边。

https://www.3rsh1.cool/wp-content/uploads/2020/06/wp_editor_md_483aed4a3a713ad44794d23c18600705.jpg

参考链接:

https://xz.aliyun.com/t/4229#toc-4
https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html
https://www.anquanke.com/post/id/176884#h3-6

发表评论

textsms
account_circle
email

3rsh1's Blog

javascript原型链污染
prototype和__proto__ js在定义一个类的时候需要以定义“构造函数”的形式进行: function Foo() { this.bar = 1; }//Foo类,其内的一个属性为bar new Foo(); 同样作为一个类也不可…
扫描二维码继续阅读
2020-06-20