Fastjson 1.2.80 autoType Throwable 异常类绕过分析
写在前面
1.2.47 缓存绕过之后,官方在 1.2.48 堵了缓存,1.2.68 又祭出safeMode这把“终极武器”。但 safeMode 默认不开–只要用户没显式setSafeMode(true),autoType 这扇门就还留着缝。1.2.80 就是顺着这条缝,用Throwable(异常类)配合expectClass机制,完成了 autoType 在 safeMode 时代的最后一击。
这篇拆expectClass这条常被忽略的路径,看 Throwable 是怎么借它绕过黑名单的。
纯源码分析,无截图。
一、漏洞简介
- 漏洞组件:Alibaba fastjson
- 影响版本:1.2.68 ≤ version ≤ 1.2.80(safeMode 之后、1.2.83 修复前)
- 漏洞类型:autoType 黑名单绕过(Throwable + expectClass)导致反序列化 RCE
- CVE:无(fastjson 后续绕过均未申请 CVE)
- 修复版本:1.2.83(收窄 expectClass 路径)
- 前提:未开启 safeMode(safeMode 默认 false)
- 根因:checkAutoType 的
expectClass分支在 autoTypeSupport=false 时仍可加载 expectClass 的子类;Throwable 的反序列化路径会把用户可控的类名以expectClass=Throwable.class传入,绕过黑名单
二、1.2.68 safeMode:开了才安全
先澄清一个误区。1.2.68 引入的safeMode确实是 autoType 的终结技:
// ParserConfig.checkAutoType (1.2.68+)publicClass<?>checkAutoType(StringtypeName,Class<?>expectClass){if(safeMode){thrownewJSONException("safeMode not support autoType: "+typeName);// 直接拒}...}开了 safeMode,@type一律不认,什么绕过都没用。但它默认是 false,要用户主动ParserConfig.getGlobalInstance().setSafeMode(true)。绝大多数项目没开–于是 autoType 仍在工作,1.2.80 的绕过就是针对这种“没开 safeMode”的默认配置。
三、expectClass:被忽略的旁路
checkAutoType 有个第二参数expectClass,平时很少被注意:
publicClass<?>checkAutoType(StringtypeName,Class<?>expectClass)它的本意是:当 fastjson 已经知道“这个字段期望的类型是 X”(比如某个方法的参数类型是Throwable),解析@type时传入expectClass=X,允许加载 X 的子类。这是为正常多态反序列化设计的–比如字段声明的是Exception,实际 JSON 里给个具体子类。
关键源码(1.2.68 ~ 1.2.80,简化):
// checkAutoType (1.2.68+,核心分支)if(safeMode)throw...;// ① 黑名单(仍查)longhash=fnv1a_64(className);for(inti=0;i<denyHashCodes.length;i++){if(hash==denyHashCodes[i])throw...;}// ② expectClass 旁路(★ 关键)if(expectClass!=null){if(expectClass==Object.class||expectClass.isAssignableFrom(/* 待加载类 */)){// 走这条:加载 typeName,只要它是 expectClass 的子类就放行Class<?>clazz=TypeUtils.loadClass(typeName,defaultClassLoader,false);if(clazz!=null&&expectClass.isAssignableFrom(clazz)){returnclazz;// ★ 不强制 autoTypeSupport,绕过!}}}...注意第 ② 步:只要expectClass不为 null,且待加载类是它的子类,就放行–这条路径不检查 autoTypeSupport。换句话说,即使 autoType 关着,只要能骗 fastjson 以一个非 null 的expectClass调 checkAutoType,就能加载expectClass的任意子类(只要不在黑名单)。
问题变成:怎么让 fastjson 用expectClass=某个宽泛类型去调 checkAutoType,而且这个宽泛类型有“不在黑名单的危险子类”?–Throwable 正好满足。
四、Throwable 的反序列化路径
fastjson 对Throwable/Exception有专门的处理(ThrowableReader/ 异常类的 deserializer)。当 JSON 里@type是java.lang.Exception(或 Throwable),fastjson 走异常解析分支,它会把 JSON 里下一个@type(具体异常类名)以expectClass = Throwable.class调 checkAutoType:
// ThrowableReader 解析异常对象(简化)StringexClassName=/* 读到的第二个 @type,具体异常类名 */;Class<?>exClass=parser.getConfig().checkAutoType(exClassName,Throwable.class);// ★ expectClass=Throwable// -> 命中第②步 expectClass 旁路,加载 exClass(只要它是 Throwable 子类且不在黑名单)Objectex=exClass.newInstance();// 填字段(异常类的 message、cause 等 setter)payload 雏形(两段@type):
{"@type":"java.lang.Exception","@type":"com.xxx.MaliciousException","field":"..."}- 第一个
@type=java.lang.Exception:进 Throwable 解析分支(Throwable 本身不在黑名单)。 - 第二个
@type=com.xxx.MaliciousException:被 ThrowableReader 以expectClass=Throwable.class调 checkAutoType。命中 expectClass 旁路,只要MaliciousException是 Throwable 子类且不在黑名单,就放行加载。
五、利用链:需要一个 Throwable 子类 gadget
绕过黑名单只是“能加载类”,要 RCE 还得这个类有副作用。这里和前几篇不一样:1.2.80 本身只提供“加载任意 Throwable 子类”的能力,gadget 得自己找。常见的姿势:
- 找一个构造方法或 setter 里有 JNDI/反射/加载动作的 Throwable 子类(通常在第三方库里)。比如某些库的自定义异常在构造时会记录上下文、加载资源,被挪用成触发点。
- 典型 PoC 常结合 Groovy、Spring 等库里的特定异常类(具体哪个类随依赖而变,核心是“它是 Throwable 子类 + 有副作用”)。
@type=java.lang.Exception -> 进 Throwable 解析分支 └ 第二个 @type=某 Throwable 子类(不在黑名单) └ checkAutoType(name, Throwable.class) └ expectClass 旁路 -> 加载该异常类(autoTypeSupport 不查) └ 实例化 + setter/构造副作用 -> RCE所以 1.2.80 的利用门槛比 1.2.24/1.2.47 高–它要目标 classpath 上有合适的 Throwable gadget。但“能绕过黑名单加载任意 Throwable 子类”这一步,本身已是 autoType 防线的失守。
六、修复:1.2.83 收窄 expectClass
1.2.83 的修复针对 expectClass 旁路:对expectClass路径也加上更严格的校验(收紧哪些 expectClass 允许走旁路,或对 Throwable 这类宽泛基类的子类也过黑名单):
// 1.2.83 思路(简化):expectClass 旁路也要过黑名单if(expectClass!=null){// 不再无条件放行子类,先过黑名单longhash=fnv1a_64(className);for(inti=0;i<denyHashCodes.length;i++){if(hash==denyHashCodes[i])throw...;}// 且 expectClass 必须在允许的范围内(如非 Object/Throwable 这种过宽基类)...}同时官方再次强调:真正可靠的修复是开启 safeMode。只要 safeMode 开着,第 ② 步根本走不到(checkAutoType 顶部就抛异常了),expectClass 旁路无从发挥。
七、小结
1.2.80 这篇,我们看到 autoType 防线在 safeMode 时代的最后一道裂痕:
- 旁路参数也是攻击面:
expectClass本是为多态设计的“信任通道”,但只要它存在且不检查 autoTypeSupport,就成了绕过黑名单的侧门。任何“为方便而开的信任通道”都要问:它能不能被外部输入触发? - 默认配置是安全水位线:safeMode 默认 false,意味着绝大多数用户其实没受保护。一个“要手动开才安全”的防御,等于把安全责任推给用户–而用户多半不会开。
- gadget 可移植:1.2.80 把“加载任意类”收窄到“加载任意 Throwable 子类”,提高了门槛,但没堵死。只要 classpath 上有合适的异常类 gadget,仍可 RCE。
至此 fastjson 的 autoType 绕过史–从 1.2.24 裸奔、1.2.41 前后缀、1.2.47 缓存、到 1.2.80 Throwable–就走完了。最后那篇防御演进,我们看官方是怎么一步步把这些洞堵上、最终走向 safeMode 的。
参考
- fastjson GitHub:https://github.com/alibaba/fastjson
- 系列前篇:1.2.24 autoType 分析、1.2.41-43 黑名单绕过、1.2.47 缓存绕过