WebShell检测绕过之上·知己知彼
WebShell原理回顾
Webshell的恶意性表现在它的实现功能上,是一段带有恶意目的的正常脚本代码。

这里仅对PHP的一句话木马进行分析,核心步骤如下:

数据传递
- 超全局变量:
$_GET、$_POST、$_COOKIES、$_REQUEST、$_FILE、$_SERVER - 从远程远程URL中获取数据:
file_get_contents、curl、svn_checkout…(将需要执行的指令数据放在远程URL中,通过URL_INCLUDE来读取) - 从本地磁盘文件中获取数据:
file、file_get_contents…(将需要执行的指令数据放在本地磁盘文件中,利用IO函数来读取) - 从数据库中读取(将需要执行的指令放在数据库中,利用数据库函数来读取)
- 从图片头部中获取:
exif_read_data…(将需要执行的指令数据放在图片头部中,利用图片操作函数来读取)
代码执行
- 命令执行类:通过
eval、assert、system、create_function等函数来执行命令/代码 - 动态构造类:在php支持的动态构造函数中调用执行命令
- preg类:正则系列函数可以使用
/e模式正则来执行命令 - 回调函数类:利用回调函数构造的Webshell,覆盖所有的
callable类型参数 - 反射函数类:利用
ReflectionFunction等类,以及对应的invoke等方法执行命令 - 文件包含类:
include、include_once、require、require_once、file_get_contents
WebShell主流检测手段
基于正则文本特征
初期对付Webshell基本上都是这种方案,根据已知Webshell使用的函数和参数,使用正则表达式制定相应的黑名单规则。
正则表达式检测法最早被广泛应用,但由于正则文法表达能力不足,以及当前WebShell混淆技术的成熟,导致基于正则的特征选择极易被绕过。

