CVE-2024-4577漏洞描述
这个漏洞是Orange大佬的团队发现的,公告:https://devco.re/blog/2024/06/06/security-alert-cve-2024-4577-php-cgi-argument-injection-vulnerability-en/
漏洞描述如下
In PHP versions 8.1.* before 8.1.29, 8.2.* before 8.2.20, 8.3.* before 8.3.8, when using Apache and PHP-CGI on Windows, if the system is set up to use certain code pages, Windows may use “Best-Fit” behavior to replace characters in command line given to Win32 API functions. PHP CGI module may misinterpret those characters as PHP options, which may allow unauthenticated attackers to bypass the previous protection of CVE-2012-1823 by specific character sequences. Arbitrary code can be executed on remote PHP servers through the argument injection attack.
从漏洞描述中看大概的问题是,Windows操作系统中编码转换的Best-Fit特性会替换命令行中的一些字符,而PHP CGI模块会将这些替换的字符误解为PHP的选项,影响正在运行的PHP二进制文件,从而造成PHP任意代码执行。这一漏洞也是CVE-2012-1823的绕过。那么有三个点需要了解一下(1)PHP CGI是什么 (2)CVE-2012-1823是什么漏洞 (3)Windows的“Best-Fit”特性是什么?为什么能利用它绕过CVE-2012-1823的修复
PHP CGI
CGI(Common Gateway Interface,CGI通用网关接口),是服务器上实现动态网页的通用协议。一般一次请求对应一个CGI脚本的执行,生成一个HTML。CGI是运行脚本的通用接口,独立于编程语言的,即可以用任何语言来编写CGI程序。用PHP编写的就是PHP CGI。
用一个例子来演示如何使用PHP CGI来生成动态网页内容。首先Web服务器(Apache或Nginx)配置需要支持CGI和PHP。以Apache
为例,需要在配置文件中添加如下内容
ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/ # 定义URL路径和文件系统路径之间的映射。用户访问 http://ip/cgi-bin/ 时,Apache 将请求映射到 /usr/lib/cgi-bin/。
<Directory "/usr/lib/cgi-bin"> # 定义对目录/usr/lib/cgi-bin的访问权限和配置
AllowOverride None # 止使用.htaccess文件覆盖此目录中的配置
Options +ExecCGI # 允许在该目录中执行CGI脚本
AddHandler cgi-script .cgi .pl .py .php # 将指定的文件扩展名(.cgi、.pl、.py、.php)作为CGI脚本处理
Require all granted # 允许所有用户访问该目录
</Directory>
然后创建一个PHP脚本并将其放置在CGI目录(如/usr/lib/cgi-bin/
)中。确保文件具有可执行权限。
#!/usr/bin/env php
<?php
header('Content-Type: text/html; charset=UTF-8');
$name = isset($_GET['name']) ? htmlspecialchars($_GET['name']) : 'World';
echo "<html>";
echo "<head>";
echo "<title>PHP CGI Example</title>";
echo "</head>";
echo "<body>";
echo "<h1>Hello, $name!</h1>";
echo "<p>This is a simple PHP CGI script that greets the user.</p>";
echo "</body>";
echo "</html>";
?>
然后就可以在浏览器中访问CGI目录下的脚本
http://localhost/cgi-bin/hello.php
看到这不禁有一个疑问,这和Web场景下的其他PHP文件有什么区别?PHP CGI是早期Web场景下的需求,现在已经比较少用到了。现在多使用PHP模块mod_php或者PHP-FPM来处理PHP文件。另外,常见的PHP文件访问时直接放在网站根目录下,如/var/www/html
,然后通过URL访问。PHP CGI文件则是放到指定的CGI目录,如/usr/lib/cgi-bin
下,通过ScriptAlias指令映射地址进行访问。
CVE-2012-1823
sapi/cgi/cgi_main.c in PHP before 5.3.12 and 5.4.x before 5.4.2, when configured as a CGI script (aka php-cgi), does not properly handle query strings that lack an = (equals sign) character, which allows remote attackers to execute arbitrary code by placing command-line options in the query string, related to lack of skipping a certain php_getopt for the ‘d’ case.
老版本的PHP在配置PHP CGI脚本时,没有正确处理缺少=
(等号)的查询字符串,允许攻击者通过查询字符串中添加命令行选项来执行任意代码。
PHP CGI脚本接受的命令行选项如下:
一些可以利用的选项包括:-s
可以用来显示脚本的源代码,-d
可用于通过修改 INI 条目来更改 PHP 配置。
上面提到PHP CGI脚本的访问方式,也就是说访问http://localhost/cgi-bin/hello.php?-s
就可以直接查看脚本的源码。那么如何构造RCE呢?
RCE需要能发送PHP代码到服务器并执行。想要发送代码,容易想到php://input
。但是它的使用有个前提,配置上要求allow_url_include=1
,但是一般是不开启这个的。所以需要先将-d
将这个配置选项进行修改。php://input
用法如下:
<?php
$inputData = file_get_contents('php://input'); // 读取 POST 请求的原始数据
echo "Received data: " . htmlspecialchars($inputData);
?>
file_get_contents
是PHP 中用于读取文件内容的函数。它可以读取本地文件、URL或者 PHP 的特殊流(如php://input
)的内容,并将其作为字符串返回。但是命令行中是无法调用方法的。
攻击者找到一个可利用的配置—PHP从4.2.3版本开始有个配置选项auto_prepend_file
,描述如下。
“Specifies the name of a file that is automatically parsed before the main file. The file is included as if it was called with the require function, so include_path is used”.
auto_prepend_file
用于指定在每个 PHP 脚本执行之前自动包含(include
)的文件。并且它是全局的,对所有脚本生效。使用时在php.ini
中配置如下,然后xxx.php
中所有定义的常量、函数等会被其他文件自动加载。
auto_prepend_file = "/path/to/xxx.php"
根据这两个命令行选项,构造漏洞POC如下。
echo "<?php system('uname -a');die(); ?>" | POST "http://ip/?-d allow_url_include=1 -d auto_prepend_file=php://input"
调用系统命令uname -a
,并用echo
输出其结果。die()
函数用于确保在执行系统命令后立即终止PHP脚本的执行,以防止后续代码执行。然后用|
管道符将前面echo命令的结果作为输入传递给后面的POST请求。设置allow_url_include
参数为1,允许在include
和require
语句中使用远程文件。设置auto_prepend_file
参数为php://input
,这样着在每个PHP脚本执行前都会自动包含来echo
的内容。Ps:利用时将=
编码成%3d
,空格用+
或%20
代替
官方给的临时修复建议如下
RewriteCond %{QUERY_STRING} ^[^=]*$
RewriteCond %{QUERY_STRING} %2d|\- [NC]
RewriteRule .? - [F,L]
Apache服务器上使用的mod_rewrite模块定义规则,第一行规则从url中获取查询字符串,并用正则匹配不包含=
的情况(如?key1&key2
),第二行规则匹配包含-
符号或其编码形式%2d
的情况。如果前两个情况被命中,就会返回HTTP 403 Forbidden响应,禁止访问
官方在源码中的修复则是加入了是否包含=
的判断,参考:https://github.com/php/php-src/commit/857fc1b473f5d27ed5ea6aa78420498dbb71c6b6
如果请求参数没有=
号,先跳过所有空白符,判断第一个字符是否是-
,是的话就不解析命令行。getopt
方法一般用于获取和处理命令行参数和选项。
Windows "Best-Fit"特性
直接Google搜漏洞描述中的Windows "Best-Fit"没有找到相关的内容,查看Orange团队给的参考链接:https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-ucoderef/d1980631-6401-428e-a49d-d71394be7da8
关于Best-Fit的内容如下
Field 1: The number of records of Unicode to byte mappings. Note that this field is often more than the number of roundtrip mappings that are supported by the codepage due to Windows best-fit behavior.
这里提到了codepage,漏洞描述中也提到了codepage。codepage翻译成中文就是代码页。所谓的代码页是一个字符编码表,将字符集中的字符映射到计算机中使用的数值。Windows系统使用代码页来支持不同语言和区域的字符编码。例如,代码页437是美国的DOS字符集,而代码页936是简体中文(GBK)的字符集。https://learn.microsoft.com/en-us/windows/win32/intl/code-page-identifiers。如果还不理解代码页是什么,可以看一下代码页936(GB2312)的字符集:http://www.staroceans.org/GB2312/GB2312.htm,展现了字符和计算机中的数值是如何映射的。
字符编码表是相对固定的,如果用户输入了代码页不支持的字符,也需要能继续运行,而不能因为无法识别字符而崩溃。所以为了这种兼容性需要引入一种机制,也就是Best-Fit。字符在无法直接映射到代码页中内容的情况下,采用最接近的字符进行替换。
那么具体怎么替换的呢?也同样有一张编码表。还是以代码页936为例,替换表如下:https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit936.txt
左边的数值是源字符的Unicode值,右边的数值是在特定代码页中的编码。右边的符号则表示替换后的字符。也就是Unicode字符0x00ad
因为在代码页936中找不到对应字符,会被best-fit替换为0x002d
(url编码为%2d
),即字符-
结合上面的CVE-2012-1823修复时是判断了-
,不难想到修复的过滤,可能使用best-fit,利用%ad
绕过-
的检测。PHP在进行检查的时候,认为字符是%ad
,而在Windows解析时,将其转换成了-
,从而执行命令行选项。
CVE-2024-4577实际利用
所以理论上将CVE-2012-1823的POC进行修改,将-
替换,为了和-d
的小写d
进行区分,我们将%ad
写为%AD
,对XAMPP进行攻击。
# CVE-2012-1823
POST "http://ip/?-d allow_url_include=1 -d auto_prepend_file=php://input"
# 改进
POST "http://ip/?%ADd allow_url_include=1 %ADd auto_prepend_file=php://input"
先搭建一下环境。下载XMAPP,更改Apache和Mysql配置文件的端口(以免和现有服务冲突),需要注意,然后要在"Config"按钮中再次更改一下端口。访问到如下界面即为成功。
查看XMAPP的PHP CGI配置C:/xampp/apache/conf/extra/httpd-xampp.conf
找到ScriptAlias
相关的内容
ScriptAlias /php-cgi/ "C:/xampp/php/" # 当请求 URL 是 `/php-cgi/xxx` 时,服务器会将其映射到 `C:/xampp/php/xxx`。
<Directory "C:/xampp/php">
AllowOverride None
Options None # 禁用所有的额外选项
Require all denied # 默认拒绝所有对该目录的访问
<Files "php-cgi.exe"> # Files 指令块用于配置对 php-cgi.exe 文件的访问控制
Require all granted # 允许对 php-cgi.exe 文件的所有访问
</Files>
</Directory>
和文章最前面的PHP CGI配置(如下)做对比
ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/ # 定义URL路径和文件系统路径之间的映射。用户访问 http://ip/cgi-bin/ 时,Apache 将请求映射到 /usr/lib/cgi-bin/。
<Directory "/usr/lib/cgi-bin"> # 定义对目录/usr/lib/cgi-bin的访问权限和配置
AllowOverride None # 止使用.htaccess文件覆盖此目录中的配置
Options +ExecCGI # 允许在该目录中执行CGI脚本
AddHandler cgi-script .cgi .pl .py .php # 将指定的文件扩展名(.cgi、.pl、.py、.php)作为CGI脚本处理
Require all granted # 允许所有用户访问该目录
</Directory>
CVE-2012-1823这个配置是在/cgi-bin/
目录下访问某个指定后缀的脚本如hello.php
,即http://localhost/cgi-bin/hello.php?-d
。XMAPP的配置则是访问/php-cgi/
,但是它拒绝了所有对该目录的访问,只放行了php-cgi.exe
。那么尝试对http://localhost//php-cgi/php-cgi.exe
进行访问,会发现响应500。
然后开始搜索php-cgi.exe
相关的内容。它是PHP的CGI可执行文件,在Windows环境下处理PHP脚本,相当于处理器。按照上面XMAPP的配置,理论上PHP脚本应该放在C:/xampp/php
目录中。但是如果放在其他目录,如Web服务器的根目录中,就无法进行处理。那么就需要引入如下的配置语句,通过 Action 和 AddHandler 指定处理器,让所有扩展名为.php
的文件都由php-cgi.exe
来处理。
AddHandler php-script .php
Action php-script /php-cgi/php-cgi.exe
和文章前面给的PHP CGI配置相比,也确实缺少了AddHandler cgi-script .cgi .pl .py .php
这一行。
在XMAPP配置httpd-xampp.conf
中查找Action相应的内容,发现是被注释掉的。
# 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>
被注释掉,意味着其实并没有启用PHP-CGI模式,那XMAPP可能用的是其他的PHP处理方式,如mod_php
或PHP-FPM
。
回到上图的phpinfo.php
界面,找到Server API选项,可以看到Apache 2.0 Handler
(mod_php)。Server API选项代表的是PHP与Web服务器交互的接口类型,即PHP是如何与Web服务器集成和运行的。
PS:CGI模式和Apache 2.0 Handler模式有一些区别。
CGI模式 | Apache 2.0 Handler |
---|---|
PHP是单独的可执行文件 | PHP作为Apache的模块(mod_php) |
每个请求都会启动一个新的php-cgi进程 | PHP作为Apache的模块子进程,加载一次后留在内存中。 |
**那么现在的情况是,XMAPP中设置了php-cgi的目录,并且具备可执行脚本php-cgi.exe
。但是缺少Action指令的配置。而Action指令用于php-cgi.exe
和PHP脚本的映射。
cgi.force_redirect
在翻阅PHP CGI安全的手册过程中,看到一个内容:https://www.php.net/manual/zh/security.cgi-bin.force-redirect.php
内容中提到我们缺少的这部分Action配置(如下),实际是为了Apache 做重定向的设置。PHP只会解析具备重定向规则的url。
Action php-script /cgi-bin/php
AddHandler php-script .php
而我们现在缺少的就是重定向的设置。但是只有在开启cgi.force_redirect
这个选项时,重定向的限制才会生效,PHP默认开启。进一步查看该配置的说明。
配置选项cgi.force_redirect
用于防止用户直接通过URL访问PHP CGI可执行文件。例如,直接访问 http://example.com/cgi-bin/php-cgi.exe。当启用时,PHP CGI可执行文件只能通过 Web 服务器重定向来访问,而不能直接访问。同样,如果用户可以直接通过http://my.host/cgi-bin/php/dir/xx.php
来访问php-cgi目录下的文件,是很危险的,所以设计了cgi.force_redirect
这个选项,只有经过了重定向规则的请求才能执行。
PS:上图中还有个配置cgi.redirect_status_env
,它的声明中提到,在不是Apache或Netscape Web服务器的环境下,cgi.force_redirect
打开想要生效还需要配置一个环境变量。配置方法是在php.ini
中增加如下内容。
cgi.redirect_status_env = "REDIRECT_STATUS"
PHP在开启cgi.force_redirect
选项后,会检查这个环境变量REDIRECT_STATUS
是否存在且不为空,满足这个条件才认为请求是合法的重定向。如果不满足就拒绝PHP CGI的执行,从而禁止访问php-cgi.exe
。
不过XMAPP是Apache环境下,没有环境变量的问题。另外,虽然cgi.force_redirect
默认开启,但是我们现在能通过-d
修改配置,那如果把cgi.force_redirect
关了呢?复现成功。
另外,如果把XMAPP中被注释掉的# PHP-CGI setup
代码部分解开注释,也可以复现。
应用场景总结
所以Orange团队也总结了两种能攻击的场景。1. 在CGI模式下运行PHP(Action指令将相应的HTTP请求映射到PHP CGI可执行文件),常见配置如下。2. 通过ScriptAlias /php-cgi/ "C:/xampp/php/"
设置了PHP CGI目录,并且该目录下存在且暴露了php-cgi.exe
,也就是XMAPP的情况。上面的思考过程也应证了这两点。
AddHandler cgi-script .php
Action cgi-script "/cgi-bin/php-cgi.exe"
或者
<FilesMatch "\.php$">
SetHandler application/x-httpd-php-cgi
</FilesMatch>
Action application/x-httpd-php-cgi "/php-cgi/php-cgi.exe"
补充
看了@PHITHON大佬的文章,发现另外一个思路。
上面的文章中提到,PHP会检查REDIRECT_STATUS
环境变量是否为true
,从而来判断是否让cgi.force_redirect
生效。然后这个漏洞也是这么利用的,PHP也将其修复了。
查看PHP源码sapi/cgi/cgi_main.c
中对于REDIRECT_STATUS
环境变量的检查代码:https://github.com/php/php-src/blob/master/sapi/cgi/cgi_main.c
除了getenv("REDIRECT_STATUS")
还有getenv("HTTP_REDIRECT_STATUS")
,这两个环境变量都可以用于cgi.force_redirect
的检查。
环境变量以HTTP_
开头的,可以通过HTTP请求头控制,复现如下
另外,@PHITHON大佬提到,高版本PHP中,allow_url_include
已经被废弃,也会返回500。需要添加一个-d error_reporting=0
来规避这一点。但是我复现时采用新版的XMAPP没有遇到。这里记录一下。