某开发工具沙箱绕过导致RCE

这是一个基于nw.js的开发的工具,有实时预览功能,预览的环境实际上是由预制的一个html渲染出来的,用户写的所有 js 代码也被自动封装成一个 module,通过script标签的方式引入。最终,用户的代码会在一个沙箱环境中被require进来,然后执行。

比如

1
2
3
<script>some code</script>
<script src="your_code_file.js"></script>
<script>require("your_code_file.js")</script>

your_code_file.js 的内容例如

1
2
3
define("your_code_file.js", function(require, module, exports, process){ "use strict";
// your original code
});

其中define语句是工具自动生成的。

工具会在之前的处理逻辑里把global变量以及require函数重写,导致不能直接调用nodejs中原本的require函数。处理后的require函数无法获取到child_process模块。

在这种情况下,我们需要知道有哪些变量可以使用。通过打印出下列变量,我们就可以对当前环境的状况有一定的了解。

  • arguments
  • this
  • new.target
  • window
  • parent
  • top
  • navigator
  • location
  • name
  • global
  • self

可以发现,可以用selfwindowtop等方式获取真正的global变量,其中我们发现了nodejs原版的require函数被存在了一个叫做__noderequire的变量中,直接用它来调用child_process即可命令执行。

几个版本之后发现用了 nodejs 自带的vm模块作为沙箱。不过这个已经早已被证实是不安全的。在另一个 nodejs 的沙箱模块vm2里甚至直接给出了绕过代码。更多的一些姿势可以参考在vm2模块issue里的一些讨论

1
2
const vm = require('vm');
vm.runInNewContext('this.constructor.constructor("return process")().mainModule.require("child_process").spawn("/Applications/Calculator.app/Contents/MacOS/Calculator")');

绕过的思路很简单,找到一个不在沙箱的Context里创建的但是能在沙箱里访问到的变量,从这个变量的属性指针里找出沙箱外的可以执行代码方法,调用即可。在上述代码里,this的创建其实就是在沙箱外的。this.constructorObject() { [native code] }this.constructor.constructorFunction() { [native code] }。这样,通过this.constructor.constructor可以获取沙箱外的Function,即可以用来在沙箱外执行任意代码。其中process.mainModule里有 nodejs 中的require函数,可以用来执行任意命令。

解决这个问题需要处理Object.prototype.constructor到沙箱外面Function的连接,以及在沙箱内重建输入数据。

在接下来的版本迭代中,似乎 nodejs 被禁止使用了。

假如 nodejs 没有被禁用,程序使用了较为安全的沙箱vm2,是不是就没有风险了呢?这里还有一个 osx 下面的绕过方式。

我们回顾用户脚本被加载的过程,是将用户的脚本以 script 标签 src 属性的方式加载到页面中。由于 js 文件会被封装成一个 module,里面的代码不会被立即执行。如果用户的脚本文件名称里带有双引号",即可闭合 src 属性,导致加载其他的文件,如果直接在这个文件中执行 js 代码,此时的执行环节就在沙箱外,可以直接 RCE。

比如我们创建一个 js 文件名为evil.json" ".js。再创建一个名为evil.json的 json 文件。evil.json里有我们恶意的payload,因为不是js后缀,所以里面的内容不会被处理。这样经过渲染,页面变成了如下所示。

1
2
<script>some code</script>
<script src="evil.json" ".js"></script>

evil.json里的恶意代码被执行。

由于在windows环境中无法创建带有双引号的文件名的文件,导致此方法无法在windows下使用。

既然谈到了沙箱,再抄一段禁止eval的绕过方法吧,来源见参考文献。

eval=undefined可以用Function(payload)()绕过。
Function.prototype.constructo=rundefined可以用Object.constructor(payload)()绕过。
Object.getPrototypeOf=undefined可以用Reflect.construct(Function, [payload])()绕过。
Function=undefined可以用(function*(){}).constructor(payload)().next()绕过。

参考文献

https://github.com/patriksimek/vm2/issues/32
https://docs.google.com/presentation/d/1bYFbCtHGimDmqdwE6Um0WZ7O97eWAK-N1qndqrv8niA/pub

分享到 评论