Javascript 中的原型链污染漏洞
才不是为了 bonus 而写的呢
哎嘛, +15%, 真香
Javascript 中的对象
在 Javascript 的世界中, “万物皆是对象”, 同时, JS 的对象和一些其它语言 (如 C++, Python) 中的对象存在一定的差异…
当我们创建一个对象的时候, 我们实际上在讨论什么?
我们用三种语言分别创建一个 “空” 的对象
class Foo{ |
class Foo: |
var foo = {} |
对于 C++ 来说, 一个空的对象, 或许就是一个空的对象: 如果学习过一定时间的 C++, 就会知道这样方法创建的对象仅仅有构造函数和析构函数, 而且是默认的构造函数和析构函数
这样干净的对象, 基本不存在可以利用的漏洞 (指对象层面的漏洞, 与 C++ 其他底层的漏洞, 如内存溢出等无关)
Python 就不一样了, 一个空的对象, 实际上隐藏着许多内置方法
当然, 这些方法的存在肯定会导致漏洞, 例如 CTF 中的沙箱逃逸就很多的利用了 Python 这种特性
让我们回到 JS: JS 会更倾向于哪一种呢?
很显然, 是自带很多内置方法的那种
即便创建的是一个空的对象, foo
依然自带许多内置的属性和方法, 例如 toString 方法
首先要明确的一点是, JS 并没有严格区分函数和类
什么意思呢? 对于下面这段程序来说
function Foo(){ |
在实例化 Foo 类的时候, 函数 Foo() 被调用了, 同时 Foo() 是可以作为一个函数单独调用的
这和构造函数十分相似, 也就是说, 在 JS 中, 函数 Foo() 是类 Foo 的构造函数
通过 construct 方法也可以查看 foo 的构造函数以证明我们的猜测
foo.construct // Foo(){...} |
因此可以得出第一个结论: 在 JS 中, 类和函数没有明确界限, 也就是说, 我们可以像使用类一样使用函数, 也可以像使用函数一样使用类
__proto__ 和 prototype
JS 的所有对象都有一个 prototype 属性和 __proto__ 属性
修改类的属性和方法
类可以通过修改 prototype 属性实现在程序运行过程中动态修改类的属性 (真的牛逼, 想想都知道多危险)
例如下面的程序
function Shape(name){ |
在最开始, Shape 是没有 change_name 方法的, 但是通过 Shape 的 prototype 属性可以为其添加这么一个方法, 并且之后新构造的对象都会有这么一个方法
实现继承
prototype 属性可以实现继承
function BaseClass(x){ |
通过修改派生类 DerivedClass 的 prototype 属性, 使 DerivedClass 继承了 BaseClass
这种继承模式下, 派生类实际上继承的是基类的某个实例化, 例如在这里, DerivedClass 继承的就是 x = 1 的 BaseClass 的实例化
function DerivedClass_(){ |
原型链
对于类, 可以用 prototype 追踪到它的父类, 对于对象, 则可以通过 __proto__ 来追踪到它的父对象
var foo = new DerivedClass(); |
并且这种追踪是可以递归的, 最终会指向 null
foo.__proto__.__proto__ // {} |
上面这种像链子一般的追溯, 就称为原型链
同时我们可以猜想对象之间的一种拓扑结构
null |
因此我们可以得出第二个结论: 可以通过子类修改父类, 甚至是父类的父类
原型链污染
那么问题来了, 我们都知道 JS 是一种脚本语言, 假设我们有三个对象 a, b, c, 继承关系分别是 b 继承自 a, c 也继承自 a
假如攻击者已经有了 b 的完全权限, 根据之前的讨论, 攻击者可以通过 b 来修改 a (这是显而易见的), 那么如果通过 b 修改了 a, 之前继承自 a 的 c 会不会被修改呢?
请看下面的实验程序
var a_ = {} |
结果非常 amazing 啊, 通过 b 修改了 a 之后, c 也跟着被修改了! 即便是原型没有的属性, 都可以通过 b 赋予给 c
与之类似的, 假如攻击者控制了一个对象, 并通过修改原型, 影响了所有这个对象的兄弟对象, 也就是同一个父类的子类对象, 这就被称为原型链污染
递归漏洞
以下程序展示了一个经典的原型链污染的递归漏洞
// program |
对于这个看上去没有问题的 merge 函数, 实际上只要构造一定的 payload 就可以实现原型链污染
以给出的 payload 为例, 假如 src 被定义为对象 {"a": 1, "__proto__": {"b": 2}}
, 执行 merge(tgt, src)
之后, 由于 src 的 key 均没有出现在 tgt 中, 就会将 src 的 key 赋值给 tgt
在吧 src.__proto__ 赋值给 tgt 时, 根据前文所介绍的, 就实现了原型链污染, 使得任何一个对象都包含了 {"b": 2}
成员
为什么要使用 JSON.parse()
?
请看下面的例子, 如果直接用对象的方法定义, 则 __proto__ 会被隐藏, 就无法让 tgt 的 __proto__ 被修改为 src 的 __proto__
实例
下面分别从攻击者和防御者的角度介绍一些原型链污染的实际应用
攻击
拒绝服务攻击
JS 中的所有对象都默认包含一些方法, 比如之前提到的 toString 方法, 如果程序包含了这个方法, 攻击者又通过原型链污染的方法污染了这个方法 (?), 就会发送拒绝服务攻击
例如某个程序的功能是字符串化一个数组 arr.toString()
如果攻击者通过某种方法污染了数组的这个方法 arr.__proto__.toString()
, 那么这个程序就无法工作
任意代码执行
与拒绝服务攻击类似, 例如某个程序的功能是执行某个对象的某个属性 (代码), 攻击者就可以通过污染这个对象的该属性, 以达到任意代码执行的效果
防御
原型冻结
通过原型冻结的方法可以避免对象被修改
var frozen_obj = Object.freeze({"a": 1}) |
无原型对象
通过创建无原型的对象, 可以避免原型链污染攻击
var no_proto_obj = Object.create(null) |