js原型链污染
参考
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 defined;var可以在声明前访问,只是会报 undefinedlet变量不能重复声明,var变量可以重复声明
普通变量
1 | var x=5; |
数组变量
用的是花括号
1 | var a = {}; |
JavaScript函数
js中用function来声明函数
函数声明
1 | function myFunction() { |
匿名函数
直接调用匿名函数
1 | (function(a){ |
也可以把变量设置成函数,调用 fn() 即调用了匿名函数的功能
1 | var fn = function() { |
闭包
假设在函数内部新建了一个变量,函数执行完毕之后,函数内部这个独立作用域或(封闭的盒子)就会删除,此时这个新建变量也会被删除。
如何令这个封闭的盒子是不会删除?可以使用“闭包”的方法(闭包涉及函数作用域、内存回收机制、作用域继承)
闭包后,内部函数可以访问外部函数作用域的变量,而外部的函数不能直接获取到内部函数的作用域变量
eg:
不使用额外的全局变量,实现计数器
因为 add 变量指定了函数自我调用的返回值(可以理解为计数器值保存在了 add 中), 每次调用值都加一而不是每次都是 1
以下两种情况对比

1 | var add = (function () { |
JavaScript类
直接这样定义即可
1 | function newClass() { |
如果是添加一下方法就是这样
1 | function newClass() { |

后面可以用到class关键字,形式如下(如果不定义构造方法,JavaScript 会自动添加一个空的构造方法)
1 | class ClassName { |
例子
1 | class myClass { |
用new创建对象
1 | let testClass = new myClass("testtest"); |
然后查看testClass的test属性的值,
1 | console.log(testClass.test); |

往对象里添加截图,直接用 .调用
1 | testClass.aaa = 333; |
类的方法形式如下
1 | class ClassName { |
原型链污染
什么是原型
原型指的是prototype,在前面JS类那里,我们使用new新建一个newClass对象
1 | function 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 | function a() { |
然后实例化一个对象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 | function merge(target, source) { |
这里写一下这个方法具体是做什么的
- 就地合并(修改的是target),把source的键合并到target
- 只新增,不覆盖:当某个键同时存在target和source是,不会直接覆盖旧值,而是试图“递归合并”,如果不是对象,就基本不变
- 递归合并仅限“都为对象”:只有当 target[key] 和 source[key] 都是对象(或数组等可枚举对象)时,递归才会真正把内部字段合起来。
- 返回值为 undefined:函数没有 return,调用后直接看被修改的 target。
直接看例子理解
1 | // 新增键,简单合并 |
所以这个merge方法在合并的过程中存在赋值的操作 target[key] = source[key],所以如果key是 _proto_,就可以做到原型链污染
这里需要用代码实验一下:
1 | let o1 = {} |
结果显示合并成功了,但是原型链没有被污染

这是因为JS在创建o2的过程let o2 = {a: 1, "__proto__": {b: 2}}中,``proto已经代表o2的原型,现在再遍历o2的所有键名,拿到的是[a,b],_proto`并不是一个key,所以不会被修改
需要修改一下让_proto_被认为是一个键名,代码改成
1 | let o1 = {} |
现在再看o3,它也存在b属性,说明Object类已经被污染

这是因为Json解析的情况下,__proto__会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历o2的时候会存在这个键。
相关题目
。。。
End
大概就是这些,还有关于js加载顺序那部分p神的文章也解释的非常清楚,还有关于js原型链污染的题目,等后面有机会补充上
无意间看到这个知识点就摸鱼学习记录一下了,之前还是在ctf听说的,感觉就很高级很难。但是搜了一下这个漏洞最近几年已经很少出现了,各个js框架都限制好了,prototype使用也比较谨慎,所以目前来看这块知识点就当是了解吧。