内容纲要

前言

前两天看完了 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 解析漏洞 .

来看下该漏洞产生的原因是什么~ 可以使用 VulhubDocker 环境做测试

漏洞现象

实验现象为 : 当访问 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_infopath info 信息去除后 , 用try_files 判断文件是否存在 .

    在 Nginx 的 fastcgi-php.conf 配置文件中可以看到相关配置

  • 借助 PHP-FPMsecurity.limit_extensions 配置项 , 避免其他后缀文件被解析 .

    PHP-FPMwww.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 ! 下面来分析一下~


攻击流程分析

  1. 如何执行我们的代码呢 ?

    看起来是非常困难的 . FastCGI 协议只能传输配置信息及需要被执行的文件名( SCRIPT_FILENAME )及客户端传进来的 GET , POST , Cookie 等数据 . 即使我们可以控制 SCRIPT_FILENAME , 让 PHP-FPM 执行任意文件 , 也只能执行目标服务器上的文件 , 并不能执行我们需要执行的文件 .

    但我们可以通过修改配置文件来执行我们指定的代码 . 值得一提的是 , 除了 disable_function 以外的大部分 PHP 配置 , 都可以通过 FastCGI 协议包来更改 . php.ini 中有两个非常有意思的参数 .

    • auto_prepend_file : 告诉 PHP 解释器在执行目标文件前 , 先包含该参数指定的内容

    • auto_append_file : 告诉 PHP 解释器在执行目标文件后 , 再包含该参数指定的内容

    这两个参数你应该非常熟悉 , 文件包含漏洞里经常会用到 .

  2. 那么如何利用这两个参数呢 ?

    可以利用 PHP 伪协议 php://input . php://input 用于访问请求的原始数据的只读流 . 简单的说 , 我们可以把要执行的代码放在请求体中 , 然后通过 php://input 把要执行的代码通过 POST 方法传递进来 . 再配合 auto_prepend_file 或者 auto_append_file 在每个要加载的文件中包含我们的要执行的代码 .

  3. 但是问题也就来了 , 如何设置 auto_prepend_file 或者 auto_append_file 的值呢 ? 除此之外 , 要使用 php://input 伪协议也需要开启 allow_url_include 选项的 , 如何修改这些配置呢 ?

    这时就需要用到 PHP-FPM 中的两个环境变量 , PHP_VALUEPHP_ADMIN_VALUE . PHP_VALUE 可以设置模式为 PHP_INI_USERPHP_INI_ALL . PHP_ADMIN_VALUE 可以设置几乎所有选项( disable_functions除外 , 这个选项是 PHP 加载的时候就确定了 )

    另外根据对 FastCGI Record 的分析 , Type 为 4 的 Record 用于传递环境参数 . 而且具体的结构为键值对形式 . 因此可以直接在报文中添加这两个 PHP-FPM 的环境变量来进行设置 .

  4. 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 , 其修改内容如下

  1. 修改第 157 行代码

    此时 PHP-FPM 的监听端口为 127.0.0.1:9000 , 因此该服务已经无法从公网上直接访问了 , 因此删除 Request 中的相关代码 .

  2. 修改第 185 行代码

    同样的道理 , 我们不再直接发送请求 , 而是将 request 返回 , 之后再调用 , 同时删除相关代码 .

  3. 修改主函数最后 2 行

    调用前面修改过的 request() 函数来获取返回的 TCP 数据流( response ),对该数据流进行 URL 编码然后拼接成 Gopher 协议的格式 , 从而生成攻击代码 .

    完整的脚本可以参考上面的链接 .


靶机配置

一个非常经典的 SSRF Demo

记得安装 php-curl 相关扩展

设置 PHP-FPM 仅能在本地监听 , 并没有暴露在公网

nmap 扫描结果为 closed , 代表没有应用程序( php-fpm )在该端口上监听 .


攻击流程

  1. 直接拿构造好的脚本打 ... 然后拿到攻击脚本

  2. 然后请求 ssrf.php . 这里拿 Burp 代理下 , 放到 Repeater 模块里

  3. 对参数值再次进行 URL 编码

    因为在服务端 NginxPHP-FPM 分别会进行一次 URL 解码 , 所以一共要URL 编码两次 , 第一次编码在输出攻击脚本时 , 第二次编码就在这里 .

    简便方法 : 右键 -> convert selection -> URL -> URL-encode key characters

  4. 执行即可拿到 RCE

    服务器成功执行了我们指定的代码 .


Bypass Disable_functions ?

这个点不准备仔细看了 , Antsword 的插件市场最近新增了一个 Bypass Disable_function 的插件

按照前面的知识 , PHP_ADMIN_VALUE 是不可以修改 disable_functions 的 , 但是该插件的利用原理好像就是与 PHP-FPM 通信 ... 可能是利用其他方法吧 , 这个以后慢慢学习 . 不过网上已经有大佬给出了分析文章 , 贴在下面

参考链接 : 从蚁剑插件看利用PHP-FPM绕过disable_function


总结

关于 PHP-FPM 的内容就准备先到这 , 这几天简单的学习了 PHP-FPM 的基础知识及利用方式 , 更深层次的东西以后再探讨~

最后修改日期:2020年3月9日

作者

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。