
Неразрушающая проверка состояния патча для SolarWinds ARM CVE-2026-28326, которая опрашивает gRPC-слушатель на TCP 55555 и сообщает VULNERABLE, PATCHED или INCONCLUSIVE.
Проверка состояния патча без аутентификации для CVE-2026-28326 — уязвимости удалённого
выполнения кода без аутентификации в SolarWinds Access Rights Manager (ARM), исправленной в
ARM 2026.2.1
17.09.2026. CWE-798, CVSS 8.8 High
(CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H). Сообщено в SolarWinds Кай Хуангом из Armadin.
AV:A в этом векторе предполагает, что TCP 55555 доступен только из смежной сети — именно на
такой сценарий рассчитаны собственные средства межсетевого экранирования ARM; там, где порт
открыт более широко, оценка занижает уровень подверженности для этой среды.
gRPC-слушатель удалённого взаимодействия ARM на TCP 55555 аутентифицирует узлы с помощью
клиентского сертификата. Непропатченные сборки также принимают резервные учётные данные,
производные от жёстко закодированного ключа, одинакового для всех установок, что обходит эту
аутентификацию и открывает небезопасный путь десериализации. Результат — выполнение кода от
имени учётной записи службы 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.
| Флаг | Описание |
|---|---|
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) |
Уязвимый сервер (вывод по умолчанию; маркер [!] и VULNERABLE отображаются красным на TTY):
$ ./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 | Проверка состояния патча |
На непропатченном сервере отклонённый анонимный запрос записывается в журнал на уровне debug
(Unable to validate remote request token) и информационной строкой с указанием узла. Пропатченные
сборки записывают отклонение с указанием узла и подсказкой о регистрации, процитированной в
grpc-message выше. Ни одна из сборок не записывает сессию.
Чтобы искать эксплуатацию, а не подверженность, просмотрите записи NetworkConnectionEntry в ARM.
Запись с IsAuthenticated=false и заполненным ClientCertThumbPrint согласуется с обходом
аутентификации и не встречается в нормальном трафике.
Исправление изменило то, какое состояние перехватчик аутентификации возвращает узлу, не
предъявляющему клиентский сертификат. До патча каждый путь отклонения в проверке клиентского
сертификата устанавливал Unavailable. После патча проверка localhost-плюс-токен устанавливает
Unauthenticated с характерным сообщением, и больше ничто на этом пути не возвращает 16. Два
анонимных вызова различают сборки:
Remoting/Send | GetCertificate | Сборка | Вердикт |
|---|---|---|---|
14 UNAVAILABLE | 0 OK | <= 2026.2.0.42 | VULNERABLE |
16 UNAUTHENTICATED | любое | >= 2026.2.1.7 | PATCHED |
14 UNAVAILABLE | 14 UNAVAILABLE | любая | INCONCLUSIVE |
Вердикт считывается из Remoting/Send, потому что только обработчики duplex-streaming и unary
распространяют состояние перехватчика; обработчики server- и client-streaming перезаписывают его
на обеих сборках.
Проверка наблюдает поведение исправленного пути кода и не полагается на строку версии. Она не сообщает, какая сборка запущена на пропатченном сервере.
Слушатель ARM сбрасывает TLS-соединения, которые не согласуют ALPN h2, поэтому инструмент
устанавливает его явно. Сканер, подключающийся по TLS без ALPN, не получает данных от порта.
VULNERABLE и PATCHED основаны на наблюдаемом ответе сервера. INCONCLUSIVE и ERROR
означают, что проверки не классифицировали цель, поэтому её состояние патча неизвестно. По этой
причине INCONCLUSIVE сообщается отдельно от PATCHED.
Каждый вердикт несёт короткий тег reason. --brief выводит его третьей колонкой, а --json
передаёт его как reason.
| Вердикт | Тег reason | Значение |
|---|---|---|
VULNERABLE | unavailable-on-server-role | Remoting/Send вернул 14 на слушателе, отвечающем на GetCertificate как роль сервера. Исправление отсутствует. |
PATCHED | unauthenticated-gate | Remoting/Send вернул 16 — состояние, введённое исправлением. Применимо только к этой CVE. |
INCONCLUSIVE | external-requests-refused | Обе проверки вернули 14. Роль коллектора и ограниченный сервер ведут себя здесь одинаково на обеих сборках. Сканируйте сам сервер ARM. |
INCONCLUSIVE | partial-response | Только одна проверка вернула состояние, поэтому пару нельзя сравнить. Повторите попытку. |
INCONCLUSIVE | unrecognized-status | Конечная точка ответила комбинацией состояний вне распознанного набора. |
UNAFFECTED | not-arm | Здесь не ответила ни одна служба gRPC ARM. |
ERROR | unreachable | Не удалось установить TCP-соединение, выполнить TLS-рукопожатие или обмен по HTTP/2. |
Для всех трёх причин INCONCLUSIVE состояние патча неизвестно. Тег reason указывает, что нужно
устранить перед повторным запуском проверки.
| Код | Значение |
|---|---|
0 | Ни одна цель не была VULNERABLE |
1 | Как минимум одна цель VULNERABLE |
2 | Ошибка использования (неверные аргументы, недопустимая цель или нечитаемый файл целей) |
130 | Прервано (Ctrl-C) |
Код выхода 0 охватывает PATCHED, UNAFFECTED и все три причины INCONCLUSIVE. Чтобы отличить
пропатченную цель от той, которую не удалось классифицировать, читайте вердикт (первую колонку
--brief или поле verdict в --json), а не код выхода.
INCONCLUSIVE. Этот случай тестировался на макете, воспроизводящем ответ, а не на
развёрнутом коллекторе.grpc-status как литеральное поле HPACK и не реализует
декодирование Хаффмана или динамической таблицы. Ядро C gRPC, которое использует ARM, отправляет
эти trailers без сжатия. Если будущая сборка закодирует их по Хаффману, инструмент сообщит
INCONCLUSIVE с partial-response.PATCHED применимо только к этой CVE и не указывает на состояние других
уязвимостей ARM.VULNERABLE указывает, что исправление отсутствует на пути,
которого достигла проверка. Инструмент не выполняет код на цели и не обнаруживает предыдущую
компрометацию; см. индикатор NetworkConnectionEntry выше.Обновитесь до ARM 2026.2.1.7 или более поздней версии, согласно примечаниям к выпуску ARM 2026.2.1. Сборки 2026.2.0.42 и более ранние подвержены уязвимости.
Дополнительные замечания:
Этот код распространяется под лицензией MIT.
Использование этого инструмента для атаки на цели без предварительного взаимного согласия незаконно. Ответственность за соблюдение всех применимых местных, региональных и федеральных законов лежит на конечном пользователе. Разработчики не несут ответственности и не отвечают за любое неправомерное использование или ущерб, вызванный этой программой.