前言
前两天看完了 PHP-FPM
的基础内容 , 感觉原理上并不复杂 , 今天来学习一下相关的安全问题 . 将过程记录在下面 .
如果您对 PHP-FPM
不太了解 , 可以参考这篇文章 : PHP-FPM 学习笔记
在该文章的末尾我提到 : Type = 4 的 FastCGI Record 是非常重要的 , 因为它与 PHP 环境变量息息相关 .
PHP-FPM
按照 FastCGI
协议将传输的 TCP
数据流解析成真正的数据 , 然后转交给进程池中的子进程中的PHP解释器处理 .
举个例子 , 当用户访问 http://127.0.0.1/index.php?a=1&b=2
这个链接 , 假设此时 Web 根目录为 : /var/www/html
, WebServer 为 Nginx , 端口为 " 80 " . 则 PHP-FPM
接收到数据包时会将这个 TCP 请求解析为如下的键值对数组 .
这个数组其实就是PHP中的 $_SERVER
数组的一部分 , 同时也是PHP里的环境变量 . 但环境变量的作用不仅是填充 $_SERVER
数组 , 也是告诉 PHP-FPM
用户要执行哪个PHP文件 .
PHP-FPM
会将 SCRIPT_FILENAME
指向这个PHP文件,也就是 /var/www/html/index.php
. 然后分配给进程池中的 PHP 解析器处理 .
这其中就会产生一个经典的安全问题~
Nginx / IIS 解析漏洞
如果你有 Web 安全相关的书籍( 比如 <<白帽子讲 Web 安全>> ) , 你会发现书中在讲解文件上传漏洞时会着重提到 Web 中间件解析漏洞 . 而且一般来说会强调 Apache 解析漏洞( a.php.aa )
, IIS6.0 解析漏洞( 1.asp;2.jpg )
, PHP-CGI 解析漏洞( a.abc/invalidfile.php )
这三个解析漏洞 .
其中 PHP-CGI 解析漏洞
就是与 PHP-FPM
相关的安全问题漏洞 , 又因为 Nginx/IIS 往往是以 PHP-FPM 模式来安装 PHP , 因此该漏洞也被称为 Nginx/IIS 解析漏洞 .
来看下该漏洞产生的原因是什么~ 可以使用 Vulhub 的 Docker 环境做测试
漏洞现象
实验现象为 : 当访问 http://127.0.0.1/uploadfiles/nginx.png
时会输出正常的图片 .
当访问 http://127.0.0.1/uploadfiles/nginx.png/a.php
时 , 该 PNG 图片会被解析为 PHP 文件 , 执行了 phpinfo()
函数
查看 nginx.png 图片 , 会发现文件中包含一段 PHP 代码 , 很明显刚才这段代码被执行了
漏洞成因
该漏洞与 Nginx , PHP 版本无关 , 属于用户配置不当而造成的解析漏洞 .
当用户访问 http://127.0.0.1/uploadfiles/nginx.png/a.php
时 , PHP-FPM
会从收到的 TCP 数据流中解析出如下一段内容
这里 SCRIPT_FILENAME
很明显是一个不存在的文件 , 按理说 PHP-FPM 找不到目标文件 , 然后 WebServer 将会返回 HTTP 404 错误 , 最后结束此次请求 . 整个过程没有任何问题 .
但是如果若管理员开启了 cgi.fix_pathinfo
选项 , 则会产生解析漏洞 . PHP为了支持 Path Info 模式而创造了 cgi.fix_pathinfo
选项 , 这个选项被打开的情况下 , PHP-FPM
会判断 SCRIPT_FILENAME
是否存在 , 如果不存在则去掉最后一个 " / " 及以后的所有内容 , 再次判断文件是否存在 . 往次循环 , 直到文件存在 , 然后去解析这个文件 .
在 phpinfo()
中可以看出管理员开启了 cgi.fix_pathinfo
, 从而产生了解析漏洞 . 开始时 PHP-FPM
发现 http://127.0.0.1/uploadfiles/nginx.png/a.php
文件不存在 , 于是去掉了 /a.php
, 然后继续解析 http://127.0.0.1/uploadfiles/nginx.png
. 当然这个文件是存在的 , 因此 PHP-FPM
把这个文件当作 PHP 文件执行 .
那如果想使用 Path Info
功能 ,又想服务器比较安全 , 可以怎么做呢 ? 有两种比较好的方法 .
-
在 Nginx 端使用
fastcgi_split_path_info
将path info
信息去除后 , 用try_files
判断文件是否存在 .在 Nginx 的
fastcgi-php.conf
配置文件中可以看到相关配置 -
借助
PHP-FPM
的security.limit_extensions
配置项 , 避免其他后缀文件被解析 .在
PHP-FPM
的www.conf
配置文件中可以看到相关配置
PHP-FPM 未授权访问漏洞
那么什么是 PHP-FPM
未授权访问漏洞呢 ? WebServer 与 PHP-FPM
通过 FastCGI 协议通信 , 但是在整个学习过程中 , 我都没有发现在通信过程中有与认证和授权相关的配置 .
前面提到过 , PHP-FPM
的 TCP 通信模式允许通过远程网络进程之间的通信 , 也可以通过 LoopBack 接口进行本地进程之间的通信 . 如果管理员配置不当 , 使得 PHP-FPM
的监听端口暴露在公网( 即将监听端口从 127.0.0.1:9000
修改为 0.0.0.0:9000
) , 那么是不是所有人都可以与 PHP-FPM
通信呢?
在没有其他原因( 例如防火墙 )的情况下 , 是这样的 ! 我们可以伪造 FastCGI 协议数据包 , 与服务端的 PHP-FPM 通信 .
利用伪造的 FastCGI 协议数据包 , 我们甚至可以拿到 RCE ! 下面来分析一下~
攻击流程分析
-
如何执行我们的代码呢 ?
看起来是非常困难的 . FastCGI 协议只能传输配置信息及需要被执行的文件名(
SCRIPT_FILENAME
)及客户端传进来的GET
,POST
,Cookie
等数据 . 即使我们可以控制SCRIPT_FILENAME
, 让PHP-FPM
执行任意文件 , 也只能执行目标服务器上的文件 , 并不能执行我们需要执行的文件 .但我们可以通过修改配置文件来执行我们指定的代码 . 值得一提的是 , 除了
disable_function
以外的大部分 PHP 配置 , 都可以通过 FastCGI 协议包来更改 .php.ini
中有两个非常有意思的参数 .-
auto_prepend_file
: 告诉 PHP 解释器在执行目标文件前 , 先包含该参数指定的内容 -
auto_append_file
: 告诉 PHP 解释器在执行目标文件后 , 再包含该参数指定的内容
这两个参数你应该非常熟悉 , 文件包含漏洞里经常会用到 .
-
-
那么如何利用这两个参数呢 ?
可以利用 PHP 伪协议
php://input
.php://input
用于访问请求的原始数据的只读流 . 简单的说 , 我们可以把要执行的代码放在请求体中 , 然后通过php://input
把要执行的代码通过 POST 方法传递进来 . 再配合auto_prepend_file
或者auto_append_file
在每个要加载的文件中包含我们的要执行的代码 . -
但是问题也就来了 , 如何设置
auto_prepend_file
或者auto_append_file
的值呢 ? 除此之外 , 要使用php://input
伪协议也需要开启allow_url_include
选项的 , 如何修改这些配置呢 ?这时就需要用到
PHP-FPM
中的两个环境变量 ,PHP_VALUE
和PHP_ADMIN_VALUE
.PHP_VALUE
可以设置模式为PHP_INI_USER
和PHP_INI_ALL
.PHP_ADMIN_VALUE
可以设置几乎所有选项(disable_functions
除外 , 这个选项是 PHP 加载的时候就确定了 )另外根据对 FastCGI Record 的分析 , Type 为 4 的 Record 用于传递环境参数 . 而且具体的结构为键值对形式 . 因此可以直接在报文中添加这两个
PHP-FPM
的环境变量来进行设置 . -
SCRIPT_FILENAME
回头再看 FastCGI Record 所需的字段 , 发现 Record 的
SCRIPT_FILENAME
选项需要我们设置一个服务端已存在的PHP文件 , 该选项是让PHP-FPM
执行的目标服务器上的文件 . 并且如果服务器端设置了security.limit_extensions
参数 , 则可能无法利用解析漏洞 , 只能找到一个服务器上已经存在的 PHP 文件 .如果能直接访问到目标主机的某个 PHP 页面 , 那么就可以使用该页面 . 如果目标主机仅开启了
PHP-FPM
却没有开启WebServer
, PHP 安装时也会默认添加几个 PHP 文件 , 可以利用这些文件 , 比如/usr/local/lib/php/PEAR.php
.
攻击环境实验
这里采用 vulhub 的环境 , 端口默认为 9000 端口
攻击脚本为 Phith0n 师傅的 fpm.py
目标文件选择 /usr/local/lib/php/PEAR.php
然后拿脚本直接打 ...
可以看到成功拿到了 RCE ~
如果要拿虚拟机实验 , 别忘了将监听端口修改为 0.0.0.0 9000
看了下使用的攻击脚本 , 其实大部分内容都在构造 FastCGI 客户端 , 这个也有前辈造好的轮子 ( Python-FastCGI-Client ) . 重点内容放在主函数里
实施攻击时 , FastCGI 客户端会向暴露于公网的 PHP-FPM
服务端口发送拼接构造好的 FastCGI
报文 , 以实现攻击利用 .
PHP-FPM 绕过 Open_basedir
现在看起来就非常简单了 . PHP_ADMIN_VALUE
可以修改除了 disable_functions
之外的所有 PHP 环境变量 , 自然也就包括可以修改 Open_basedir
的参数值 .
攻击流程
先拿本地的 Ubuntu 作为靶机 . 看一下原本是否可以利用成功 , 以读取 /etc/passwd
为例 .
测试完毕后 , 开启 open_basedir
相关配置 , 并且重启服务 .
然后运行 fpm.py
发现由于 open_basedir
的影响 , 无法读取 /etc/passwd
了
修改 fpm.py
脚本 , 添加更改 open_basedir
的相关配置
这里 open_basedir
的配置必须放在 auto_prepend_file = php://input
后才能生效 .
再次运行攻击脚本 , 发现成功绕过了 open_basedir
的限制 , 读取到了 /etc/passwd
文件 .
总的来看 , 其实就是利用 PHP-FPM
可以修改大部分 PHP
环境变量的特性修改了服务器端 Open_basedir
的相关配置 , 从而 " 绕过 " 了 Open_basedir
.
SSRF 攻击内网 PHP-FPM
学习过程中发现 SSRF 还可以打 PHP-FPM , 参考 大佬的文章 学习一波~
因为一般情况下 , PHP-FPM
的端口是不会暴露在公网的 . 也就是说 , 很少有管理员会把 PHP-FPM
的端口改为 0.0.0.0 9000
.
因此我们没有办法直接攻击 PHP-FPM
, 但是如果目标站点还存在其他漏洞( 比如 SSRF ) , 就可以配合 PHP-FPM
进行攻击 , 从而拿到 RCE .
使用的攻击协议是 Gopher
协议 , 该协议在 SSRF 中被广泛利用 , 因此这里不一一举例了 . 总之 , 该协议是一个很古老的协议( 在 HTTP 出现前被使用 ) , 常被用来构造 TCP/IP 数据包来攻击内网应用 .
Gopher
协议的基本格式是这样的 .
gopher://<host>:<port>/<gopher-path>_TCP数据流
可以利用该协议构造 SSRF 攻击代码 , 这里的攻击代码是指 FastCGI Record
报文 .
攻击代码的构造
可以通过修改Phith0n 师傅的 fpm.py
来构造新的 Exp , 其修改内容如下
-
修改第 157 行代码
此时
PHP-FPM
的监听端口为127.0.0.1:9000
, 因此该服务已经无法从公网上直接访问了 , 因此删除Request
中的相关代码 . -
修改第 185 行代码
同样的道理 , 我们不再直接发送请求 , 而是将
request
返回 , 之后再调用 , 同时删除相关代码 . -
修改主函数最后 2 行
调用前面修改过的
request()
函数来获取返回的 TCP 数据流( response ),对该数据流进行 URL 编码然后拼接成Gopher
协议的格式 , 从而生成攻击代码 .完整的脚本可以参考上面的链接 .
靶机配置
一个非常经典的 SSRF Demo
记得安装 php-curl
相关扩展
设置 PHP-FPM
仅能在本地监听 , 并没有暴露在公网
nmap 扫描结果为 closed
, 代表没有应用程序( php-fpm )在该端口上监听 .
攻击流程
-
直接拿构造好的脚本打 ... 然后拿到攻击脚本
-
然后请求
ssrf.php
. 这里拿 Burp 代理下 , 放到 Repeater 模块里 -
对参数值再次进行 URL 编码
因为在服务端
Nginx
和PHP-FPM
分别会进行一次 URL 解码 , 所以一共要URL 编码两次 , 第一次编码在输出攻击脚本时 , 第二次编码就在这里 .简便方法 : 右键 ->
convert selection
->URL
->URL-encode key characters
-
执行即可拿到 RCE
服务器成功执行了我们指定的代码 .
Bypass Disable_functions ?
这个点不准备仔细看了 , Antsword 的插件市场最近新增了一个 Bypass Disable_function 的插件
按照前面的知识 , PHP_ADMIN_VALUE
是不可以修改 disable_functions
的 , 但是该插件的利用原理好像就是与 PHP-FPM
通信 ... 可能是利用其他方法吧 , 这个以后慢慢学习 . 不过网上已经有大佬给出了分析文章 , 贴在下面
参考链接 : 从蚁剑插件看利用PHP-FPM绕过disable_function
总结
关于 PHP-FPM
的内容就准备先到这 , 这几天简单的学习了 PHP-FPM
的基础知识及利用方式 , 更深层次的东西以后再探讨~