利用Blitz.js中的原型污染漏洞实现远程代码执行

匿名者  620天前

referentialEqualities映射告诉superjson对json的值做如下分配。第三方依赖库是开发人员向其应用程序添加功能的简便方法。虽然这种方法能够提高开发效率,但同时也增加了更多的攻击面和潜在的漏洞。当然,依赖久经考验的库比重新发明轮子要好,但检查代码库是否存在安全漏洞也是非常重要的。

作为我们致力于帮助保护开源世界的一部分,我们决定考察Blitz.js库的安全性:这是一个即将推出的全栈React框架。它基于Next.js,包括身份验证、API层和开箱即用的代码生成等功能,目前已经在GitHub上获得了11,000颗星。

我们在Blitz.js库中发现了一个严重漏洞,该漏洞允许攻击者接管大多数实例。在本文中,我们首先会介绍一个名为Prototype Pollution的类,我们所发现的漏洞就位于这个类中。然后,我们将为读者详细介绍该漏洞的技术细节、影响以及如何防御该漏洞。

漏洞的影响

我们在Blitz.js库的RPC层使用的序列化库superjson中发现了一个原型污染漏洞(CVE-2022-23631)。它会导致服务器上的远程代码执行,未经身份验证的攻击者可以通过互联网利用该漏洞。如果基于Blitz.js库的应用程序实现了至少一个RPC调用,则很容易受到相应的攻击。

该安全问题已在superjson 1.8.1Blitz.js 0.45.3中得到了修复,因此,我们建议大家将这些依赖库更新到这些版本或更高版本。

技术细节

在本节中,我们将首先介绍原型是如何在javascript中工作的,以及什么是原型污染漏洞(PrototypePollution),然后,将为读者展示Blitz.js中的一个真实例子。最后,我们将给出在javascript代码中避免原型污染漏洞的具体建议。

什么是原型污染?

javascript中,类是用所谓的原型实现的。任何对象的原型都可以通过__proto__属性来访问,例如,"abc".__proto__ === String.prototype为真。一个对象从它的原型继承所有的属性,这就是为什么 "abc".substring(1)可以工作的原因:字符串从它的原型String.prototype中继承了substring函数。

实际上,原型就是一些普通对象,这意味着它们可以被修改。例如,如果在原型中添加一个属性,将导致该类型的所有现有对象都将拥有这个属性。

const obj1 = {};

obj1.__proto__.x = 1;

console.log(obj1.x === 1); // true

const obj2 = {};

console.log(obj2.x === 1); // true

javascript解释器遇到表达式obj.x时,它首先在obj本身中寻找x,然后在obj.__proto__,然后在obj.__proto__.__proto__中寻找,以此类推。然后,它将使用找到的第一个原型,如果在obj表达式的所有原型中都没有找到x,则抛出一个错误。正如这所展示的,原型可以被链起来,就像类可以有多级父类一样。对javascript的继承性感兴趣的读者,可以参阅MDN的这篇文章,其中给出了更详细的解释。

原型污染的根本原因,是攻击者可以控制原型的属性。举例来说,一个容易导致这种情况的代码模式是:obj[a][b]=c——如果攻击者控制了abc的值,他们就可以为a设置"__proto__",为b设置属性名,为c设置属性值,这将导致所有对象都获得一个新的属性,这将给目标应用程序带来极大的影响。

javascript代码中,一个常见的模式是使用普通对象来向函数传递可选参数。在下面的例子中,函数doTask可以接收一个包含多个可选参数的对象:

function doTask(name, options) {

   if (options.delay) {

       // handle delay

    }

   if (options.priority) {

       // handle priority

    }

   

   // do the task

}

doTask('dQw4w9WgXcQ', {

   delay: 100,

});

object原型上设置一个新属性将导致所有这些参数对象都具有该新属性,从而改变程序的行为。在上面的示例中,可以在object原型上设置一个新的优先级属性,使所有任务都以该优先级处理。

