飞道的博客

红与蓝:现代Webshell检测引擎免杀对抗与实践

645人阅读  评论(0)

上半年Webshell话题很火,业界举办了数场对抗挑战赛,也发布了多篇站在安全产品侧,着重查杀思路的精彩文章,但鲜有看到以蓝军视角为主的paper。

作为多场挑战赛的参赛者及内部红蓝对抗的参与者,笔者试着站在蓝军角度,聊聊现代Webshell对抗的一些思路,也以PHP Webshell为例,分享包括利用PHP自身bug、构造PHP内存马、强制赋值私有变量等一些还没有被提及到但又比较有趣的trick。

希望能对正在参与某些大型攻防对抗演练的你有所收获。

1 轻松绕过传统规则查杀

即便是现在一些主流商业产品,也大量沿用了传统木马特征检测的思路,使用规则或者样本库作为单一查杀手段。
对蓝军来说,这样的安全防护能力无疑是十分单薄和脆弱,通常是不堪一击的。

1.1一个极端的case

先来看一个极端的case

一个公开的Webshell

这样一个在百度就可以搜索到的webshell,自然会被检出查杀。但只是将其做一个简单变形:

POST改成GET

将POST改成GET,就能成功绕过该产品。可见即使是很多商业级产品,也只是简单维护了一个样本库而已。

这虽然是一个比较极端的case,但在面对主流的规则查杀产品时,对抗原理也是相通的,只是需要更高强度地编码。

1.2 花括号的妙用

PHP中,实现了数组索引对于curly brace的支持(7.4之后已标记为deprecated,commit 4416)

这意味着一个数组索引a[0]中的方括号也可以用花括号{0}替代,同时再插入一些圆括号,使原义不变,但正则却匹配不到,便可以成功bypass几乎所有主流规则查杀工具。

更多这类符号可以从词法分析角度挖掘,对于不同的编程语言,其编译器源码或者官方文档中会有对Token的定义。

PHP Token列表

通过查找Space、Separator、Comment、Other等类别,能发现更多有趣的trick。

除此之外,利用加密、或者异或等数学运算生成的样本,也能达到特征湮灭的目的,传统规则查杀的绕过不是本篇重点,这里不再一一列举。其根本原因是,正则表达式的意义就只是在有限长的字符集中匹配有限长度的字符子集,但PHP等编程语言却是图灵完备的,其在字符层面上的排列会有无穷种组合,所以通过高强度的编码便能达到bypass目的。

正则表达式和图灵完备的编程语言之间天然的矛盾也决定了,无论怎样复杂的规则都无法从字符层面cover住图灵完备的PHP等语言,必然带来绕过的可能。

2 突破现代Webshell查杀引擎

在上半年发起公开挑战赛的数款Webshell检测产品都聪明地摒弃了规则方式,通过静态分析或沙箱执行等手段,尽可能地贴近编程语言的解释和执行,有效地减少了传统绕过的可能。

但同时在参赛时也发现,由于静态分析或沙箱执行本身的缺陷,也带来了新的bypass可能。

2.1静态分析缺陷

静态分析也就是白盒的思路,从语法树角度实现了从sink->source的追溯,其一般的工作路径是这样:

通过词法分析->语法分析->语义分析实现了对程序语法上的理解。但由于代码没有真正run起来,实际上并不能实现对一段代码完全地理解,尤其是在面对PHP这类较为灵活的脚本,而非静态类型语言时,利用这点缺陷可以发掘出非常多的case。

2.1.1利用php自身的bug

最极端的情况是,利用php解释器或其他php底层的bug,使语法树和实际run起来的代码表达不一致,达到在语法层面合法,但实际运行起来是webshell的目的。

在这个case中,按照php的语法,并没有任何操作对KaTeX parse error: Expected 'EOF', got '&' at position 27: …了修改(php中引用必须显式=&̲形式)。但在实际运行时由于ph…bar->arr的值实际上被改变,key中会新增system和$_GET[mo]值。

