4.11 调用 OS 命令引起的安全隐患
Web 开发所使用的编程语言中,大多数都能够通过 Shell 执行 OS(操作系统)命令。通过 Shell 执行 OS 命令时,或者开发中用到的某个方法其内部利用了 Shell 时,就有可能出现 OS 命令被任意执行的情况。这种现象被称为 OS 命令注入,接下来本节就将详解 OS 命令注入这一安全隐患。
4.11.1 OS 命令注入
概要
如上所述,Web 应用开发使用的编程语言中大多都提供了通过 Shell 调用 OS 命令的功能,而如果调用 Shell 功能的方法不当,就可能导致意料之外的 OS 命令被执行。这被称为 OS 命令注入漏洞。Shell 是用来启动程序的命令行界面,比如 Windows 的 cmd.exe 和 Unix 的 sh、bash 等。OS 命令注入漏洞就是对 Shell 功能的恶意利用。
一旦 Web 应用中存在 OS 命令注入漏洞,外界的攻击者就能够使用各种各样的方式来发动攻击,危险性极高。以下为典型的攻击流程。
1. 从外部下载专门用来攻击的软件
2. 对下载的软件授予执行权限
3. 从内部攻击 OS 漏洞以取得管理员权限(Local Exploit)
4. 攻击者在 Web 服务器上为所欲为
攻击者能够在 Web 服务器上进行的恶意行为有以下几种。
- 浏览、篡改或删除 Web 服务器内的文件
- 对外发送邮件
- 攻击其他服务器(称为垫脚石)
可见 OS 命令注入漏洞的危害极大,因此在开发过程中一定要避免该漏洞的产生。
OS 命令注入漏洞总览
攻击手段与影响
首先让我们来看一下针对 OS 命令注入漏洞的典型的攻击模式及其影响。
- 调用 sendmail 命令发送邮件
这里我们以如图 4-97 所示的填写反馈信息的表单为例来说明 OS 命令注入漏洞。首先来看一下正常的运行情况。
图 4-97 反馈表单的页面跳转
输入表单的 HTML 代码如下。
代码清单 /4b/4b-001.html
<body> <form action="4b-002.php" method="POST"> 请输入您的问题 <br> 邮箱地址 <input type="text" name="mail"><br> 提问 <textarea name="inqu" cols="20" rows="3"> </textarea><br> <input type="submit" value=" 发送 "> </form> </body>
接收页面的脚本如下。通过在
system
函数中调用 sendmail 命令,将邮件发送至表单中所填入的邮箱地址 59 。邮件的信息固定为 template.txt 文件的内容。代码清单 /4b/4b-002.php
<?php $mail = $_POST['mail']; system("/usr/sbin/sendmail -i <template.txt $mail"); // 以下略 ?> <body> 提问已受理 </body>
下面为邮件模板 template.txt 的示例。此处的 Subject 消息头已根据邮件的规则进行了 MIME 编码。
代码清单 /4b/template.txt
From: webmaster@example.jp
Subject: =?UTF-8?B?5M+X44GR5LuM44GR44G+44GX44Gf?= Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: 8bit 提问已受理
收到以上表单发送的邮件后,邮件客户端的显示如下。
图 4-98 收到邮件
- OS 命令注入攻击与影响
下面我们来对这段脚本实施 OS 命令注入攻击。在表单的邮箱地址输入框中填入以下内容。
bob@example.jp;cat /etc/passwd
点击发送按钮后,如图 4-99 所示,页面上显示了 /etc/passwd 文件的内容。
图 4-99 攻击成功
虽然在上面的攻击示例中,攻击者只是查看了文件内容,但实际上,通过 OS 命令注入攻击,攻击者能够执行 Web 应用的用户权限所能够执行的所有命令。比如删除或更改文件、下载外部文件、使用下载的恶意软件等。
针对 OS 命令注入漏洞的典型的攻击方法为,下载攻击 OS 漏洞的恶意代码,并通过内部攻击取得管理员权限。这样,攻击者就能够完全支配 Web 服务器。
- 通过添加命令选项进行攻击
根据应用中调用的 OS 命令,有时也能通过添加命令选项的方法来发动攻击。比如 Unix 的 find 命令。find 命令是通过指定条件来查找文件的命令,但是,在指定了 -exec 选项后,find 就能够针对查找结果的文件名执行命令。由此可见,仅通过添加 OS 命令的选项,也可能会造成意料之外的 OS 命令被执行。
- 通过添加命令选项进行攻击
59 收件人通过 sendmail 命令的选项来指定。-i 选项表示禁止通过行首的点号结束邮件。
安全隐患的产生原因
内部调用 OS 命令的函数以及系统调用(System Call)中,多数都通过 Shell 来启动命令。Shell 是用来操作 OS 的命令行界面,如 Windows 中的 cmd.exe、Unix 系的 OS 中的 sh、bash、csh 等。通过 Shell 来启动命令,能够使管道命令(Pipe)或重定向等功能的使用变得更加便捷。
图 4-100 通过 Shell 调用 OS 命令
然而,Shell 提供的便利功能却会成为 OS 命令注入漏洞产生的根源。Shell 提供了一次启动多个命令的语法,因此外界就可以在参数中做手脚,使得在原来的命令的基础上又有其他的命令被启动。这就是 OS 命令注入。
还有一种情况是,虽然开发者并没有想要调用 OS 命令,但却在无意中使用了内部会启动 Shell 的函数。典型的例子为 Perl 的 open
函数,详情会在本节的最后讲述。
综上所述,产生 OS 注入漏洞的情况有如下两类。
- 通过 Shell 调用 OS 命令时,没有转义 Shell 的元字符
- 使用了内部调用 Shell 的函数
下面就让我们来依次看一下这两种情况。
- 在 Shell 中执行多条命令
Shell 提供了通过指定 1 行来启动多个程序的方法。而 OS 命令注入攻击就是恶意利用了 Shell 能够启动多个程序的特性。比如,在 Unix 的 Shell 中,能够使用以下写法。
执行例 在 Shell 中执行多条命令
Windows 的 cmd.exe 中能够使用 & 来连续执行多条命令(同 Unix 的 ;)。另外 |(管道功能)、&& 或 || 的用法也和 Unix 一样 60 。
Shell 中拥有特殊意义的字符(如 ;、| 等)被称为 Shell 的元字符。把元字符当作普通字符使用时需要对其进行转义。但由于 Shell 的元字符的转义方法很复杂,因此此处不做说明,详情可以参考 Shell 的相关手册。
而如果在指定 OS 命令参数的字符串中混入了 Shell 的元字符,就会使得攻击者添加的 OS 命令被执行,这也就是 OS 命令注入漏洞产生的原因。
- 使用了内部调用 Shell 的函数
Perl 的
open
函数,顾名思义,是用于打开文件的函数。然而根据open
的调用方法的不同,有些情况下会通过 Shell 执行 OS 命令。例如,通过open
函数启动 Linux 的 pwd 命令(显示当前目录名的命令)时,只要像下面的 CGI 脚本一样,调用open
函数时在命令名后面加上管道符号 | 即可。代码清单 /4b/4b-003.cgi
#!/usr/bin/perl print "Content-Type: text/plain\n\n<body>"; open FL, '/bin/pwd|' or die $!; print <FL>; close FL; print "</body>";
执行该脚本后,当前目录名就会通过 pwd 命令显示出来。
在使用了 Perl 的
open
函数的脚本中,如果外界能够指定文件名,就能通过在文件名的前后加上管道符号 | 来实施 OS 命令注入攻击。接下来我们就来演示如何发动攻击。以下为一段 CGI 脚本,其作用仅限于打开文件并将其显示。
代码清单 /4b/4b-004.cgi
#!/usr/bin/perl use strict; use utf8; use open ':utf8'; # 将默认字符编码设为 UTF-8 use CGI; print "Content-Type: text/plain; charset=UTF-8\r\n\r\n"; my $q = new CGI; my $file = $q->param('file'); open (IN, $file) or die $!; # 打开文件 print <IN>; # 显示文件的全部内容 close IN; # 关闭文件
如果将查询字符串中的
file
指定如下,/sbin 目录下的文件一览就会被显示在页面上 61 。file=ls+/sbin|
执行结果如下图所示。
图 4-101 显示了 /sbin 目录的文件一览
- 安全隐患的产生原因总结
Web 应用的开发语言中,有些函数的内部实现利用了 Shell。如果开发者使用了这些内部调用 Shell 的函数,就可能会使得意料之外的 OS 命令被执行。这种状态被称为 OS 命令注入漏洞。
OS 命令注入漏洞的形成需要同时满足以下三项条件。
- 使用了内部调用 Shell 的函数(
system
、open
等) - 将外界传入的参数传递给内部调用 Shell 的函数
- 参数中 Shell 的元字符没有被转义
- 使用了内部调用 Shell 的函数(
60 详情请参考以下链接: http://technet.microsoft.com/zh-cn/library/cc737438(v=ws.10).aspx 。
61 该脚本同时也存在目录遍历漏洞。详情请参考 4.10 节。
对策
为了防范 OS 命令注入漏洞,推荐大家使用下列方法中的任意一项,这里我们将以下四种方法按照推荐度由高到低进行了排序。
- 选择不调用 OS 命令的实现方法
- 避免使用内部调用 Shell 的函数
- 不将外界输入的字符串传递给命令行参数
- 使用安全的函数对传递给 OS 命令的参数进行转义
- 在设计阶段决定对策方针
具体选择哪一项对策方法,应当在设计阶段就确定下来。为此,建议在各个设计阶段分别探讨以下内容。
基本设计阶段
围绕代码实现方式的设计进行以下讨论。
- 决定主要功能的代码实现方针
- 尽量利用专门的程序库,迫不得已时再使用 OS 命令来实现
详细设计阶段
- 设计各功能的详细的实现方式时,极力避免使用内部调用 Shell 的函数
- 只能使用内部调用 Shell 的函数时,讨论决定是将参数固定,还是由标准输入来指定参数
下面我们就来分别看一下各个方法的详情。
- 选择不调用 OS 命令的实现方法
推荐度最高的方法为不调用 OS 命令,即不利用调用 Shell 的功能。这样一来,既杜绝了 OS 命令注入漏洞混入的可能性,又消除了调用 OS 命令的系统开销,能够从多方面提高应用的性能。
下面是利用 PHP 程序库重写之前的发送邮件脚本(/4b/4b-002.php)的例子。PHP 中发送邮件时可以利用
mb_send_mail
函数。代码清单 /4b/4b-002a.php
<?php $mail = $_POST['mail']; mb_language('Japanese'); mb_send_mail($mail, " 已受理 ", " 提问已受理 ", "From: webmaster@example.jp"); ?> <body> 提问已受理 </body>
然而,发送邮件的功能中可能会引入邮件头注入漏洞,详情请参考 4.9 节。后面的一个示例脚本也有同样问题。
- 避免使用内部调用 Shell 的函数
在不调用 OS 命令就无法实现所需功能的情况下,调用 OS 命令时最好使用不经过 Shell 的函数。由于 PHP 中没有合适的函数 62 ,因此这里以 Perl 为例进行讲解。而如果只是想了解 PHP 中的对策方法,则可以跳过本小节而直接阅读下一小节。
Perl 中也存在名为
system
的函数来启动 OS 命令。Perl 的system
函数有两种指定命令和参数的方法,即既可以在一个参数中将它们用空格相隔,也可以将它们分别指定为函数的不同参数。下面为 Perl 脚本中启动 grep 命令的示例。首先是经过 Shell 的调用方法。此调用方法存在 OS 命令注入漏洞。
my $rtn = system("/bin/grep $keyword /var/data/*.txt");
接下来是不经过 Shell 的调用方法。
my $rtn = system('/bin/grep', '--', $keyword, glob('/var/data/*.txt'));
像上面这样分别指定命令名和参数时,由于不经过 Shell,因此 Shell 的元字符(
;
、|
、`
等)就会作为命令的参数被直接传递。也就是说,理论上不会产生 OS 命令注入漏洞。system
函数的第 2 个参数中指定的 '--',表示选项(Option)的指定已经结束,后面指定的都是选项以外的参数(Parameter)。如果不这样做,外界就可以通过 -R 等第一个字符为 - 的关键字来任意指定选项。另外,
system
函数的第 4 个参数中用到了glob
函数,它能通过展开通配符(*.txt)来取得所有匹配的文件名(与 PHP 的glob
函数相同)。经过 Shell 调用命令时,Shell 会展开通配符,而不经过 Shell 时就需要像本例一样自己手动展开通配符。在使用之前提到的 Perl 的
open
函数时,可以采用以下任一方法来避免启动 Shell。- 使用
sysopen
函数来代替open
函数 - 在
open
函数的第 2 个参数中指定访问模式(如下)open(FL, '<', $file) or die ' 错误消息 'txt'));
第 2 个参数中能够指定的访问模式如下。
表 4-18 open 语句的模式指定
模式 说明 `<` 只读模式 `>` 读写模式(覆盖) `>>` 读写模式(追加) `|-` 打开程序管道 `-|` 从程序或命令的输出中取得数据 比如,下面的例子中指定了 |- 模式。这是 Perl5.8 以后的版本支持的写法。此调用方法不经过 Shell,因此理论上不会产生 OS 命令注入漏洞。
代码清单 /4b/4b-002b.cgi
#!/usr/bin/perl use strict; use CGI; use utf8; use Encode; my $q = new CGI; my $mail = $q->param('mail'); # 在不经过 Shell 的情况下将 sendmail 命令作为管道打开 open (my $pipe, '|-', '/usr/sbin/sendmail', $mail) or die $!; # 传入邮件内容 print $pipe encode('UTF-8', <<EndOfMail); To: $mail From: webmaster\@example.jp Subject: =?UTF-8?B?5M+X44GR5LuM44GR44G+44GX44Gf?= Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: 8bit 提问已受理 EndOfMail close $pipe; # 下面为页面显示 print encode('UTF-8', <<EndOfHTML); Content-Type: text/html; charset=UTF-8 <body> 提问已受理 </body> EndOfHTML
需要注意的一点为,与
system
函数同样,这里也应该使用多个参数的形式来指定命令与其参数。因为如果使用/usr/sbin/sendmail $mail
这种利用空格来区分命令和参数的形式,调用时就会经过 Shell,从而也就会引入 OS 命令注入漏洞。 - 使用
- 不将外界输入的字符串传递给命令行参数
只能经过 Shell 调用 OS 命令的函数时,或者不清楚函数的内部实现是否经过 Shell 时,防范 OS 命令注入漏洞的根本性策略就是不将参数传递给命令行。
下面就让我们结合具体例子来看。sendmail 命令指定了 -t 选项后,收件人邮箱地址就不再在命令行中指定,而是变为从邮件的各个消息头 To、Cc、Bcc 中读取。采用这个方法,就可以不用将外界输入的字符串指定给命令行,从而也就消除了 OS 命令注入漏洞。
示例脚本如下。
代码清单 /4b/4b-002c.php
<?php $mail = $_POST['mail']; $h = popen('/usr/sbin/sendmail -t -i', 'w'); if ($h === FALSE) { die(' 现在服务器繁忙,请稍后再试 ..'); } fwrite($h, <<<EndOfMail To: $mail From: webmaster@example.jp Subject: =?UTF-8?B?5M+X44GR5LuM44GR44G+44GX44Gf?= Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: 8bit 提问已受理 EndOfMail ); pclose($h); ?> <body> 提问已受理 </body>
这段脚本中通过指定 sendmail 的 -t 选项,使得收件人信息变为从 To 消息头中读取。然后又使用了 PHP 的
popen
和fwrite
函数将邮件内容传给 sendmail 命令。然而,虽然该脚本中消除了 OS 命令注入漏洞,但还是存在邮件头注入漏洞。解决方法请参考 4.9 节。
- 使用安全的函数对传递给 OS 命令的参数进行转义
如果使用以上 3 个方法都无法消除 OS 命令注入漏洞,就只能经过 Shell 来调用 OS 命令,这时就需要对传给 OS 命令的参数进行转义。然而 Shell 的转义规则颇为复杂,所以不应该自己去手动实现,而是要使用专门用来安全转义的程序库函数。PHP 中相应的函数为
escapeshellarg
。使用
escapeshellarg
对 4b-002.php 进行操作后,调用system
函数的部分就被修改如下。代码清单 /4b/4b-002d.php
system('/usr/sbin/sendmail <template.txt ' . escapeshellarg($mail));
PHP 中还有与
escapeshellarg
类似的escapeshellcmd
函数,但是由于使用方法不当时可能会产生安全隐患,因此不推荐使用。详情请参考笔者的博客 [2]。另外,由于 Shell 转义规则的复杂性以及其他一些环境相关的原因,有时即使使用了
escapeshellarg
也可能无法完全杜绝安全隐患。因此,建议大家配合使用下面介绍的校验参数等辅助性对策。 - OS 命令注入攻击的辅助性对策
上面介绍了防范 OS 命令注入漏洞的根本性对策,但由于对策的执行过程中稍有疏漏就会造成极大影响,因此,为了减少攻击造成的损害,建议配合实施以下辅助性对策。
- 校验参数
- 将运行应用的权限设为所需的最低权限
- 给 Web 服务器上的 OS 或中间件更新安全补丁
下面就让我们来依次看一下以上各项。
- 校验参数
4.2 节中讲过,外界的输入值应当以应用的需求为基准进行校验,而输入值校验有时也具有防范 OS 命令注入的效果。特别是在经过 Shell 调用 OS 命令的情况下,最好对参数字符串的字符种类加以限制。
例如,将文件名传给 OS 命令的参数时,如果应用需求中将文件名限定为仅包含字母或数字,那么即使应用中忘了进行转义处理,OS 命令注入攻击也无法得逞。
- 将运行应用的权限设为所需的最低权限
遭到 OS 命令注入攻击后,由于命令执行权限即为 Web 应用所持有的权限,因此将 Web 应用的权限设为所需的最低权限,就能够将攻击造成的损害程度控制到最低。
将用户权限设为所需的最低权限,对防范目录遍历漏洞也同样有效。
- 给 Web 服务器上的 OS 或中间件更新安全补丁
服务器在内部受到针对操作系统漏洞的攻击(Local Exploit)时,OS 命令注入攻击造成的危害程度最大。通常情况下,攻击造成的损害受限于操作 Web 服务器的用户权限,而内部攻击的情况下,一旦攻击者获取到了 root 权限,就能够对服务器为所欲为。
因此,即使是不会受到外部攻击的安全隐患,也最好能够为系统更新安全补丁等。详情请参考 7.1 节。
62 严格来说有 pcntl_exec 函数,但该函数只能用于 CGI 版的 PHP 中。 http://php.net/manual/zh/pcntl.installation.php 。
参考:内部调用 Shell 的函数
作为参考,下面对各个编程语言中内部调用 Shell 的函数进行了归纳。在开发过程中,建议不要使用下面列出的这些函数,而如果不得不使用的话,则应该选择不经过 Shell 的调用方式。
PHP
`system()` | `exec()` | `passthru()` | `proc_open()` | `popen()` | `shell_exec()` | \`...\` |
Perl
`exec()` | `system()` | \`...\` | `qx/.../` | `open()` |
Ruby
`exec()` | `system()` | \`...\` |
注:Ruby 中也能够像 Perl 一样使用管道符号启动 Shell。例如使用 File.open() 来代替 open(),就不用担心调用 Shell 的问题了。
参考文献
[1] 佐名木智貴 .(2008)《.セキュア Web プログラミング Tips 集》(《Web 编程安全性技巧》). ソフト · リサーチ · センター .
[2] 德丸浩 .(2011 年 1 月 1 日). PHP の escapeshellcmd の危険性(PHP 的 escapeshellcmd 的危险性). 参考日期:2011 年 1 月 1 日,参考网址:德丸浩の日記 : http://www.tokumaru.org/d/20110101.html#p01
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论