针对 CVE-2026-28326 的未认证补丁状态检查,该漏洞是 SolarWinds Access Rights Manager (ARM) 中的未认证远程代码执行漏洞,已于 2026-09-17 在 ARM 2026.2.1 中修复。CWE-798,CVSS 8.8 高危
(CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)。由 Armadin 的 Kai Huang 报告给 SolarWinds。
该向量中的 AV:A 假设 TCP 55555 仅可从相邻网络访问,这正是 ARM 自带防火墙工具所针对的部署态势;在端口被更广泛开放的环境中,该评分低估了该环境的暴露程度。
ARM 在 TCP 55555 上的 gRPC 远程调用监听器使用客户端证书对等端进行身份验证。未打补丁的版本还会接受一个回退凭据,该凭据派生自一个在所有安装中完全相同的硬编码密钥,从而绕过该身份验证并暴露一条不安全的反序列化路径。其结果是代码以 ARM 服务账户身份执行,该账户默认以 LocalSystem 运行。
该脚本报告每个目标上是否存在修复。它读取身份验证拦截器返回的状态码,不使用该密钥、不进行身份验证,也不向反序列化路径发送数据。除 VULNERABLE 之外的结果本身并不表示目标已打补丁;请参阅解读非易受攻击结果。
# single target (port defaults to 55555)
./cve_2026_28326_check.py arm.example.com
# explicit port
./cve_2026_28326_check.py arm.example.com:55555
# several targets
./cve_2026_28326_check.py arm-a.example.com arm-b.example.com
# scan a list, one target per line ('#' comments allowed), compact output
./cve_2026_28326_check.py -f targets.txt --brief
# machine-readable output for pipelines
./cve_2026_28326_check.py -f targets.txt --json > results.json
Python 3.8+,仅使用标准库 — 无第三方包。
请针对 ARM 服务器而非收集器运行该工具。两种角色都监听 55555,但收集器在修复前后都会以相同方式拒绝外部请求,因此该工具会将其报告为 INCONCLUSIVE。
易受攻击的服务器(默认输出;在 TTY 上 [!] 标记和 VULNERABLE 以红色显示):
$ ./cve_2026_28326_check.py arm.example.com
[!] arm.example.com:55555: VULNERABLE [unavailable-on-server-role]
Remoting/Send returned 14 UNAVAILABLE on a listener answering as the server role, the pre-patch rejection path (<= 2026.2.0.42)
已打补丁的服务器。修复后的版本会返回 grpc-message,该工具会将其与判定结果一并打印:
$ ./cve_2026_28326_check.py arm-dr.example.com
[+] arm-dr.example.com:55555: PATCHED [unauthenticated-gate]
Remoting/Send returned 16 UNAUTHENTICATED, the localhost and per-process-token gate introduced by the fix (>= 2026.2.1.7)
grpc-message: Client certificate not found. Please register your application first.
无法分类的监听器。收集器,或拒绝外部请求的服务器,在任一版本上对两次探测的响应都相同:
$ ./cve_2026_28326_check.py collector.example.com
[?] collector.example.com:55555: INCONCLUSIVE [external-requests-refused]
the listener refuses external requests, which a collector role and a restricted server both do identically on either build; scan the ARM server itself
使用 --brief 的多个目标:
$ ./cve_2026_28326_check.py -f targets.txt --brief; echo "exit: $?"
VULNERABLE arm.example.com:55555 unavailable-on-server-role
PATCHED arm-dr.example.com:55555 unauthenticated-gate
INCONCLUSIVE collector.example.com:55555 external-requests-refused
UNAFFECTED db.example.com:55555 not-arm
ERROR offline.example.com:55555 unreachable
exit: 1
使用 --json 的 JSON 输出。包含两次探测结果,以便对照原始响应检查判定:
$ ./cve_2026_28326_check.py arm.example.com --json
[
{
"target": "arm.example.com:55555",
"verdict": "VULNERABLE",
"reason": "unavailable-on-server-role",
"detail": "Remoting/Send returned 14 UNAVAILABLE on a listener answering as the server role, the pre-patch rejection path (<= 2026.2.0.42)",
"grpc_message": "",
"probes": [
{
"path": "/SolarWinds_ARM_GRPC_ConnectionAuthrization.ConnectionAuthorization/GetCertificate",
"status": 0,
"message": "",
"alpn": "h2",
"note": ""
},
{
"path": "/SolarWinds_ARM_Remoting.Remoting/Send",
"status": 14,
"message": "",
"alpn": "h2",
"note": ""
}
]
}
]
该检查旨在用于生产系统:
Remoting/Send 消息体中的序列化对象触发。两次探测都发送正文长度为零的 gRPC 消息,拦截器会在任何消息体被分发之前拒绝该调用。GetCertificate 探测发送空的注册码,服务器在查找或删除任何待处理注册码之前就会拒绝,因此该探测不会消耗正处于注册过程中的客户端的注册码。| # | 调用 | 目的 |
|---|---|---|
| 1 | ConnectionAuthorization/GetCertificate | 确认该端点是 ARM 且处于服务器角色 |
| 2 | Remoting/Send | 补丁状态探测 |
在未打补丁的服务器上,被拒绝的匿名请求会以调试级别记录日志
(Unable to validate remote request token),并记录一行指明对等端的信息性日志。已打补丁的版本会记录一条拒绝日志,指明对等端以及上面 grpc-message 中引用的注册提示。两个版本都不会记录会话。
若要查找利用行为而非暴露情况,请检查 ARM 的 NetworkConnectionEntry 记录。一条 IsAuthenticated=false 且 ClientCertThumbPrint 有值的记录与身份验证绕过一致,且不会出现在正常流量中。
该修复改变了身份验证拦截器对未出示客户端证书的对等端返回的状态。打补丁前,客户端证书检查中的每条拒绝路径都设置 Unavailable。打补丁后,localhost 加令牌检查会设置 Unauthenticated 并附带一条独特消息,该路径上没有任何其他情况返回 16。两次匿名调用即可区分版本:
判定从 Remoting/Send 读取,因为只有双工流式处理和一元处理器会传播拦截器的状态;服务器流式和客户端流式处理器在两个版本上都会覆盖它。
该检查观察已打补丁代码路径的行为,不依赖版本字符串。它不会报告已打补丁的服务器运行的是哪个版本。
ARM 监听器会重置未协商 ALPN h2 的 TLS 连接,因此该工具显式设置它。未使用 ALPN 通过 TLS 连接的扫描器不会从该端口收到任何数据。
VULNERABLE 和 PATCHED 各自基于观察到的服务器响应。INCONCLUSIVE 和 ERROR 表示探测未能对目标进行分类,因此其补丁状态未知。因此 INCONCLUSIVE 与 PATCHED 分开报告。
每个判定都带有一个简短的 reason 标签。--brief 将其作为第三列打印,--json 将其作为 reason 携带。
对于所有三种 INCONCLUSIVE 原因,补丁状态未知。原因标签指示在再次运行检查之前需要解决什么。
| 代码 | 含义 |
|---|---|
0 |
退出码 0 涵盖 PATCHED、UNAFFECTED 以及所有三种 INCONCLUSIVE 原因。要区分已打补丁的目标与无法分类的目标,请读取判定(--brief 的第一列或 --json 的 verdict 字段),而非退出码。
INCONCLUSIVE。此情况是针对复现该响应的模拟环境测试的,而非针对已部署的收集器。grpc-status 作为字面 HPACK 字段进行扫描,未实现 Huffman 或动态表解码。ARM 使用的 gRPC C 核心会以未压缩方式发送这些尾部字段。如果未来版本对它们进行 Huffman 编码,该工具会报告 INCONCLUSIVE 并附带 partial-response。PATCHED 仅适用于此 CVE,不表示其他 ARM 漏洞的状态。VULNERABLE 表示探测所到达的路径上不存在修复。该工具不会在目标上执行代码,也不会检测先前的失陷;请参阅上面的 NetworkConnectionEntry 指标。根据 ARM 2026.2.1 发行说明,升级到 ARM 2026.2.1.7 或更高版本。版本 2026.2.0.42 及更早版本受影响。
附加说明:
此代码在 MIT 许可证 下分发。
未经事先相互同意,使用此工具攻击目标是非法的。最终用户有责任遵守所有适用的地方、州和联邦法律。开发者不承担任何责任,也不对由此程序造成的任何误用或损害负责。
| 标志 | 描述 |
|---|
TARGET | 一个或多个 HOST[:PORT] 目标;端口默认为 55555 |
-f, --targets-file FILE | 从文件读取目标(每行一个;# 为注释) |
-p, --port PORT | 目标未指定端口时的默认端口(默认:55555) |
--timeout SECS | 每次探测的超时时间(默认:12) |
--workers N | 并发目标数(默认:16);输出保持输入顺序 |
-b, --brief | 每个目标一行对齐输出,适用于扫描大量主机 |
--json | 输出结构化 JSON,包含每个目标的两次探测结果 |
--no-color | 禁用彩色输出(同时遵循 NO_COLOR 和非 TTY 环境) |
Remoting/Send | GetCertificate | 版本 | 判定 |
|---|
14 UNAVAILABLE | 0 OK | <= 2026.2.0.42 | VULNERABLE |
16 UNAUTHENTICATED | 任意 | >= 2026.2.1.7 | PATCHED |
14 UNAVAILABLE | 14 UNAVAILABLE | 任一 | INCONCLUSIVE |
| 判定 | 原因标签 | 含义 |
|---|
VULNERABLE | unavailable-on-server-role | Remoting/Send 在作为服务器角色应答 GetCertificate 的监听器上返回 14。修复不存在。 |
PATCHED | unauthenticated-gate | Remoting/Send 返回 16,即修复引入的状态。仅适用于此 CVE。 |
INCONCLUSIVE | external-requests-refused | 两次探测都返回 14。收集器角色和受限服务器在两个版本上此处行为相同。请扫描 ARM 服务器本身。 |
INCONCLUSIVE | partial-response | 只有一次探测返回了状态,因此无法比较这一对。请重试。 |
INCONCLUSIVE | unrecognized-status | 端点应答的状态组合不在已识别集合内。 |
UNAFFECTED | not-arm | 此处没有 ARM gRPC 服务应答。 |
ERROR | unreachable | TCP 连接、TLS 握手或 HTTP/2 交换失败。 |
没有目标为 VULNERABLE |
1 | 至少一个目标为 VULNERABLE |
2 | 用法错误(参数错误、目标无效或目标文件不可读) |
130 | 被中断(Ctrl-C) |