superjson中的原型污染漏洞(CVE-2022-23631

Blitz.js的特点之一是它易于集成RPC调用。为此,它实现了一个所谓的Zero-API层,这意味着一段业务逻辑可以简单地实现为一个函数,并且客户端可以调用这个函数,而不需要编写API代码。当在客户端进行调用时,Blitz.js将透明地对服务器进行RPC调用,等待响应,然后作为函数调用的结果返回。

对于RPC调用参数的反序列化,Blitz.js已经实现了自己的JSON扩展版本,称为superjson。它增加了对更多数据类型的支持,如日期和正则表达式类型,并允许循环依赖。后者是通过从一个特殊的元数据属性中读取一个赋值操作列表,然后将这些操作应用到数据上实现的。接下来,让我们以下面这个JSON为例进行介绍。

{

 "json": {

   "brands": [

     { "name": "Sonar" }

   ],

   "products": [

     { "name": "SonarQube",  "brand": null }

    ]

  },

 "meta": {

   "referentialEqualities": {

     "brands.0": ["products.0.brand"]

    }

  }

}

referentialEqualities映射告诉superjson对json的值进行如下赋值:

products[0].brand = brands[0];

这些赋值操作对数据中的任何路径都有效。由于赋值目标的路径可以包含任何属性名称,这就引入了一个原型污染的漏洞。攻击者可以使用路径__proto__.xobject.prototype上的x属性设置为他们可以控制的数据中的任何值。

从原型污染漏洞到RCE

为了利用原型污染漏洞,攻击者需要找到导致任意代码执行或其他有趣行为的gadget。接下来,让我们看看构成最终exploit所需的三个gadget

gadget 1:从零到require()

由于Blitz.js是基于Next.js的,因此,它们使用的是相同的路由机制。在构建时,会创建一个页面清单,其中包含HTTP和文件系统路径之间的映射。

当一个请求到达时,服务器将检查映射是否包含与请求路径相匹配的条目。如果存在匹配的条目,它将使用相应的文件系统路径并加载其引用的javascript文件。该文件包含渲染该路径上的页面所需的代码。下面是一个pages-manifest.json的例子。

{

 "/api/rpc/signup": "pages/api/rpc/signup.js",

 "/forgot-password": "pages/forgot-password.html"

}

该文件是使用Node.js库的require()函数加载的,并且不会检查文件路径是否位于某一目录内。而清单是从JSON文件加载的,这意味着生成的对象继承了object.prototype。这使得页面路由功能成为原型污染的gadget,通过在清单中插入一个新的映射,就可以执行任何本地javascript文件。

Gadget 2:从require()到spawn()

要把这个漏洞转换为任意代码执行,攻击者要么需要具有在服务器上创建文件的权限,要么需要另一个gadget与第一个gadget组合使用。由于Blitz.js默认没有任何上传功能,因此,我们需要寻找具有相关行为的现有文件。

该文件必须存在于每一个Blitz.js实例中,所以寻找Blitz.js本身和它的依赖库是最有意义的。一个可能有用的文件是Blitz.js库的CLI包装器脚本。它将在一个新进程中生成实际的CLI脚本并退出。然而,既然命令是固定的,参数也是不可控的,那么,攻击者是利用这一点的呢?

Gadget 3:从spawn()到任意代码执行

生成新进程是一个已知的原型污染漏洞gadget,因Micha? Bentkowski用它来利用Kibana漏洞而名声大噪。

事实上,spawn()函数是通过一个对象来接收可选参数的。因此,攻击者可以用它为具有env属性的子进程设置环境变量。这可以用来设置NODE_OPTIONS,为node进程设置更多的命令行参数。不过,有些参数是不允许的,比如--eval,但参数--require可以用来包含任何文件。这似乎与第一个gadget的作用相同,但有一个区别:由于生成了新进程,因此,文件系统上会产生一些新文件。文件/proc/self/environ包含当前进程的环境变量,这些变量已经被攻击者通过env选项控制了。

利用这一点的常规方法,就是在NODE_OPTIONS之前插入一个新的环境变量,其中包含javascript代码,并提供一个尾部注释以避免语法错误。然而,Node.js现在似乎对NODE_OPTIONS的处理方式有所不同,它总是把它放在environ文件的最前面。

改进gadget 3

为了绕过这一点,攻击者可以使用spawn()函数的另外两个选项:argv0shell。第一个选项,即argv0,用于控制传递给新进程的参数列表中的第一个元素。通常情况下,这就是被执行的二进制文件。整个参数列表都反映在文件/proc/self/cmdline中,所以,第一个元素会在开头位置。如果攻击者将NODE_OPTIONS的值改为--require/proc/self/cmdline,并将他们的payload放在argv0中,这应该是可行的,对吗?

几乎可以这样说,但还有一个最后的障碍。因为第一个参数被改变了,导致进程无法被生成,因为它不是一个有效的命令或文件路径。这可以通过spawn()函数的shell选项来绕过。该选项可以被设置为一个二进制文件的路径,然后被用来在shell中生成命令。在Linux系统上,shell会被添加到命令及其参数的前面,像这样:/bin/myshell -c "command arg1 arg2 arg3"

为了将shell设置为node可执行文件的路径,攻击者可以直接使用/proc/self/exe,而不必知道实际路径。最后的结果是,一个node进程将被生成,具体如下所示:

execve("/proc/self/exe",["console.log('pwned!');//", "-c", "node …"], {NODE_OPTIONS: "--require /proc/self/cmdline" })

将其组合在一起

最终的利用方法是这样的:

  1.     攻击者发送一个请求,利用RPC层的原型污染漏洞,向object原型添加属性。
  2.     这将创建一个新条目,指向页面清单中的Blitz.jsCLI包装器脚本。同时,它还会为步骤3中的spawn()调用设置argv0envshell
  3.     攻击者通过向新创建的页面清单条目的URL发送请求来触发该漏洞链。这将导致CLI包装器脚本被执行,用攻击者控制的argv0envshell选项生成一个新进程,最终在新进程中执行攻击者的payload

1.png

漏洞的修复

为了修复这个安全漏洞,Blitz.js禁止了一些属性名称在路径中的使用,即__proto__constructorprototype属性。没有了这些属性,攻击者就无法利用原型对象了。这可以概括为一条javascript安全经验法则:当使用不受信任的输入来访问或修改一个对象的属性时,一定要确保这三个属性名称被屏蔽。

另一个修复方式,是尽可能使用object.create(null)而不是普通的对象字面量({})。这样的话,由于返回的对象不是从object.prototype派生的,无论使用什么样的属性名称,也不可能访问该原型:

const obj = object.create(null);

object.prototype.x = 1;

console.log(obj.x === 1); // false

console.log(obj.__proto__); // undefined

如果您想对代码库进行安全加固,从而提高原型污染漏洞的利用难度的话,还是有许多方法的,但它们都有各自的弊端。第一种加固措施是通过尽早调用object.freeze(object.prototype)使object.prototype不可变。这种方式的缺点是:必须对每一个类都重复该操作,而且一些老的库会因为修改原型而无法正常使用。

第二个加固措施只适用于Node.js,而不适用于在浏览器中运行的javascript。如果你用--disable-proto=delete标志启动Node.js进程,那么__proto__属性将不再可用,而设置object原型的唯一方法是通过Reflect.setPrototypeOf()等函数。与之前的措施一样,库可能会因此而无法正常使用。另外,攻击者仍然有可能通过obj.constructor.prototype到达一个对象的原型,所以在验证用户控制的属性名时,这些属性名仍然应该被屏蔽。

时间轴

2022-02-07:我们向Blitz.js维护者报告这个安全问题。

2022-02-07:维护者确认该问题。

2022-02-10:安全补丁随superjson1.8.1Blitz.js 0.45.3一起发布。

小结

在本文中,我们为读者介绍了Blitz.js的原型污染漏洞背后的技术细节;Blitz.js是一个全栈React框架。攻击者可以利用该漏洞在运行基于易受攻击的Blitz.js版本的应用程序的服务器上执行代码。此外,我们还介绍了在javascript代码中防止此类问题的各种方法。

如果您在应用程序中使用了Blitz.jssuperjson,我们强烈建议更新到上面提到的修复版本。最后,我们要感谢Blitz.jssuperjson的维护者,感谢他们快速的回复和修复。

参考资料

https://blog.sonarsource.com/nodebb-remote-code-execution-with-one-shot/

https://blog.sonarsource.com/zimbra-pre-auth-rce-via-unrar-0day/

https://blog.sonarsource.com/path-traversal-vulnerabilities-in-icinga-web/

原文地址:https://blog.sonarsource.com/blitzjs-prototype-pollution/

 

最新评论

昵称
邮箱
提交评论