CVE-2012-1823一问世就被说成是“PHP远程代码执行漏洞”,一度“轰动一时”。那时候我刚一踏进防盗门,还只是个小盘。直到前段时间番茄大师给我看了一个案例,我才想起这个漏洞。通过对Vulhub中漏洞环境和漏洞原理的分析,我觉得挺有意思的,就写一篇文章和大家分享一下。
首先介绍PHP的运行模式。
下载PHP源代码,可以看到有一个目录叫sapi。sapi在PHP中的作用类似于消息“发送者”的作用,比如我在文章《Fastcgi协议分析 PHP-FPM未授权访问漏洞 Exp编写》中介绍的fpm。它的作用是通过fastcgi协议接受Web容器封装的数据,并将其交给PHP解释器执行。
除了fpm,最常见的sapi应该是针对apache的mod_php,用于php和Apache之间的数据交换。
Php-cgi也是一个sapi。在古代,web应用程序的运行模式非常简单。web容器收到http数据包后,得到用户请求的文件(cgi脚本),并分叉出一个子进程(解释器)执行该文件,然后得到执行结果并直接返回给用户,同时解释器子进程结束。大多数基于bash、perl和其他语言的web应用程序都是以这种方式执行的,这种方式通常被称为cgi。安装Apache时,默认有一个cgi-bin目录,最早是用来放置这些cgi脚本的。
然而,cgi模式有一个致命的缺点。众所周知,进程的创建和调度消耗一定的量,进程的数量不是无限的。因此,在cgi模式下运行的网站通常不能同时接受大量的请求,否则每个请求都会产生一个子流程,可能会压垮服务器。然后是fastcgi。fastcgi进程可以一直在后台运行自己,通过fastcgi协议接受数据包,执行后返回结果,但不会自行退出。
Php有一个叫php-cgi的sapi。php-cgi有两个功能,一个是提供cgi交互,另一个是提供fastcgi交互。也就是说,像perl一样,我们可以让web容器直接分叉一个php-cgi进程来执行一个脚本;也可以在后台运行PHP-CGI-b 127 . 0 . 0 . 1:9000(PHP-CGI是fastcgi的管理器),让web容器使用fastcgi协议与9000进行交互。
我之前提到的fpm是什么?为什么php有两个fastcgi管理器?php中有两个fastcgi管理器,php-cgi可以在fastcgi模式下运行,fpm也可以在fastcgi模式下运行。然而,fpm是由php在5.3版本之后引入的,它是一个更高效的fastcgi管理器。它的优点我就不多说了,可以自己去翻源代码。因为fpm有更多的优势,现在越来越多的web应用使用php-fpm来运行php。
回到这个漏洞。CVE-2012-1823是php-cgi sapi的漏洞。我介绍了php-cgi提供的两种运行模式:cgi和fastcgi。此漏洞仅出现在以cgi模式运行的php中。
简单来说,这个漏洞就是用户请求的querystring被用作php-cgi的参数,最终导致一系列的结果。
为了探索这个原理,RFC3875规定,当querystring不包含未解码的=符号时,querystring应该作为cgi参数传入。因此,Apache服务器根据需要实现了这个功能。
但是PHP并没有注意到RFC的这个规则,可能是注意处理了一次,处理的方法是不允许在web上下文中传递参数。但在2004年,一位开发商发表了这样的言论:
来自lerdorf.comSubject:处的拉斯马斯勒德尔夫拉斯姆斯: [PHP-DEV] php-cgi命令行开关内存检查新闻组: gmane。比较。PHP。发展: 2004-02-04 23:26:41 GMT(7年49周3天20小时39分钟前)在我们的SAPI cgi中,我们沿着这些线进行检查:} if(!cgi) getopt(.)与中一样,如果我们在网上下文中运行,我们不会解析计算机生成图像二进制文件的命令行参数。与此同时,我们的回归测试系统试图使用计算机生成图像二进制,并设置这些变量,以便正确测试获取/发布请求。在回归测试系统中,我们广泛使用-d来覆盖初始化设置文件的后缀名设置,以确保我们的测试环境是正常的。当然这两个想法是冲突的,所以目前我们的回归测试有些破碎。我们没有注意到这一点,因为我们没有太多包含获取/发布数据的测试,也很少构建计算机生成图像二进制文件。这里问题的关键是,是否有人记得为什么我们决定不解析计算机生成图像版本的命令行参数?我可以很容易地看到它是有用的,能够写一个像: # CGI脚本!/usr/local/bin/PHP-CGI-d include _ path=/path?服务器端编程语言(专业超文本预处理器的缩写).并让它在命令行和网环境中工作。据我所知,这不会与任何事情发生冲突,但一定有人在某个时候有理由不允许这样做。拉斯穆斯显然,这位开发者是为了方便使用类似#!/usr/local/bin/PHP-CGI-d include _ path=/path的写法来进行测试,认为不应该限制php-cgi接受命令行参数,而且这个功能不和其他代码有任何冲突。
于是,如果(!cgi) getopt(.)被删掉了。
但显然,根据请求评论中对于命令行的说明,命令行参数不光可以通过#!/usr/local/bin/PHP-CGI-d include _ path=/path的方式传入php-cgi,更可以通过查询字符串的方式传入。
这就是本漏洞的历史成因。
那么,可控命令行参数,能做些什么事。
通过阅读源码,我发现计算机生成图像模式下有如下一些参数可用:
-c指定php.ini文件的位置同表示“发展”、“创造”或“状态的加剧”:widen | deepen | loosen不要加载php.ini文件-d指定配置项-b启动fastcgi进程构成名词复数显示文件源码相当于表示“有…的”执行指定次该文件-h和-?显示帮助
最简单的利用方式,当然就是-s,可以直接显示源码:
但阅读过我写的fastcgi那篇文章的同学应该很快就想到了一个更好的利用方法:通过使用-d指定auto_prepend_file来制造任意文件读取漏洞,执行任意代码:
注意,空格用或代替,=用全球资源定位器(统一资源定位符)编码代替。
这个漏洞被爆出来以后,PHP官方对其进行了修补,发布了新版本5.4.2及5.3.12,但这个修复是不完全的,可以被绕过,进而衍生出CVE-2012-2311漏洞。
服务器端编程语言(专业超文本预处理器的缩写)的修复方法是对-进行了检查:
if(QUERY _ STRING=getenv(' QUERY _ STRING '){ decoded _ QUERY _ STRING=str up(QUERY _ STRING);PHP _ URL _ decode(decoded _ query _ string,strlen(decoded _ query _ string));if(* decoded _ query _ string=='-' strchr(decoded _ query _ string,'=')==NULL){ skip _ getopt=1;}免费(解码后的_ query _ string);}可见,获取查询字符串后进行解码,如果第一个字符是-则设置skip_getopt,也就是不要获取命令行参数。
这个修复方法不安全的地方在于,如果运维对php-cgi进行了一层封装的情况下:
#!/bin/shexec/usr/local/bin/PHP-CGI $ *通过使用空白符加-的方式,也能传入参数。这时候查询字符串的第一个字符就是空白符而不是-了,绕过了上述检查。
于是,php5.4.3和php5.3.13中继续进行修改:
if((QUERY _ STRING=getenv(' QUERY _ STRING '))!=NULL strchr(query_string,'=')==NULL) { /*我们得到的查询字符串没有=- apache CGI将把它传递给命令行*/无符号字符* p;decoded _ query _ string=str dup(query _ string);PHP _ URL _ decode(decoded _ query _ string,strlen(decoded _ query _ string));for(p=decoded _ query _ string;* p * p=p ) { /*跳过所有前导空格*/} if(* p=='-'){ skip _ getopt=1;}免费(解码后的_ query _ string);}在判断第一个字符是否为-之前,跳过所有空格(所有小于或等于空格的字符)。
当年这个漏洞的影响应该说是适度的。因为PHP-CGI,一个SAPI,在漏洞出现的时候已经慢慢退出历史舞台,因为它的性能等问题。但考虑到在Web领域扮演重要角色的PHP跨越多年,使用量巨大,很多老设备、老服务器仍在运行易受攻击的版本和PHP-CGI,影响不容小觑。
但是,2017年的今天,我分析了这个漏洞之后,谈不上有什么影响,但是思考真的很有趣,让我再次明白了阅读RFC的重要性。