JavaScript原型链污染
JavaScript原型链污染
参考博客:
1 | https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html?page=1 |
prototype和__proto__
1 | prototype是一个类的属性,所有类对象在实例化的时候将会拥有prototype中的属性和方法 |
例子:
1 | function Foo() { |
1 | foo.__proto__ === Foo.prototype //True |
Foo.prototype
是构造函数 Foo
的原型对象,定义了所有 Foo
实例共享的方法
foo.__proto__
是 foo
对象的原型链上的对象,它指向 Foo.prototype
因此,foo.__proto__
和 Foo.prototype
是相同的对象,foo.__proto__ === Foo.prototype
的结果是 true
JavaScript原型链继承
由于JavaScript没有类似Java中的Class概念,继承都是由原型链来实现的。
所有类对象在实例化的时候将会拥有prototype
中的属性和方法,这个特性被用来实现JavaScript中的继承机制。
比如:
1 | function Father() { |
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
JavaScript的这个查找的机制,被运用在面向对象的继承中,被称作prototype继承链
原型链污染
第一章中说到,foo.__proto__
指向的是Foo
类的prototype
。那么,如果我们修改了foo.__proto__
中的值,是不是就可以修改Foo类呢?
做个简单的实验:
1 | // foo是一个简单的JavaScript对象 |
最后,虽然zoo是一个空对象{}
,但zoo.bar
的结果居然是2
原因也显而易见:
因为前面我们修改了foo的原型foo.__proto__.bar = 2
,而foo是一个Object类的实例,所以实际上是修改了Object这个类,给这个类增加了一个属性bar,值为2
后来,我们又用Object类创建了一个zoo对象let zoo = {}
,zoo对象自然也有一个bar属性了
那么,在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染
哪些情况下我们可以设置__proto__
的值呢?其实找找能够控制数组(对象)的“键名”的操作即可:
- 对象merge
- 对象clone
在JavaScript发展历史上,很少有真正的私有属性,类的所有属性都允许被公开的访问和修改,包括proto,构造函数和原型。攻击者可以通过注入其他值来覆盖或污染这些proto,构造函数和原型属性。然后,所有继承了被污染原型的对象都会受到影响。原型链污染通常会导致拒绝服务、篡改程序执行流程、导致远程执行代码等漏洞。
原型链污染的发生主要有两种场景:不安全的对象递归合并和按路径定义属性
不安全的对象递归合并
以不安全的对象递归合并操作为例,我们定义一个递归合并函数merge()。
该函数来自 https://gist.github.com/ahtcx/0cd94e62691f539160b32ecda18af3d6#file-deep-merge-js
1 | const merge = (target, source) => { |
如果有这样一个场景:job对象是由用户输入的,并且用户可以输入任意对象。那么我们输入一个含有“proto”属性的对象,那合并的时候就可以把person的原型给修改了。 此时如果我们新建一个Person对象,可以发现,新建的对象的原型已被修改
1 | let newperson=new Person("test1",22,"male"); |
需要注意的是,只有不安全的递归合并函数才会导致原型链污染,非递归的算法是不会导致原型链污染的
例如JavaScript自带的Object.assign
1 | function Person(name,age,gender){ |
此时再新建一个Person对象,可以发现,原型未被污染
这是因为Object.assign在合并时,对于简单类型的属性值得到的是深拷贝,如string,number。如果属性值是对象或其他引用类型,则是浅拷贝。上面例子中的proto就是一个浅拷贝,合并后person的原型只是指向了一个新对象,即{“x”: 1},Person.prototype没有受到影响
CVE-2021-25928
POC
1 | var safeObj = require("safe-obj"); |
查看safe-objv1.0.0中lib/index.js中的extend函数定义如下
1 | expand: function (obj, path, thing) { |
第一次调用expand函数,传参如下
1 | obj = {} |
执行split('.')
函数后,props数组值如下
1 | props = (2) ["__proto__", "polluted"] |
此时进入else分支,执行prop.shift()语句后,prop和props的值如下
var prop = props.shift();
:从 props 数组中删除并返回第一个元素(即路径中的第一个部分),并将其赋值给 prop 变量
1 | prop = "__proto__" |
跳过if判断,递归调用expand,props.join('.')
:将 props
数组中的元素连接成一个由.
分隔的字符串
相当于执行
1 | expand(obj[__proto__],"polluted","Yes! Its Polluted") |
再次调用split('.')
后,props的值为”polluted”。props.length===1结果为true,
执行obj[props.shift()]=thing
,由于第二次传入的obj=obj[__proto__]
所以相当于执行obj[__proto__]["polluted"]="Yes! Its Polluted"
造成原型污染
CVE-2021-25927
该漏洞存在于safe-flat,v2.0.0~v2.0.1版本中,POC如下:
1 | var safeFlat = require("safe-flat"); |
具体内容查看博客:
1 | https://www.freebuf.com/articles/web/275619.html |
CVE-2019-11358
3.4.0版本之前的jQuery存在一个原型污染漏洞CVE-2019-11358,POC如下
1 | $.extend(true, {}, JSON.parse('{"__proto__": {"z": 123}}')) |
具体内容查看博客:
1 | https://www.freebuf.com/articles/web/275619.html |
按路径定义属性
有些JavaScript库的函数支持根据指定的路径修改或定义对象的属性值
通常这些函数类似以下的形式:theFunction(object, path, value)
将对象object的指定路径path上的属性值修改为value。如果攻击者可以控制路径path的值,那么将路径设置为__proto__.theValue
,运行theFunction函数之后就有可能将theValue属性注入到object的原型中
CVE-2020-8203
lodash是一个JavaScript实用工具库,提供一致性,及模块化、性能和配件等功能。在4.17.16版本之前,lodash存在一个原型污染漏洞。漏洞原因在于zipObjectDeep
函数能通过指导路径修改Object原型的属性。
漏洞POC如下:
1 | const _ = require('lodash'); |
具体内容查看博客:
1 | https://www.freebuf.com/articles/web/275619.html |
- Title: JavaScript原型链污染
- Author: exp3n5ive
- Created at : 2024-09-03 15:38:42
- Updated at : 2024-09-03 15:39:10
- Link: https://redefine.ohevan.com/2024/09/03/JavaScript原型链污染/
- License: This work is licensed under CC BY-NC-SA 4.0.