php 安全基础 第六章 文件与命令

  本章主要讨论伴随着文件与shell命令的使用所产生的风险。PHP有大量的文件系统函数,与直接执行shell命令只有少量的区别。在本章中,我会着重强调开发者在使用这些功能时常犯的错误。

  总的来说,伴随这些功能所产生的风险类似于很多本书已提及的风险——使用被污染数据具有灾难性的副作用。尽管漏洞是不同的,但是用来对付它们的方法都是你已学过的方法。


6.1. 文件系统跨越

  无论你用什么方法使用文件,你都要在某个地方指定文件名。在很多情况下,文件名会作为fopen()函数的一个参数,同时其它函数会调用它返回的句柄:

  <?php

 

  $handle = fopen('/path/to/myfile.txt', 'r');

 

  ?>

 

  当你把被污染数据作为文件名的一部分时,漏洞就产生了:

 

  <?php

 

  $handle = fopen("/path/to/{$_GET['filename']}.txt", 'r');

 

  ?>

 

  由于在本例中路径和文件名的前后两部分无法由攻击者所操纵,攻击的可能性受到了限制。可是,需要紧记的是有些攻击会使用NULL(在URL中表示为%00)来使字符串终止,这样就能绕过任何文件扩展名的限制。在这种情况下,最危险的攻击手段是通过使用多个../来方问上级目录,以达到文件系统跨越的目的。例如,想像一下filename的值被指定如下:

 

  http://example.org/file.php?file ... nother/path/to/file

 

  与许多攻击的情况相同,在构造一个字串时如果使用了被污染数据,就会给攻击者以机会来更改这个字串,这样就会造成你的应用以你不希望方式运行。如果你养成了只使用已过滤数据来建立动态字串的习惯,就可以防止很多类型包括很多你所不熟悉的漏洞的出现。

  由于fopen()所调用的文件名前导的静态部分是/path/to,所以上面的攻击中向上跨越目录的次数比所需的更多。因为攻击者在发起攻击前无法察看源码,所以典型的策略是过多次地重复../字串。../字串使用太多次并不会破坏上面的攻击效果,所以攻击者没有必要猜测目录的深度。

 

  在上面的攻击中使fopen()调用以你不希望方式运行,它简化后等价于:

  <?php

 

  $handle = fopen('/another/path/to/file.txt', 'r');

 

  ?>

 

  在意识到这个问题或遭遇攻击后,很多开发者都会犯试图纠正潜在的恶意数据的错误,有时根本不会先对数据进行检查。正如第一章所述,最好的方法把过滤看成检查过程,同时迫使使用者遵从你制定的规则。例如,如果合法的文件名只包含字母,下面的代码能加强这个限制:

 

  <?php

 

  $clean = array();

 

  if (ctype_alpha($_GET['filename']))

  {

    $clean['filename'] = $_GET['filename'];

  }

  else

  {

    /* ... */

  }

 

  $handle = fopen("/path/to/{$clean['filename']}.txt", 'r');

 

  ?>

 

  并没有必要对filename值进行转义,这是因为这些数据中只用在PHP函数中而不会传送到远程系统。

 

basename( )函数在检查是否有不必要的路径时非常有用:

  <?php

 

  $clean = array();

 

  if (basename($_GET['filename']) == $_GET['filename'])

  {

    $clean['filename'] = $_GET['filename'];

  }

  else

  {

    /* ... */

  }

 

  $handle = fopen("/path/to/{$clean['filename']}.txt", 'r');

 

  ?>

 

  这个流程比只允许文件名是字母的安全性要差了一些,但你不太可能要求那样严格。比较好的深度防范流程是综合上面的两种方法,特别是你在用正则表达式检查代码合法性时(而不是用函数ctype_alpha( ))。

  当文件名的整个尾部是由未过滤数据组成时,一个高危漏洞就产生了:

  <?php

 

  $handle = fopen("/path/to/{$_GET['filename']}", 'r');

 

  ?>

 

  给予攻击者更多的灵活性意味着更多的漏洞。在这个例子中,攻击者能操纵filename参数指向文件系统中的任何文件,而不管路径和文件扩展名是什么,这是因为文件扩展名是$_GET['filename']的一部分。一旦WEB服务器具有能读取该文件的权限,处理就会转向这个攻击者所指定的文件。

  如果路径的前导部分使用了被污染数据的话,这一类的漏洞会变得甚至更加庞大。这也是下一节的主题。

 

