阴雨绵绵正好眠,奈何 KPI 催人奋起

介绍

  哥斯拉 (Godzilla) 是一款国内流行且优秀的红队 webshell 权限管理工具,使用 java 开发的可视化客户端,shell 支持 java、php、asp 环境,通信流量使用 AES 算法加密,具有文件管理、数据库操作、命令执行、内存马、隧道反弹等后门功能。上传特征 WAF,IDS/IPS 都能做,这里不做分析,主要关注下通讯阶段的特征。

流量特征

User-Agent (弱特征)

  哥斯拉客户端使用 JAVA 语言编写,在默认的情况下,如果不进行修改,可能是:User-Agent: Java/1.8.0_131 。但是哥斯拉支持自定义 HTTP 头部,这个默认特征是可以很容易去除的。

Accept(弱特征)

  Accept 为 text/html, image/gif, image/jpeg, *; q=.2, /; q=.2
对这个默认特征应该很熟悉了,之前冰蝎也出现过同样的Accept。为什么会这么巧合出现两个工具都会出现这个特征呢,其实这个也是 JDK 引入的一个特征,并不是作者自定义的 Accept(参考:https://link.zhihu.com/?target=https%3A//bugs.openjdk.java.net/browse/JDK-8177439)同样的这个默认特征也可以通过自定义头部去除,只能作为默认情况下的辅助检测特征。

Cookie (强特征)

  在请求包的 Cookie 中有一个非常致命的特征,最后的分号。标准的 HTTP 请求中最后一个 Cookie 的值是不应该出现 ; 的,这个可以作为现阶段的一个辅助识别特征。后面如果作者意识到这个问题的话应该会发布新版本修复这个问题。

请求体特征 (较强特征)

  哥斯拉支持对加密的数据进行 base64 编码以及原始的加密 raw 两种形式的通讯数据,对于请求体的检测也要考虑两种情况。

  Base64 编码后包含大写字母:A-Z、小写字母:a-z、数字:0-9、符号:+ 和 /、填充字符:= 。正则匹配
  RAW 的话,排除掉文件上传,也就是 multipart/form-data 数据包,然后匹配到大量不可见字符。

响应体特征 (强特征)

  和请求体一样,请求响应体也分两个格式,base64 编码的和原始加密 raw 数据。如果请求体采用 base64 编码,响应体返回的也是 base64 编码的数据。在使用 base64 编码时,响应体会出现一个很明显的固定特征。这个特征是客户端和服务端编写的时候引入的。

  从代码可以看到会把一个 32 位的 md5 字符串按照一半拆分,分别放在 base64 编码的数据的前后两部分。整个响应包的结构体征为:md5 前十六位 + base64 + md5 后十六位。

  从响应数据包可以明显看到这个特征,检测时匹配这个特征可以达到比较高的检出率,同时也只可以结合前面的一些弱特征进行检查,进一步提高检出率。因为 md5 的字符集范围在只落在 0123456789ABCDEF 范围内,因此很容易去匹配,正则匹配类似于 (?i:[0-9A-F]{16})[\w+/]{4,}=?=?(?i:[0-9A-F]{16})。需要注意的是 md5 需要同时匹配字母大小写两种情况,因为在 JAVA 版 webshell 响应中为大写字母,在 PHP 版中为小写字母。

哥斯拉静态特征

  在默认脚本编码的情况下,jsp 会出现 xc、pass 字符和 Java 反射 (ClassLoader,getClass().getClassLoader()),base64 加解码等特征。

哥斯拉动态特征

  User-Agent 字段(弱特征),如果采用默认的情况,会暴露使用的 jdk 信息。不过哥斯拉支持自定义 HTTP 头部,这个默认特征是可以很容易去除的。

  Accept 字段(弱特征),默认是 Accept:text/html, image/gif, image/jpeg, *; q=.2, /; q=.2。同上,这个也可修改,只能作为辅助检测的特征。

  Cookie 中有一个非常关键的特征,最后会有个分号。估计后续的版本会修复。

  响应体的数据有一定特征,哥斯拉会把一个 32 位的 md5 字符串按照一半拆分,分别放在 base64 编码的数据的前后两部分。整个响应包的结构体征为:md5 前十六位 + base64 + md5 后十六位。

哥斯拉长流量特征

  第三篇参考文章中写着哥斯拉使用了长 TCP 连接,中间的前三个 http 会话具备明显的统计特征。(这里的情况有待验证)

  客户端首次连接 shell 会上传一段代码 payload,以备后续操作调用。查看其请求,发现内容长度居然超过 23000 字节。同时,http 响应内容为空。
  第二个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 的每一位与被加密字符串进行异或位运算。(具体生成算法参考文章 [3])这就引出了第一个真理:

长度为 l 的字符串与长度为 n 的加密因子循环按位异或,密文的长度为 l。

  对于哥斯拉中频繁使用的 Base64 编码,又会引出真理二:

长度为l的字符串进行Base64编码后长度为定值。

检测思路(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]);
 }
}

个人思考:这个方案对于针对版本哥斯拉甚至只针对某个默认的 webshell,如果稍作改变应该表现效果不佳,既然第一个 http 是上传,后面的两个 http 是调用,则通过长度范围来匹配比较好。哥斯拉不支持自定义,全部提取一遍也可以,算是检测比较简单的一种远控了。

参考文章

[1]哥斯拉流量特征检测思路
[2]冰蝎、蚁剑、哥斯拉的流量特征
[3]巧用Zeek在流量层狩猎哥斯拉Godzilla