CGI远程代码执行漏洞分析与防范

CVE-2012-1823出来时据说是“PHP远程代码执行漏洞”,曾经也“轰动一时”,当时的我只是刚踏入安全门的一个小菜,直到前段时间tomato师傅让我看一个案例,我才想起来这个漏洞。通过在 Vulhub 中对这个漏洞环境的搭建与漏洞原理的分析,我觉得还挺有意思的,故写出一篇文章来,和大家分享。

首先,介绍一下PHP的运行模式。

下载PHP源码,可以看到其中有个目录叫sapi。sapi在PHP中的作用,类似于一个消息的“传递者”,比如我在《 Fastcgi协议分析 && PHP-FPM未授权访问漏洞 && Exp编写 》一文中介绍的fpm,他的作用就是接受Web容器通过fastcgi协议封装好的数据,并交给PHP解释器执行。

除了fpm,最常见的sapi应该是用于Apache的mod_php,这个sapi用于php和apache之间的数据交换。

php-cgi也是一个sapi。在远古的时候,web应用的运行方式很简单,web容器接收到http数据包后,拿到用户请求的文件(cgi脚本),并fork出一个子进程(解释器)去执行这个文件,然后拿到执行结果,直接返回给用户,同时这个解释器子进程也就结束了。基于bash、perl等语言的web应用多半都是以这种方式来执行,这种执行方式一般就被称为cgi,在安装Apache的时候默认有一个cgi-bin目录,最早就是放置这些cgi脚本用的。

但cgi模式有个致命的缺点,众所周知,进程的创建和调度都是有一定消耗的,而且进程的数量也不是无限的。所以,基于cgi模式运行的网站通常不能同时接受大量请求,否则每个请求生成一个子进程,就有可能把服务器挤爆。于是后来就有了fastcgi,fastcgi进程可以将自己一直运行在后台,并通过fastcgi协议接受数据包,执行后返回结果,但自身并不退出。

php有一个叫php-cgi的sapi,php-cgi有两个功能,一是提供cgi方式的交互,二是提供fastcgi方式的交互。也就说,我们可以像perl一样,让web容器直接fork一个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年的时候某个开发者发表过这么一段言论:

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=https://www.jb51.net/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

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/a8d2e9bbee84840eb3dc58906b9a6a97.html