6.2. 远程文件风险

  PHP有一个配置选项叫allow_url_fopen,该选项默认是有效的。它允许你指向许多类型的资源,并像本地文件一样处理。例如,通过读取URL你可以取得某一个页面的内容(HTML):

  <?php

 

  $contents = file_get_contents('http://example.org/');

 

  ?>

 

  正如第五章所讨论的那样,当被污染数据用于include和require的文件指向时,会产生严重漏洞。实际上,我认为这种漏洞是PHP应用中最危险的漏洞之一,这是因为它允许攻击者执行任意代码。

  尽管严重性在级别上要差一点,但在一个标准文件系统函数中使用了被污染数据的话,会有类似的漏洞产生:

 

  <?php

 

  $contents = file_get_contents($_GET['filename']);

 

  ?>

 

  该例使用户能操纵file_get_contents( )的行为,以使它获取远程资源的内容。考虑一下类似下面的请求:

  http://example.org/file.php?file ... mple.org%2Fxss.html

  这就导致了$content的值被污染的情形,由于这个值是通过间接方式得到的,因此很可能会忽视这个事实。这也是深度防范原则会视文件系统为远程的数据源,同时会视$content的值为输入,这样你的过滤机制会潜在的起到扭转乾坤的作用。

  由于$content值是被污染 的,它可能导致多种安全漏洞,包括跨站脚本漏洞和SQL注入漏洞。例如,下面是跨站脚本漏洞的示例:

  <?php

 

  $contents = file_get_contents($_GET['filename']);

 

  echo $contents;

 

  ?>

 

  解决方案是永远不要用被污染的数据去指向一个文件名。要坚持过滤输入,同时确信在数据指向一个文件名之前被过滤即可:

 

  <?php

 

  $clean = array();

 

  /* Filter Input ($_GET['filename']) */

 

  $contents = file_get_contents($clean['filename']);

 

  ?>

 

  尽管无法保证$content中的数据完全没有问题,但这还是给出了一个合理的保证,即你读取的文件正是你想要读取的文件,而不是由攻击者指定的。为加强这个流程的安全性,你同样需要把$content看成是输入,并在使用前对它进行过滤。

 

  <?php

 

  $clean = array();

  $html = array();

 

  /* Filter Input ($_GET['filename']) */

 

  $contents = file_get_contents($clean['filename']);

 

  /* Filter Input ($contents) */

 

  $html['contents'] = htmlentities($clean['contents'], ENT_QUOTES, 'UTF-8');

 

  echo $html['contents'];

 

  ?>

 

  上面的流程提供了防范多种攻击的强有力的方法,同时在实际编程中推荐使用。

 

 

6.3. 命令注入

  使用系统命令是一项危险的操作,尤其在你试图使用远程数据来构造要执行的命令时更是如此。如果使用了被污染数据,命令注入漏洞就产生了。

  Exec()是用于执行shell命令的函数。它返回执行并返回命令输出的最后一行,但你可以指定一个数组作为第二个参数,这样输出的每一行都会作为一个元素存入数组。使用方式如下:

 

  <?php

 

  $last = exec('ls', $output, $return);

 

  print_r($output);

  echo "Return [$return]";

 

  ?>

 

  假设ls命令在shell中手工运行时会产生如下输出:

 

  $ ls

  total 0

  -rw-rw-r--  1 chris chris 0 May 21 12:34 php-security

  -rw-rw-r--  1 chris chris 0 May 21 12:34 chris-shiflett

 

  当通过上例的方法在exec()中运行时,输出结果如下:

  Array

  (

      [0] => total 0

      [1] => -rw-rw-r--  1 chris chris 0 May 21 12:34 php-security

      [2] => -rw-rw-r--  1 chris chris 0 May 21 12:34 chris-shiflett

  )

  Return [0]

 

  这种运行shell命令的方法方便而有用,但这种方便为你带来了重大的风险。如果使用了被污染数据构造命令串的话,攻击者就能执行任意的命令。

  我建议你有可能的话,要避免使用shell命令,如果实在要用的话,就要确保对构造命令串的数据进行过滤,同时必须要对输出进行转义:

 

  <?php

 

  $clean = array();

  $shell = array();

 

  /* Filter Input ($command, $argument) */

 

  $shell['command'] = escapeshellcmd($clean['command']);

  $shell['argument'] = escapeshellarg($clean['argument']);

 

  $last = exec("{$shell['command']} {$shell['argument']}", $output, $return);

 

  ?>

 

  尽管有多种方法可以执行shell命令,但必须要坚持一点,在构造被运行的字符串时只允许使用已过滤和转义数据。其他需要注意的同类函数有passthru( ), popen( ), shell_exec( ),以及system( )。我再次重申,如果有可能的话,建议避免所有shell命令的使用。

posted @ 2008-04-02 09:14  曹振华  阅读(395)  评论(0编辑  收藏  举报