通过前两篇文章的铺垫,现在我们可以了解 CVE-2024-4577这个漏洞的原理
漏洞原理
CVE-2024-4577是CVE-2012-1823这个老漏洞的绕过,php cgi的老漏洞至今已经12年,具体可以参考我的另一个文档
简单来说,就是使用cgi模式运行的PHP,根据RFC3875的规定,Apache会将请求的QUERY_STRING作为命令行参数交给php-cgi执行。攻击者利用这个特性,就可以-d选项修改PHP的配置项,最后执行任意代码。
文章的最后,我也提到了当时PHP官方修复了两次才完成的补丁:
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); }
修复方式就是在获取到QUERY_STRING后,检查其开头是否是横线(-),如果是-,则跳过后面对参数的解析。而@Orange 这次发现的CVE-2024-4577实际上就是横线检查的绕过,来说下原理。
在Windows中,如果当前系统的字符集(也被称为代码页,Code Page)非Unicode编码,在main()函数获取argv的时候,会自动执行编码的转换,其中就会涉及到所谓的Best-Fit。cp936
gbk
Best-Fit是一种字符映射策略,用以解决源代码页中的字符在目标代码页中没有直接等价物时的问题。在将Unicode代码页中字符转换成非Unicode代码页字符时,如果无法找到对应的字符,就会按照Best-Fit预定义的一个转换表进行转换。
比如,GBK编码(cp936)的Best-Fit Mapping转换表是:https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit936.txt
其中有一些有趣的字符转换,比如0xaa在转换后会变成a,0xb2在转换后会变成2,0xad在转换后会变成-。
所以,这里我们就可以利用Best-Fit这个特性,使用%ad来代替横线-。PHP在执行上述检查的时候,会认为命令行的第一个字符是0xAD;实际上main()函数的argv的第一个字符已经被Windows按照best-fit mapping转换成-。
本质来说,这仍然是一个利用解析差异绕过防御措施的案例。
什么情况下才会使用Best-Bit转换字符串?
我们可以编写下面这个简单的Python代码来复现best-fit这个trick:
os.system("php \xadh")
虽然传入的是\xad,但实际最后执行成功。
但我们多测试几次就会发现,并不是所有程序的所有参数都会进行Best-Bit的转换,我们测试下面几个命令:
php -h => php \xadh => 成功
php -v => php \xadv => 出错
python -h => python \xadh => 出错
arp -h => arp \xadh => 成功
有的成功,有的出错。这是为什么呢?
阅读PHP代码就会发现,有一部分选项是直接使用的main()函数的argv,另一部分选项是从Windows API GetCommandLineW()函数获取。由GetCommandLineW()获取的这部分参数仍然保持原样,没有转换成横线,最后无法正确执行。
XAMPP为什么可以被利用?
Orange在文章中提到,XAMPP默认配置就可以被利用。最初读到这里的时候,我会以为XAMPP是以php-cgi模式运行的PHP服务器,但下载安装XAMPP后我发现,PHP实际上是以Apache 2.0 Handler的方式运行。
这时候就非常有趣了,为什么XAMPP仍然可以在不修改任何配置文件的情况下直接利用呢?为什么很多人在实际测试中会遇到500错误呢?
这实际上是PHP没有修复的另一个安全机制bypass
比如,默认情况下我们需要将php解释器放在cgi-bin目录下,这样用户通过访问/cgi-bin/php/dir/script.php,即可执行/dir/script.php。
这个操作是很危险的,所以PHP增加了如下配置:cgi.force_redirect=1,开启了这个选项(默认开启)以后,只有经过了重定向规则请求才能执行。
Apache在重定向(rewrite)的时候,会增加一个名为REDIRECT_STATUS的环境变量,cgi.force_redirect就是依赖这个环境变量,来判断是否经历了重定向。
如果非Apache服务器,我们就需要设置一下cgi.redirect_status_env,来指定php判断请求是否经历重定向的条件。
XAMPP默认使用的sapi
我们下载XAMPP后,查看PHPINFO可以发现,其运行PHP的模式是“Apache 2.0 Handler”。在这个模式下,PHP会被编译成Apache的一个模块(dll动态链接库)并由Apache来调用执行。
我们在apache/conf/extra/httpd-xampp.conf中可以找到相关的配置:
LoadFile "/program/xampp/xampp/php/php8ts.dll"LoadFile "/program/xampp/xampp/php/libpq.dll"LoadFile "/program/xampp/xampp/php/libsqlite3.dll"LoadModule php_module "/program/xampp/xampp/php/php8apache2_4.dll"<FilesMatch "\.php$">SetHandler application/x-httpd-php</FilesMatch><FilesMatch "\.phps$">SetHandler application/x-httpd-php-source</FilesMatch>
可见,这里所有的.php后缀文件会被交给php8apache2_4.dll来执行,这和PHP CGI是没有任何关系的。
如何正常配置使用PHP CGI?
思考一个问题,如果我们需要正常配置一个使用PHP CGI解析PHP的Apache服务器,应该如何编写配置文件呢?
如果是部署基于脚本的CGI服务器,我们需要将可执行文件放置在某个目录下,比如/cgi-bin/hello.cgi。这个hello.cgi脚本的第一行是“shebang”,用以指定当前脚本的解释器,比如 # !/bin/bash
ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
<Directory "/usr/lib/cgi-bin">
AllowOverride None
Options +ExecCGI
AddHandler cgi-script .cgi .pl .php
Require all granted
</Directory>
-
设置 PHP 文件的 Shebang 行和 Content-Type 头
在每个 PHP 脚本的开头添加 Shebang 行和
Content-Type
头。例如,/cgi-bin/hello.cgi
文件内容如下:#!/usr/bin/php <?php header('Content-Type: text/html; charset=UTF-8');echo "<html>"; echo "<head><title>Hello, CGI!</title></head>"; echo "<body>"; echo "<h1>Hello, World!</h1>"; echo "</body>"; echo "</html>
其中,
#!/usr/bin/php
是 Shebang 行,用于指定 PHP 解释器的路径。 -
设置文件权限
确保 PHP 文件具有执行权限,使其可以作为 CGI 脚本运行:
chmod +x /cgi-bin/hello.cgi
-
测试脚本
通过浏览器访问该脚本。例如,如果 CGI 脚本路径是
/cgi-bin/hello.cgi
,则访问http://yourdomain.com/cgi-bin/hello.cgi
。
事实上几乎没有PHP应用会这么编写代码,相比于这种将脚本直接作为可执行文件的方法,PHP另辟蹊径,专门设计了一个SAPI就叫php-cgi。
调用php-cgi执行一个PHP文件时,它会负责输出“Content-Type”头。比如执行echo '<?php echo 123;' | php-cgi
所以,类似于hello.cgi,我们也可以直接将php-cgi这个可执行文件映射到/cgi-bin/目录下,然后再使用Apache的Action指令,将PHP相关请求“重定向”到php-cgi上,最后执行PHP代码。
此时配置文件如下:
ScriptAlias /php-cgi/ "/program/xampp/xampp/php/"<Directory "/program/xampp/xampp/htdocs">AddHandler application/x-httpd-php .phpAction application/x-httpd-php /php-cgi/php-cgi.exe</Directory>
ScriptAlias指令的作用是将/php-cgi/路径指向/program/xampp/xampp/php/目录,
Action指令的作用就是将所有对.php文件的请求都使用/php-cgi/php-cgi.exe,也就是/program/xampp/xampp/php/php-cgi.exe来执行。
Apache在调用php-cgi的时候,会设置环境变量REDIRECT_STATUS。php-cgi为了确认这个请求确实是由Action指令执行的,而不是用户直接请求的,增加了一个开关“cgi.force_redirect”,默认开启。开启这个开关的情况下,php-cgi会验证此次执行是否包含环境变量REDIRECT_STATUS。
XAMPP为什么可以被利用?
了解了php-cgi正常的部署方式,我们回来看下XAMPP。虽然XAMPP执行PHP时使用的是Apache 2.0 Handler,但它仍然给PHP CGI预留了一个口子,就是使用ScriptAlias指令把php-cgi.exe映射到了Web目录下。
上述两个关键指令,XAMPP中使用了ScriptAlias指令,但是Action指令被注释了
这本身也没太大问题,因为有cgi.force_redirect的限制,在没有Action指令的情况下直接访问php-cgi.exe,REDIRECT_STATUS环境变量不会被设置。我们可以做个测试,直接请求/php-cgi/php-cgi.exe会返回500错误,查看日志会有Security Alert!的字眼
php-cgi.exe 接收cgi格式 -d=cgi.force_redirect=0 说明不再检测环境变量REDIRECT_STATUS
因为我们前面说到,对于REDIRECT_STATUS环境变量的限制是cgi.force_redirect这个开关来决定的。那么我们直接利用CVE-2024-4577漏洞,添加-d cgi.force_redirect=0关闭这个开关,即可绕过限制了。
值得注意的是,因为在高版本PHP中,allow_url_include这个开关已经被废弃,所以会抛出一个警告,导致Content-Type头输出失败,也会返回500。网上很多人没有注意到这一点,我们需要添加一个-d error_reporting=0来规避这一点。
PHP没有修复的另一种cgi.force_redirect绕过
前面我们使用-d cgi.force_redirect=0关闭PHP的检查,实际上PHP中有另一个Bug,也可以导致cgi.force_redirect的绕过,而且最新版本仍然没有修复
我们查看PHP源码sapi/cgi/cgi_main.c中对于REDIRECT_STATUS环境变量的检查代码:
/* check force_cgi after startup, so we have proper output */if (cgi && CGIG(force_redirect)) {/* Apache will generate REDIRECT_STATUS,* Netscape and redirect.so will generate HTTP_REDIRECT_STATUS.* redirect.so and installation instructions available from* http://www.koehntopp.de/php.* -- kk@netuse.de*/if (!getenv("REDIRECT_STATUS") &&!getenv ("HTTP_REDIRECT_STATUS") &&/* this is to allow a different env var to be configured* in case some server does something different than above */(!CGIG(redirect_status_env) || !getenv(CGIG(redirect_status_env)))) {zend_try {SG(sapi_headers).http_response_code = 400;PUTS("<b>Security Alert!</b> The PHP CGI cannot be accessed directly.\n\n\<p>This PHP CGI binary was compiled with force-cgi-redirect enabled. This\n\means that a page will only be served up if the REDIRECT_STATUS CGI variable is\n\set, e.g. via an Apache Action directive.</p>\n\<p>For more information as to <i>why</i> this behaviour exists, see the <a href=\"PHP: 以 CGI 模式安装时 - Manual \">\manual page for CGI security</a>.</p>\n\<p>For more information about changing this behaviour or re-enabling this webserver,\n\consult the installation file that came with this distribution, or visit \n\<a href=\"PHP: Windows 系统下的安装 - Manual \">the manual page</a>.</p>\n");} zend_catch {} zend_end_try();if defined(ZTS) && !defined(PHP_DEBUG)/* XXX we're crashing here in msvc6 debug builds at* php_message_handler_for_zend:839 because* SG(request_info).path_translated is an invalid pointer.* It still happens even though I set it to null, so something* weird is going on.*/tsrm_shutdown();endif free(bindpath);return FAILURE;}}
可见,除了getenv("REDIRECT_STATUS")以外,还有一个getenv("HTTP_REDIRECT_STATUS"),这两个环境变量都可以用于cgi.force_redirect的检查。而第二个环境变量HTTP_REDIRECT_STATUS是由HTTP_开头。
httpoxy漏洞就是因为很多HTTP客户端会使用HTTP_PROXY环境变量的值作为代理,因为这个环境变量是HTTP_开头,导致我们可以通过HTTP请求头控制,造成漏洞
PHP这里也是一样的问题,环境变量HTTP_REDIRECT_STATUS原本是为了兼容Netscape,但它是由HTTP_开头,所以用户可以直接控制。
PHP这里也是一样的问题,环境变量HTTP_REDIRECT_STATUS原本是为了兼容Netscape,但它是由HTTP_开头,所以用户可以直接控制。
我们去掉前面POC中的-d cgi.force_redirect=0,并添加一个HTTP头Redirect-Status: 1,仍然可以成功利用漏洞
虽然CVE-2024-4577漏洞在最新的PHP版本中已经修复了,但这个环境变量HTTP_REDIRECT_STATUS仍然可以导致cgi.force_redirect的绕过。