目录
原型污染攻击, 如名所示, 是通过污染一个基础对象的原型来引发RCE。Olivier Arteau 完成了此项研究,并在NorthSec 2018中介绍了它的发生过程。让我们用Nullcon HackIm 2019 challenge中的proton
来深入地了解一下它的触发过程。
javaScript中的对象其实就是一些键值对的集合,每一个键值对叫做一个属性。举例说明如下(你可以用浏览器的控制台来自己执行一下试试):
在上述例子中,name
和website
是对象obj
的属性。如果你仔细观察上述语句,就可以发现除了我们显式定义的内容,console.log
还打印了很多额外的信息,这些多出来的信息是来自于哪里呢?
Object
是最基础的对象,其他所有的对象都是通过它创建的。在对象创建期间,我们可以通过传递一个null
参数创建一个空对象(没有任何属性),但默认情况下,它会创建一个与其值类型相对应的的对象,并将所有属性继承给新创建的对象(除非值为null)。
javaScript中的函数/类
在javaScript中,函数和类的概念是相对的(函数本身充当类的构造函数,而且也没有一个显式的表示类的方法)。让我们举个例子:
在上述例子中,我们定义了一个名为person
的函数,然后创建了两个对象,分别叫做person1
和person2
。如果仔细观察新创建的函数和对象的属性,我们可以发现2件事:
Constructor
是一个神奇的属性,它返回用于创建这个对象的函数。原型对象有一个Constructor,它指向函数本身而且Constructor的Constructor是一个全局函数Constructor。
在这里要注意的一点是prototype属性可以在运行时被修改(增/删/改)。比如:
在上面这段代码中我们通过添加一个新属性的方式修改了函数的prototype,使用对象我们也可以达到同样的效果:
有发现一些可疑的东西吗?我们修改了person1
对象但为什么person2
也被修改了?原因是在第一个例子中我们是使用person.prototype
添加的属性,而在第二个例子中我们是通过使用对象来实现同样的效果的。我们知道,constructor返回的是创建对象时的函数,所以person1.constructor
指向person
本身,而person1.constructor.prototype
和person.prototype
相同。
让我们举个例子,obj[a][b] = value
,如果攻击者可以控制a
和value
,他就可以把a
的值设为__proto__
,而程序中所有对象中的属性b
的值会被设为value
。
攻击者所做的事情并不像上面这个例子这么简单,根据论文所介绍的,只有在下面3个条件同时满足时,漏洞利用才会发生:
我们先看看Nullcon HackIM比赛中的实战例子,这场比赛中通过迭代MongoDB id(这不重要),我们能够获得下列源代码:
代码以一个merge
函数开头,事实上这种合并两个对象的方式是不安全的。因为在最新的库中merge()
函数已经被打了补丁,所以在这场比赛中故意使用了旧的merge
函数的实现方法。
我们可以发现,在上述代码中有两个admin
的定义,一个是const admin
另一个是var admin
。理想情况下,JavaScript不允许将Const变量再次定义为var,因此这必须是不同的。要花很长时间才能想明白其中一个是正常的a
,而另一个a
表示另一种含义。所以与其花时间在这里,我把其中重命名为正常的a
,这样,一旦问题解决,我们就可以发送payload了。
从这场比赛的源代码中,我们可以发现下述问题:
Merge()函数的实现方式使得原型污染可以发生(更多的分析在这篇文章之后),所以我们确实需要使用原型污染来解决这个问题。
在通过clone(body)
发送/signup
调用merge
的时候,我们可以在signing up的时候发送我们的JSON payload,在payload中可以添加admin
属性,然后调用/getflag
获取flag。
正如上面讨论的那样,我们可以使用__proto__
(指向constructor.prototype
)来用值1
创建admin
属性。
最简单的payload是:{"__proto__": {"admin": 1}}
所以最终的解决这个题目的payload(使用curl是因为我用burp发送不了)是:
一个很显眼的问题是,为什么merge()函数不安全?下面是原因:
这个函数对对象b
的所有属性进行迭代(因为对象b
在键值对相同的情况下拥有更高的优先级)
如果属性同时存在于第一个和第二个参数中,且他们都是Object
,它就会递归地合并这个属性。
现在我们如果控制b[attr]
的值,使其值变成__proto__
,且我们能控制b
中__proto__
属性的值,在递归的时候,a[attr]
在某个特定的时候就会指向对象a
的prototype
,我们就能成功地添加一个新的属性到所有的对象中了。
仍然没看懂?这不怪你,因为要理解这个概念我也是花了很长的时间才明白。我们先写个debug语句来看看具体发生了什么。
我们先试着发送上面的curl请求。可以发现,对象b
的值目前是:{ __proto__: { admin: 1 } }
,其中__proto__
只是一个属性名,并不指向函数的prototype
,接下来的函数merge()
中,for (var attr in b)
迭代每个属性,而其中第一个属性就是__proto__
。
因为__proto__
总是对象类型的,所以继续递归调用,也就是调用merge(a[__proto__], b[__proto__])
.这样就能帮助我们访问a
的函数原型,并向里面添加一个b
中的属性。
原文:https://blog.0daylabs.com/2019/02/15/prototype-pollution-javascript/
翻译:看雪翻译小组 梦野间
校对:看雪翻译小组 lordVice
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)