从语法树角度看是回调了两个空值,但实际运行时却是回调了
register_tick_function(‘system’,$_GET[‘mo’]);
成功绕过所有主流的静态分析工具。

不过PHP官方并不将其视为一个bug,官方对此的解释是

但从PHP对引用的定义来看,必须显式使用=&操作才能获取到引用的返回。如果非要强行解释的话,笔者认为是当执行&get_value( k e y ) 时 实 际 r e t u r n 了 key)时实际return了 key)returnthis->arr[ k e y ] 的 引 用 , 在 传 值 执 行 时 会 在 内 存 新 建 一 个 key]的引用,在传值执行时会在内存新建一个 key]this->arr[‘system’]的别名(不是指针,可理解为unix文件系统的硬链接,但指向的值是空)。但无论如何,这都和PHP文档中相关的定义不自洽,所以其更像是一个引用实现上的bug。

2.1.2 利用sink或source点不覆盖

通过流程图能看出,还可以利用白盒不覆盖的sink或source点的进行bypass,在此前的多篇文章中也提到这点,如利用filter_input、getenv等。这里举两个没有提到的case:

session_set_save_handler这个函数的特殊性在于他的每一个参数都是callable形式,并且read、write、destroy三个函数都会接收 s e s s i o n I d 值 , 而 sessionId值,而 sessionIdsessionId又受到session_id()的控制,通过构造回调,隐式调用了assert($_request)。类似的回调函数在php非常多,静态分析的sink点必须完全覆盖才能达到效果。

同理,source点上也有类似问题:

虽然关注了php://input形式,但在语法树上这个值仅仅是个字符串,通过简单变形混淆即可bypass。

2.1.3 打断污点传播

在之前的几次比赛中,被选手利用最多的方式恐怕便是打断污点传播了。这也是静态分析最大的问题—打断污点传播的方式五花八门、层出不穷。

如在php中,一个类似php://filter/read=convert.base64-encode/recource=index.php的wrapper/filter是可以自定义的,通过注册一个var协议,便隐去了从fopen到myclass之间语法树上的联系


成功bypass主流静态分析查杀工具。

还可以利用一些静态分析无法获得的操作系统资源,如内存、CPU、磁盘等动态值

通过取当前进程开销,由于其中ru_oublock参数常为0,便可以在bypass主流静态分析的同时,又得到一个稳定的webshell。

除此之外,如果静态分析没有适配unset等函数的话,可以通过变量销毁打断污点的传播:

原因是unset销毁了 x 在 c h a n g e 函 数 内 的 引 用 , 其 后 紧 跟 的 x在change函数内的引用,其后紧跟的 xchangex='q23’只是函数作用域内的局部变量,所以参与回调的 x 并 未 发 生 改 变 。 当 静 态 分 析 未 能 正 确 处 理 u n s e t 时 便 会 造 成 x并未发生改变。当静态分析未能正确处理unset时便会造成 xunset便x值的混淆,导致bypass。

2.1.4 强制修改私有变量

还可以利用一些静态分析上常犯的错误,如对private的处理上,理论上被private修饰的私有变量无法从外部访问(OOP语法明确规定),但实际上常见的语言无论是php、java、python等都有特殊的方法突破这一限制,从而实现对private属性的访问,如java的反射。

在php中,除了利用反射机制,还可以通过\Closure::bind实现类似的功能

大概原理是将一个闭包中$this的指向改为其他实例(或者叫绑定),作用域的改变使得外部函数也可以访问到受保护的private、protected属性。从而导致了语法树的不可见,但webshell却能被实际执行。

2.2沙箱执行的弱点

动态沙箱和静态分析在流程上大同小异。主要区别在于,利用污点追踪技术,沙箱实现了对用户输入的追溯,并能真实执行目标代码。同时沙箱还能收集样本的实时行为并打上标签,如命令执行后门(HEUR.WebShell.Exec)、中国菜刀(HEUR.WebShell.Chopper)等,从而能做出更准确的判断。


