js原型链污染
Flow

js原型链污染

参考

https://blog.lxscloud.top/2022/11/13/nodejs%E5%8E%9F%E5%9E%8B%E9%93%BE%E6%B1%A1%E6%9F%93%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/

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

https://drun1baby.top/2022/12/29/JavaScript-%E5%8E%9F%E5%9E%8B%E9%93%BE%E6%B1%A1%E6%9F%93

JavaScript介绍

我的印象就是一个写前端的。接下来是正经介绍:特性是一种具有函数优先的轻量级,解释型或即时编译型的编程语言,js一般作为web页面的脚本语言出名,但是也被用到了很多非浏览器中,js基于原型编程,多范式的动态脚本语言,支持面向对象、命令式、声明式、函数式编程范式,是一门前端语言。

NodeJS

是一个后端语言,可以解释JS,让JavaScript 运行在服务端的开发平台,它让JavaScript成为与 PHP、Python、Perl、Ruby 等服务端语言平起平坐的脚本语言。

JavaScript数据类型

关键字var和let

这两个关键字都可以定义变量

主要区别是:

  • var在全局有效,let只在代码块内有效
  • 所以当在代码块外访问let声明的变量时,会报错
  • var有变量提升,let没有变量提升
  • let 必须先声明再使用,否则报 Uncaught ReferenceError xxx is not definedvar 可以在声明前访问,只是会报 undefined
  • let 变量不能重复声明,var 变量可以重复声明
普通变量
1
2
3
4
5
6
var x=5;
var y=6;
var z=x+y;
var x,y,z=1;

let x=5;
数组变量

用的是花括号

1
2
var a = {};
var a = {"foo":"bar"};

JavaScript函数

js中用function来声明函数

函数声明
1
2
3
4
5
6
7
8
function myFunction() {

}

// 设置传参和返回值
function myFunction(a) {
return a;
}
匿名函数

直接调用匿名函数

1
2
3
(function(a){
console.log(a);
})(123);

也可以把变量设置成函数,调用 fn() 即调用了匿名函数的功能

1
2
3
var fn = function() {
return "将匿名函数赋值给变量"
}
闭包

假设在函数内部新建了一个变量,函数执行完毕之后,函数内部这个独立作用域或(封闭的盒子)就会删除,此时这个新建变量也会被删除。

如何令这个封闭的盒子是不会删除?可以使用“闭包”的方法(闭包涉及函数作用域、内存回收机制、作用域继承)

闭包后,内部函数可以访问外部函数作用域的变量,而外部的函数不能直接获取到内部函数的作用域变量

eg:

不使用额外的全局变量,实现计数器

因为 add 变量指定了函数自我调用的返回值(可以理解为计数器值保存在了 add 中), 每次调用值都加一而不是每次都是 1

以下两种情况对比

1
2
3
4
5
6
7
8
9
var add = (function () {
var counter = 0;
return function () {return counter += 1;}
})();

var add1 = function () {
var cnt = 0;
return cnt += 1;
};

JavaScript类

直接这样定义即可

1
2
3
4
5
function newClass() {
this.test = 1;
}

var newObj = new newClass();

如果是添加一下方法就是这样

1
2
3
4
5
6
7
8
9
function newClass() {
this.test = 123;
this.fn = function() {
return this.test;
}
}

var newObj = new newClass();
newObj.fn();

后面可以用到class关键字,形式如下(如果不定义构造方法,JavaScript 会自动添加一个空的构造方法)

1
2
3
class ClassName {
constructor() { ... }
}

例子

1
2
3
4
5
6
class myClass {
//newClass的构造方法如下
constructor(a) {
this.test = a;//含有一个test属性,值为构造时传入的参数
}
}

用new创建对象

1
let testClass = new myClass("testtest");

然后查看testClass的test属性的值,

1
console.log(testClass.test);

往对象里添加截图,直接用 .调用

1
testClass.aaa = 333;

类的方法形式如下

1
2
3
4
5
6
class ClassName {
constructor() { ... }
method_1() { ... }
method_2() { ... }
method_3() { ... }
}

原型链污染

什么是原型

原型指的是prototype,在前面JS类那里,我们使用new新建一个newClass对象

1
2
3
4
5
function newClass() {
this.test = 1;
}

var newObj = new newClass();

这里new了一个newClass对象赋值给newObj变量

实际上newObj这个对象使用了原型(prototype)实现对象的绑定,而不是绑定在“类”中,鱼JS多特性有关,这个“类”与其他语言类不同,js的“类”是基于原型