上图检测脚本可以通过以下链接复制:
基于统计特征
之后为了对付混淆,人们把视野放到了统计学上,通过统计文本熵,字符串长度,特殊符号个数,重合指数,压缩比等信息制定告警阈值,在对付混淆上有一定的威力。之后随着机器学习的兴起,阈值设置就交给了算法。
Webshell由于往往经过了编码和加密,会表现出一些特别的统计特征,根据这些特征统计学习。但是这种方式着眼于全局,无法顾及局部细节,将混淆的恶意代码插入到正常文件中,基本上就失效了。
所以实践发现这种方法应该只能适用于大马和加密马的检测。
典型的代表:
ShellDaddy(https://gitee.com/tdifg/shelldaddy)。算法部分主要借鉴了 NeoPI 以及趋势科技开源出来的 TLSH。
使用的检测方法:
- 信息熵(Entropy):通过计算文本ASCII码表的总体熵值来衡量文件的不确定性
- 最长单词(LongestWord):最长的字符串可能表示潜在的被编码或被混淆
- 重合指数(Indexof Coincidence):低重合指数指示文件代码潜在的被加密或被混效过
- 特征(Signature):在文件中搜索已知的恶意代码字符串片段
- 压缩(Compression):文件的压缩比指示潜在的编码对抗行为
- 文件的重合指数(IC):种方式可以用来判断文件的加密可能性。
- 特殊字符串比重:比如一个文件中像^ ~ 等等不常用的符号出现的频率超过一定的值可判断文件是webshell的可疑程度。
- TLSH:一种新型的模糊 hash 算法,经过测试对比发现比 ssdeep 的速度快了接近一倍。不过有个问题就是不支持太小的文件。
- 简易模型:引入算法是为了补充规则在覆盖率上的不足,但是以上的算法除了 TLSH 算法之外 ,如果单独使用都会存在误报,所以ShellDaddy引入了一个简单的朴素贝叶斯模型,每种算法各占一些权重。
基于AST语法树
抽象语法树(AST)是一种将原始Webshell文本转换为抽象语法结构的树状表示的一种方法。AST语法树弥补了统计特征的不足,进一步深化,进行语法检测,关注于每个函数和参数,这种方式精确,误报较少。
对于Webshell这种实时编译并执行的语言来说,抽象语法树结构本质上是另一个新的中间语言形态的数据结构,基本过程入下图所示:

这里使用一个基于PHP-Praser生成AST的工具,我找了很久没找到原作者,就索性放到我的github上了。
基于上述特征的WebShell检测方法已经能很好的识别Java类的WebShell。但是对于PHP这种动态特性很多的语言,检测就比较吃力,AST是无法了解语义的。
动/静态符号执行
现在市面上大部分的WebShell查杀引擎都是基于这种方式去检测的。其本质上是数据流特征分析(污点分析),通过给$_GET、$_POST等输入参数打污点的方式,通过污点数据流是否流入敏感函数,来判断是否可疑。大体上有静态和动态两种。
静态污点分析
静态污点分析过程可以分为三个部分,首先发现eval危险函数,之后追踪eval参数$a,发现是在函数test中,然后再跟踪test函数的调用信息,最后锁定调用参数是否外部可控。
典型例子:https://github.com/opensec-cn/chip
动态污点分析
动态模拟执行部分是基于参数赋值+污点分析的方式实现的。通过在运行时对外界参数进行模拟赋值的方式,可以将模拟值(Source点)标记为污点数据,通过追踪污点数据相关的信息是否流向敏感函数(Sink点),以此来发现可通过外界参数控制敏感函数参数的风险行为。
典型例子:https://github.com/laruence/taint
污点传递理论概述
污点传递、或者说污点追踪,是一个泛概念,它在不同的应用领域都可以发挥良好的作用。
一般来说,污点分析可以抽象成一个三元组的形式,其中
- source 即污点源,代表直接引入不受信任的数据或者机密数据到系统中。
- sink 即污点汇聚点,代表直接产生安全敏感操作(违反数据完整性)或者泄露隐私数据到外界(违反数据保密性)。
- processor 即数据流处理,代表整个数据传输和处理的过程(例如加密、编码处理),外部输入的数据经过processor处理后会得到一个适合软件核心模块处理的数据形式。

以PHP为例的污点传递过程
污点分析就是分析程序中由污点源引入的数据,在经过数据流处理后,传播到污点汇聚点后,是否符合预设的策略。这里的策略是一个泛概念,例如:
- 污点直接被传入了危险的函数中(例如eval)
- 污点未经编码和转义处理,传入了输出函数中(例如xss漏洞)

例如,如下WebShell:
1 |
|
这是一个有攻防经验的人很容易看出的回调后门,但因为没有包含eval、assert之类的敏感语句或其它敏感的类方法,计算机难以识别。
但人类可以快速识别,运用逻辑分析能力将上面的代码分析成两个步骤:
- 用户可控参数被传入到了
uasort函数的第2个参数中 uasort函数的第2个参数为callable类型
按照如下规则将其绘制成图:
- 所有标识符都对应一个结点
- 函数调用以及函数参数都分别对应一个结点
- 如果标识符之间存在数据传递,则按照数据传递方向给结点连边
我们会看到一张清晰的有向图:

这张图中存在一条从绿色用户可控变量经过蓝色其他变量传递,最终到达红色动态特性数据结点的路径。
假如,我们再多加入参数传递和函数调用会变成什么样呢?
例如,如下WebShell:
1 |
|
发现随着代码行数的增加,靠人眼分析已经有点困难了。
那么,要是将其绘制成一个有向图呢?

发现无论怎么假如参数传递、函数调用,这条从用户可控端(绿色方框)到危险函数参数(红色方框)的路径都会存在。因为这份代码无论怎么修改,本质都是将用户输入的数据传递到uasort函数的参数中,来控制执行任意代码。
其实这种基于将代码审计的经验映射到有向图的特征中,让程序自动化地提取这些特征并进行分析,便是大部分的WebShell查杀引擎的检测方法。
基于机器学习/深度学习
主要目标是生成样本在解释执行过程中的行为序列,使用运行行为序列来建立词袋模型,最终对这个词袋模型进行训练和测试。
以PHP脚本为例,有两种类型的运行序列:
- 利用VLD编译源码得到的OPCODE序列
- 通过Zend OPCODE劫持实现的Runtime OPCODE序列。
由于这个需要大量的Webshell样本训练,目前来看效果可能还是不太好。而有些算法可解释性比较差,不利于运营。而且存在大量误报的可能。这种检测方式仅仅用于少量云查杀引擎中。
沙箱
动态沙箱和静态分析在流程上大同小异。主要区别在于,利用污点追踪技术,沙箱实现了对用户输入的追溯,并能真实执行目标代码。同时沙箱还能收集样本的实时行为并打上标签,如命令执行后门(HEUR.WebShell.Exec)、中国菜刀(HEUR.WebShell.Chopper)等,从而能做出更准确的判断。

显然,沙箱的绕过方法要远远少于静态分析,主要原因还是沙箱真实执行了静态分析中没有分析到位的部分,真正将代码run了起来,使得打断污点传播变得极为困难。
但即使如此,也依然存在绕过可能,除了和静态分析同样对sink和source不覆盖问题,还可以从以下方面考虑。
- 随机行为绕过
- 利用定时/延时绕过
- 文件信息不对称绕过
流量日志检测
这种检测技术属于统计机器学习方法,通过对HTTP访问日志中的URL作为聚类主键,结合攻防领域经验,生成一系列的统计特征。典型的特征如图所示:

针对修改WebShell管理工具的流量特征这里就不展开讲解了。
可以参考以下几篇文章:
魔改 - 蚁剑:
魔改 - 冰蝎:
魔改 - 哥斯拉:

PHP语法特性
PHP字符串的转义表示
- 八进制表示:
\[0-7]{1,3}(如:”a” === “\141”) - 十六进制表示:
\x[0-9A-Fa-f]{0,2}(如”a” === “\x61”) - Unicode表示:
\u{[0-9A-Fa-f]+}(如”a” === “\u{0061}”)
1 |
|
PHP的不同标记风格
PHP有如下几种标记风格:
- 标准风格:
<?php ... ?> - 简短风格:
<? ... ?> - 脚本风格:
<script language="php"> ... </script>(PHP7后被移除) - ASP风格:
<% ... %>(默认不开启,PHP7后被移除)
PHP-Parser只支持前两个标签,传入一个由 <script> 标签构造的 Webshell ,不识别该标签的检测引擎就会出现绕过。
PHP对未定义字符串的处理
将其直接强行转换成字符串,然后结合PHP可变函数进行利用,绕过单/双引号。
1 |
|
在Web应用全局过滤单/双引号的环境下,把后门代码留在某个Controller下,可以使用这个特性避免单双引号。
1 | PASS = eval('print(1)'); |
PHP内建函数 get_defined_functions
PHP自带的get_defined_functions函数包含了所有PHP内建及用户自定义的函数,内建函数会放在internal键中,而用户自定义函数会放在user中。
1 |
|
可以使用get_defined_functions['internal'][xx]获取特定的函数,如assert、create_function等,绕过语义分析中的sink点黑名单。
PHP通过define()定义常量数组
Array类型的常量现在可以通过define()来定义。在PHP5.6中仅能通过const定义。
1 |
|
PHP返回值类型声明
PHP 7 增加了对返回值类型声明的支持。类似与参数类型的声明,返回类型的声明指明了函数返回值的类型。可用的类型与参数声明中可用的类型相同。
1 |
|
就是函数返回值类型为int。否则会强制转换或者显示语法错误。如果未更新此特性的查杀软件,就不会认识此函数,导致被绕过。
全局变量-参数传递
超全局变量
$_GET
用来获取由浏览器通过GET方法提交的数据。
1 | eval($_GET['cmd']); |
$_POST
用来获取由浏览器通过POST方法提交的数据。
1 | eval($_POST['cmd']); |
$_REQUEST
用来获取由浏览器通过POST方法提交的数据。
1 | eval($_REQUEST['cmd']); |
$_COOKIE
用来获取由浏览器COOKIE中的数据。
1 |
|
$_FILE
用来获取通过 POST 方法上传文件的相关信息。
1 | <html> |
这里上传的文件名就是我们的payload(base64编码后),发送后便可执行代码。


$_SERVER
包含了诸如头信息(header)、路径(path)、以及脚本位置(script locations)等等信息的数组。
1 |
|
$_ENV
是一个包含服务器端环境变量的数组
1 |
|
注:需要修改php.ini文件的 variables_order值为
EGPCS(默认为GPCS)
EGPCS是Environment、Get、Post、Cookies、Server的缩写
$GLOBALS全局变量
$GLOBALS的作用是引用全局作用域中可用的全部变量。
$GLOBAL['_GET']==$_GET$GLOBAL['_POST']==$_POST$GLOBAL['_REQUEST']==$_REQUEST$GLOBAL['_COOKIE']==$_COOKIE$GLOBAL['_FILE']==$_FILE$GLOBAL['_SERVER']==$_SERVER$GLOBAL['_ENV']==$_ENV
使用方法与超全局变量一样
PHP变长参数
变长参数是PHP5.6新引入的特性。
和Python中的**kwargs,类似。在PHP中可以使用func(...$arr)这样的方式,将$arr数组展开成多个参数传入func函数。
可以结合回调函数构造WebShell:
1 | usort(...$_GET); |

注:PHP7不允许assert函数通过动态函数回调
PHP7下表达式执行顺序

PHP7之前是不允许用($a)();这样的方式来执行动态函数的,但PHP7中增加了对此的支持,利用这个特性执行phpinfo()的多种方式如下:
1 |
|
异或取反运算
该利用方式是通过 “ ^ “( 异或运算符 ) 和 “ ! “( 取反运算符 ) 组成一个WebShell。
异或运算
1 |
|
可以使用国光师傅写的一个脚本生成一个异或结果字典:
1 | import string |
脚本运行效果如下:
1 | python3 xxx.py > results.txt |
最终会生成1k多条结果,并按顺序排列。

取反运算
与异或运算有异曲同工之妙,唯一差异就是,方法一使用的是位运算里的“异或”,方法二使用的是位运算里的“取反”。
这里利用的是UTF-8编码的某个汉字,并将其中某个字符取出来,比如'和'{2}的结果是"\x8c",其取反即为字母s:

1 |
|
这里还利用了PHP的弱类型特性。因为要获取’和’{2},就必须有数字2。而PHP由于弱类型这个特性,true的值为1,故true+true==2,也就是(‘>’>’<’)+(‘>’>’<’)==2。
上面这种方法看起来真的很酷炫 , 但是尽管变化成这样 , 还是有很多 WAF 可以检测到的。
字符串变形与编码
- substr():返回字符串的一部分
- strtr():转换字符串中特定的字符
- str_replace():其他字符替换字符串中的一些字符
- substr_replace():把字符串的一部分替换为另一个字符串
- trim():移除字符串两侧的空白字符或其他预定义字符
- base64_encode():对使用 MIME base64 编码的数据进行编码
- base64_decode():对使用 MIME base64 编码的数据进行解码
- chr():从指定的十进制ASCII编码返回字符
- hexdec():十六进制转换为十进制
- str_rot13():对字符串执行 ROT13 编码。
注:字符串的变形可以很容易地绕过安全狗,但对于D盾的话效果甚微。
下一节我将结合主流的WebShell查杀引擎,通过绕过Source点、Sink点与绕过数据流处理检测对Webshell进行科学免杀。
参考资料
- WebShell通用免杀的思考 - 云+社区 - 腾讯云 (tencent.com)
- webshell免杀的一些学习和思考——以PHP为例_闵行小鱼塘-CSDN博客
- Webshell入侵检测初探(一) - FreeBuf网络安全行业门户
- 污点传递理论在Webshell检测中的应用 — PHP篇 - 知乎 (zhihu.com)
- Webshell研究综述:检测与对抗技术的动态博弈进展 - 知乎 (zhihu.com)
- 腾讯TEG安全平台部lake2 / 杜海章 演讲 《纵深防御之WebShell攻防》
- 基于语义分析和神经网络的WebShell检测方法—《网络空间安全》2019年02期 (cnki.com.cn)
- 初探机器学习检测 PHP Webshell (seebug.org)
- 红与蓝:现代Webshell检测引擎免杀对抗与实践 (qq.com)
- KCon/PHP动态特性的捕捉与逃逸.pdf at master · knownsec/KCon (github.com)