显然,沙箱的绕过方法要远远少于静态分析,主要原因还是沙箱真实执行了静态分析中没有分析到位的部分,真正将代码run了起来,使得打断污点传播变得极为困难。

但即使如此,也依然存在绕过可能,除了和静态分析同样对sink和source不覆盖问题,还可以从以下方面考虑。

2.2.1随机行为


通常沙箱只会运行一次,一但这次执行得不到可疑之处,便不再运行。那么可以引入随机数来达到目的,如上述case中沙箱猜中的几率是1/9,但攻击者却可以无限次尝试来逼近webshell。

2.2.2利用定时/延时绕过


同样还可以利用时间戳,由于时间是自然增长的,当前时间戳的第7位是1,加上114后为char(115),即字符s。执行的是assers,第一时间也就通过了沙盒检测。但当1000秒后执行时,时间发生了变化,此时执行的是assert。

可以通过控制时间戳的位数来决定webshell的有效时长,也可以直接指定一个时间点如12月15日0点,在此之前上传通过检查,等待到达时间点后再访问执行。

2.2.3文件信息不对称

通常沙箱执行环境与真实文件所处环境通常会不同,比如沙盒通常会重命名文件然后检测,再比如执行路径不同。利用这点导致的信息不对称,也可绕过沙箱检测。


通过取自身文件名,成功bypass。

3 其他trick

除此之外,跨文件分析、时效性、编程语言版本差异等问题也是常利用的弱点,举两个例子:

3.1 PHP下的内存马、无文件马

Java中通过Instrumentation接口或者注册Bean Controller等方式可以将Webshell代码注入进内存/JVM进程中。但对PHP来说,由于PHP生命周期的关系,很难达到相同的效果。

最简单的便是通过死循环来使代码驻留在内存,最早在乌云中就提到类似思路:

删除自身后,通过死循环常驻内存,不停生成真实的webshell,达到不死马的效果。
缺点显而易见,真实webshell没有任何伪装,且以文件形式存在,会被检测引擎快速捕获。

我们将其进行改进:

Require完成后立即删除该文件,使真实webshell存储在内存中,却无文件存在。静态分析/沙盒检测时会查无文件,导致bypass。

3.2 PHP版本差异

不同PHP版本之间往往存在语法和执行上的差异,而沙盒/静态分析工具通常是固定于某个版本,利用这点也会导致bypass,最常见的莫过于利用php的新特性,如:

PHP7新引入的Null合并运算符,可以简单实现三元运算。

PHP7.4新增的函数类型声明:

PHP7.4的短闭包写法:

实现了一个短闭包,并隐式地调用了use($code)。

一旦静态分析/沙盒不支持这类新的语法特性,则依然会导致层出不穷的bypass问题。

3.3 莫名其妙的case

当然也有一些莫名其妙的case,可能由于检测引擎本身的问题,导致失效,比如:

因为一些不明原因bug,一些引擎带上var_dump函数后,再执行eval就会失效。

同理,echo在某些环境下也可以:

4. 结语

我们讨论了蓝军视角下传统和现代webshell查杀的一些缺陷和弱点,但同时也注意:在红蓝对抗中,webshell查杀通常是hids的一部分。作为主机层面的入侵检测工具,hids除了漏报率和误报率要达到条件,能否及时探测web路径、能否云上互相感知同步等基础能力也十分重要,一但某一环节的及时性或有效性跟不上,都有可能带来新的gap点。

最后打个广告:

百度webshell查杀引擎webdir+(https://scanner.baidu.com/)
自2014年就一直开放测试中,基于RASP思路很好地解决了静态分析及沙盒执行的弱点,对上述绝大部分免杀的webshell均能够很好地支持,也欢迎各路白帽子挑战尝试。


转载:https://blog.csdn.net/Baidu_Secrity/article/details/108709536
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场