prototype是newClass类的一个属性,所有用到newClass类实例化的对象都将拥有这个属性的所有内容,包括变量和方法

总结起来就是:

  • prototype是newClass类的一个属性
  • newClass类实例化的对象newObj不能访问prototype,但是可以通过.__proto__来访问newClass类的prototype
  • newClass是厉害的对象newObj的.__proto__指向newClass的prototype

这样会导致“未授权”的出现,下面的图片画的挺清晰的

原型链污染原理

实例化对象的 .__proto__ 指向类的 prototype

那么如果修改了实例化对象的 .__proto__ 的内容,类的prototype是否也会发生改变

答案是会,这是原型链污染的利用方法

看一个例子

现在有一个类a

1
2
3
function a() {
this.test = 1;
}

然后实例化一个对象obj是a类型的

1
var obj = new a();

此时查看obj的内容,可以说obj有a的一些特性

然后尝试直接修改a类的原型

1
a.prototype.test1 = 123;

然后我们此时再查看obj,会发现里面多了一个test1属性

然后再新实例化一个a类的变量

1
var obj1 = new a();

可以看到obj1里面也有test1属性内容

然后尝试通过obj1的.__proto__属性来修改test1的值

1
obj1.__proto__.test1 = 124;

会发现obj.test1的值和a.prototype内容都被改变了

然后再obj1再添加一个属性,发现obj也有添加

所以我的理解概括就是只要一个变量和某个类有关系(可以说是绑定了),那么不管是用 __proto__还是prototype的方法修改原型,所有相关的变量都会被影响到

存在原型链污染的地方

需要思考有哪些情况可能存在原型链被攻击者修改的情况,需要思考有哪些情况下我们可以设置 __proto__ 的值,答案是找到能够控制数组(对象)的”键名“操作

分为对象merge/对象clone(就是将待操作的对象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),把source的键合并到target
  • 只新增,不覆盖:当某个键同时存在target和source是,不会直接覆盖旧值,而是试图“递归合并”,如果不是对象,就基本不变
  • 递归合并仅限“都为对象”:只有当 target[key] 和 source[key] 都是对象(或数组等可枚举对象)时,递归才会真正把内部字段合起来。
  • 返回值为 undefined:函数没有 return,调用后直接看被修改的 target。

直接看例子理解

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
// 新增键,简单合并
const target = { a: 1 };
const source = { b: 2 };
merge(target, source);
// target => { a: 1, b: 2 } // 新键 b 被加入

// 嵌套对象合并(深合并)
const target = { cfg: { x: 1 } };
const source = { cfg: { y: 2 } };
merge(target, source);
// target => { cfg: { x: 1, y: 2 } } // 同名键且都是对象时,递归合并内部键

// 不会覆盖已有标量
const target = { a: 1 };
const source = { a: 99 };
merge(target, source);
// target => { a: 1 } // 因为两边都有 a,但不是对象,递归无作为,不会覆盖为 99

// 类型不一致时保持旧值
const target = { cfg: { x: 1 } };
const source = { cfg: 10 };
merge(target, source);
// target => { cfg: { x: 1 } } // source.cfg 是数字,递归对数字无作为,旧对象保留

// 数组行为
const target = { list: [1, 2] };
const source = { list: [3] };
merge(target, source);
// target => { list: [1, 2] } // index 0 同名,递归到数字无作为;不会替换为 [3],也不会拼接

所以这个merge方法在合并的过程中存在赋值的操作 target[key] = source[key],所以如果key是 _proto_,就可以做到原型链污染

这里需要用代码实验一下:

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

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

结果显示合并成功了,但是原型链没有被污染

这是因为JS在创建o2的过程let o2 = {a: 1, "__proto__": {b: 2}}中,``proto已经代表o2的原型,现在再遍历o2的所有键名,拿到的是[a,b],_proto`并不是一个key,所以不会被修改

需要修改一下让_proto_被认为是一个键名,代码改成

1
2
3
4
5
6
7
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类已经被污染

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

相关题目

。。。

End

大概就是这些,还有关于js加载顺序那部分p神的文章也解释的非常清楚,还有关于js原型链污染的题目,等后面有机会补充上

无意间看到这个知识点就摸鱼学习记录一下了,之前还是在ctf听说的,感觉就很高级很难。但是搜了一下这个漏洞最近几年已经很少出现了,各个js框架都限制好了,prototype使用也比较谨慎,所以目前来看这块知识点就当是了解吧。

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep