显然,这位开发者是为了方便使用类似 #!/usr/local/bin/php-cgi -d include_path=https://www.jb51.net/path 的写法来进行测试,认为不应该限制php-cgi接受命令行参数,而且这个功能不和其他代码有任何冲突。
于是, if(!cgi) getopt(...) 被删掉了。
但显然,根据RFC中对于command line的说明,命令行参数不光可以通过 #!/usr/local/bin/php-cgi -d include_path=https://www.jb51.net/path 的方式传入php-cgi,更可以通过querystring的方式传入。
这就是本漏洞的历史成因。
那么,可控命令行参数,能做些什么事。
通过阅读源码,我发现cgi模式下有如下一些参数可用:
-c 指定php.ini文件的位置
-n 不要加载php.ini文件
-d 指定配置项
-b 启动fastcgi进程
-s 显示文件源码
-T 执行指定次该文件
-h 和 -? 显示帮助
最简单的利用方式,当然就是 -s ,可以直接显示源码:
但阅读过我写的fastcgi那篇文章的同学应该很快就想到了一个更好的利用方法:通过使用 -d 指定 auto_prepend_file 来制造任意文件读取漏洞,执行任意代码:
注意,空格用 + 或 %20 代替, = 用url编码代替。
这个漏洞被爆出来以后,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); }
先跳过所有空白符(小于等于空格的所有字符),再判断第一个字符是否是 - 。
这个漏洞在当年的影响应该说中等。因为PHP-CGI这个SAPI在漏洞出现的时间点,因为其性能等问题,已经在慢慢退出历史舞台了。但考虑到PHP这个在Web领域举足轻重的语言,跨越多年,用量巨大,很多老的设备、服务器仍在运行有漏洞的版本和PHP-CGI,所以影响也不能低估。
不过,在2017年的今天,我分析这个漏洞当然已经不能谈影响了,只是其思路确实比较有意思,又让我领会了一次阅读RFC的重要性。
您可能感兴趣的文章: