《PHP學習:PHP-CGI遠程代碼執行漏洞分析與防范》要點:
本文介紹了PHP學習:PHP-CGI遠程代碼執行漏洞分析與防范,希望對您有用。如果有疑問,可以聯系我們。
CVE-2012-1823出來時據說是“PHP遠程代碼執行漏洞”,曾經也“轟動一時”,當時的我只是剛踏入安全門的一個小菜,直到前段時間tomato師傅讓我看一個案例,我才想起來這個漏洞.通過在 Vulhub 中對這個漏洞環境的搭建與漏洞原理的分析,我覺得還挺有意思的,故寫出一篇文章來,和大家分享.PHP應用
首先,介紹一下PHP的運行模式.PHP應用
下載PHP源碼,可以看到其中有個目錄叫sapi.sapi在PHP中的作用,類似于一個消息的“傳遞者”,比如我在《 Fastcgi協議分析 && PHP-FPM未授權訪問漏洞 && Exp編寫 》一文中介紹的fpm,他的作用就是接受Web容器通過fastcgi協議封裝好的數據,并交給PHP解釋器執行.PHP應用
除了fpm,最常見的sapi應該是用于Apache的mod_php,這個sapi用于php和apache之間的數據交換.PHP應用
php-cgi也是一個sapi.在遠古的時候,web應用的運行方式很簡單,web容器接收到http數據包后,拿到用戶請求的文件(cgi腳本),并fork出一個子進程(解釋器)去執行這個文件,然后拿到執行結果,直接返回給用戶,同時這個解釋器子進程也就結束了.基于bash、perl等語言的web應用多半都是以這種方式來執行,這種執行方式一般就被稱為cgi,在安裝Apache的時候默認有一個cgi-bin目錄,最早就是放置這些cgi腳本用的.PHP應用
但cgi模式有個致命的缺點,眾所周知,進程的創建和調度都是有一定消耗的,而且進程的數量也不是無限的.所以,基于cgi模式運行的網站通常不能同時接受大量請求,否則每個請求生成一個子進程,就有可能把服務器擠爆.于是后來就有了fastcgi,fastcgi進程可以將自己一直運行在后臺,并通過fastcgi協議接受數據包,執行后返回結果,但自身并不退出.PHP應用
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交互.PHP應用
那我之前說的fpm又是什么呢?為什么php有兩個fastcgi管理器?php確實有兩個fastcgi管理器,php-cgi可以以fastcgi模式運行,fpm也是以fastcgi模式運行.但fpm是php在5.3版本以后引入的,是一個更高效的fastcgi管理器,其諸多優點我就不多說了,可以自己去翻翻源碼.因為fpm優點更多,所以現在越來越多的web應用使用php-fpm去運行php.PHP應用
回到本漏洞.CVE-2012-1823就是php-cgi這個sapi出現的漏洞,我上面介紹了php-cgi提供的兩種運行方式:cgi和fastcgi,本漏洞只出現在以cgi模式運行的php中.PHP應用
這個漏洞簡單來說,就是用戶請求的querystring被作為了php-cgi的參數,最終導致了一系列結果.PHP應用
探究一下原理, RFC3875 中規定,當querystring中不包含沒有解碼的 =
號的情況下,要將querystring作為cgi的參數傳入.所以,Apache服務器按要求實現了這個功能.PHP應用
但PHP并沒有注意到RFC的這一個規則,也許是曾經注意并處理了,處理方法就是web上下文中不允許傳入參數.但在2004年的時候某個開發者發表過這么一段言論:PHP應用
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接受命令行參數,而且這個功能不和其他代碼有任何沖突.PHP應用
于是, if(!cgi) getopt(...)
被刪掉了.PHP應用
但顯然,根據RFC中對于command line的說明,命令行參數不光可以通過 #!/usr/local/bin/php-cgi -d include_path=/path
的方式傳入php-cgi,更可以通過querystring的方式傳入.PHP應用
這就是本漏洞的歷史成因.PHP應用
那么,可控命令行參數,能做些什么事.PHP應用
通過閱讀源碼,我發現cgi模式下有如下一些參數可用:PHP應用
-c
指定php.ini文件的位置-n
不要加載php.ini文件 -d
指定配置項 -b
啟動fastcgi進程 -s
顯示文件源碼 -T
執行指定次該文件 -h
和 -?
顯示幫助PHP應用
最簡單的利用方式,當然就是 -s
,可以直接顯示源碼:PHP應用
PHP應用
但閱讀過我寫的fastcgi那篇文章的同學應該很快就想到了一個更好的利用方法:通過使用 -d
指定 auto_prepend_file
來制造任意文件讀取漏洞,執行任意代碼:PHP應用
PHP應用
注意,空格用 +
或 %20
代替, =
用url編碼代替.PHP應用
這個漏洞被爆出來以后,PHP官方對其進行了修補,發布了新版本5.4.2及5.3.12,但這個修復是不完全的,可以被繞過,進而衍生出CVE-2012-2311漏洞.PHP應用
PHP的修復方法是對 -
進行了檢查: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應用
這個修復方法不安全的地方在于,如果運維對php-cgi進行了一層封裝的情況下:PHP應用
#!/bin/sh exec /usr/local/bin/php-cgi $*
通過使用空白符加 -
的方式,也能傳入參數.這時候querystring的第一個字符就是空白符而不是 -
了,繞過了上述檢查.PHP應用
于是,php5.4.3和php5.3.13中繼續進行修改:PHP應用
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應用
這個漏洞在當年的影響應該說中等.因為PHP-CGI這個SAPI在漏洞出現的時間點,因為其性能等問題,已經在慢慢退出歷史舞臺了.但考慮到PHP這個在Web領域舉足輕重的語言,跨越多年,用量巨大,很多老的設備、服務器仍在運行有漏洞的版本和PHP-CGI,所以影響也不能低估.PHP應用
不過,在2017年的今天,我分析這個漏洞當然已經不能談影響了,只是其思路確實比較有意思,又讓我領會了一次閱讀RFC的重要性.PHP應用
轉載請注明本頁網址:
http://www.snjht.com/jiaocheng/834.html