CVE-2024-4577 PHP-CGI漏洞

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脚本接受的命令行选项如下:
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,允许在includerequire语句中使用远程文件。设置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

bestfit936

左边的数值是源字符的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

查看XMAPP的PHP CGI配置C:/xampp/apache/conf/extra/httpd-xampp.conf
XMAPP默认配置

找到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_phpPHP-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

配置选项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关了呢?复现成功。

CVE-2024-4577复现

另外,如果把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

HTTP_REDIRECT_STATUS

除了getenv("REDIRECT_STATUS")还有getenv("HTTP_REDIRECT_STATUS"),这两个环境变量都可以用于cgi.force_redirect的检查。

环境变量以HTTP_开头的,可以通过HTTP请求头控制,复现如下
HTTP_REDIRECT_STATUS

另外,@PHITHON大佬提到,高版本PHP中,allow_url_include已经被废弃,也会返回500。需要添加一个-d error_reporting=0来规避这一点。但是我复现时采用新版的XMAPP没有遇到。这里记录一下。

  • 29
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值