這個漏洞嚴格上說並不是 Nginx 和 PHP 本身的漏洞造成的,而是由配置造成的。在我之前寫的許多配置中,都普遍存在這個漏洞。
簡易檢測方法:
開啟 Nginx + PHP 伺服器上的任意一張圖片,如:
http://hily.me/test.png
如果在圖片連結後加一串 /xxx.php (xxx為任一字元)後,如:
http://hily.me/test.png/xxx.php
圖片還能訪問的話,說明你的配置存在漏洞。
漏洞分析:
下面通過分析一個很常見的 Nginx 配置來解釋下漏洞的成因:
server { listen 80; server_name test.local; access_log /work/www/logs/test.access.log main; error_log /work/www/logs/test.error.log; location / { root /work/www/test; index index.html index.htm index.php; } location ~ \.php$ { root /work/www/test; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; fastcgi_pass unix:/tmp/php-fpm.sock; }}
我們在 /work/www/test/ 目錄下建立一個檔案 test.png,內容如下:
那麼訪問 http://test.local/test.png 時,輸出為常值內容:
但是當在後面加上 /xxx.php 時,即 http://test.local/test.png/xxx.php,可怕的事情發生了:
Array( [HOSTNAME] => [PATH] => /usr/local/bin:/usr/bin:/bin [TMP] => /tmp [TMPDIR] => /tmp [TEMP] => /tmp [OSTYPE] => [MACHTYPE] => [MALLOC_CHECK_] => 2 [USER] => www [HOME] => /home/www [FCGI_ROLE] => RESPONDER [SCRIPT_FILENAME] => /work/www/test/test.png [QUERY_STRING] => [REQUEST_METHOD] => GET [CONTENT_TYPE] => [CONTENT_LENGTH] => [SCRIPT_NAME] => /test.png/xxx.php [REQUEST_URI] => /test.png/xxx.php [DOCUMENT_URI] => /test.png/xxx.php [DOCUMENT_ROOT] => /work/www/test [SERVER_PROTOCOL] => HTTP/1.1 [GATEWAY_INTERFACE] => CGI/1.1 [SERVER_SOFTWARE] => nginx/0.7.62 [REMOTE_ADDR] => 192.168.1.163 [REMOTE_PORT] => 4080 [SERVER_ADDR] => 192.168.1.12 [SERVER_PORT] => 80 [SERVER_NAME] => test.local [REDIRECT_STATUS] => 200 [HTTP_ACCEPT] => image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/QVOD, application/QVOD, application/x-ms-application, application/x-ms-xbap, application/vnd.ms-xpsdocument, application/xaml+xml, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */* [HTTP_ACCEPT_LANGUAGE] => zh-cn [HTTP_ACCEPT_ENCODING] => gzip, deflate [HTTP_USER_AGENT] => Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQPinyin 689; QQDownload 627; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; InfoPath.2; TheWorld) [HTTP_HOST] => test.local [HTTP_CONNECTION] => Keep-Alive [ORIG_SCRIPT_FILENAME] => /work/www/test/test.png/xxx.php [PATH_TRANSLATED] => /work/www/test [PHP_SELF] => /test.png/xxx.php [REQUEST_TIME] => 1274125615)
環境變數中,SCRIPT_FILENAME 是 Nginx 傳過來的:
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
$fastcgi_script_name 變數說明請參考:
http://wiki.nginx.org/NginxHttpFcgiModule
Nginx 傳給 PHP 的值為 /work/www/test/test.png/xxx.php,即 $_SERVER 中 ORIG_SCRIPT_FILENAME 的值,但是 $_SERVER 中 SCRIPT_FILENAME 卻是 /work/www/test/test.png。
原因是,/work/www/test/test.png/xxx.php 並不存在,對於這些不存在的路徑,PHP 會檢查路徑中存在的檔案,並將多餘的部分當作 PATH_INFO。
這裡,/work/www/test/test.png 被 PHP 解析為 SCRIPT_FILENAME,/xxx.php 被 PHP 解析為 PATH_INFO 後被丟棄,因此並沒有在 $_SERVER 中出現。
解決方案:
解決這個漏洞的方法很顯然:關閉上面所述的解析即可。
這個解析可以在 PHP 的設定檔中設定,預設為開啟。在這裡我們需要將它關閉:
; cgi.fix_pathinfo provides *real* PATH_INFO/PATH_TRANSLATED support for CGI. PHP's
; previous behaviour was to set PATH_TRANSLATED to SCRIPT_FILENAME, and to not grok
; what PATH_INFO is. For more information on PATH_INFO, see the cgi specs. Setting
; this to 1 will cause PHP CGI to fix its paths to conform to the spec. A setting
; of zero causes PHP to behave as before. Default is 1. You should fix your scripts
; to use SCRIPT_FILENAME rather than PATH_TRANSLATED.
; http://php.net/cgi.fix-pathinfo
;cgi.fix_pathinfo=1
cgi.fix_pathinfo=0
其中 cgi.fix_pathinfo=0 為新增的配置行,表示關閉 PHP 的自動 PATH_INFO 檢測。關閉後,該配置漏洞即可消除。
更好的解決方案?
以上方案並不是最完美的,如果你先前有用到 cgi.fix_pathinfo 這個特性,影響會很大,比如關閉後,我的 Blog(Wordpress)文章的 URL 目錄形式就得用 rewrite 來實現了。
如果可以將 PHP 設定成只解析 .php 為副檔名的檔案,那麼這個問題解決起來會更合理。
不過我沒找到相關的設定項,或許今後應該出現在 php-fpm 的設定檔中?
總結:
這類問題基本上是無法預料的,但是如果架構設計良好的話,即使存在這個問題,也不會影響安全性。這裡給出架構上的安全建議:
* 儘可能使動靜內容分離,所有的靜態內容存在於靜態內容伺服器,靜態內容伺服器上不解析PHP,這樣靜態檔案就永遠不能被解析了。