
SolarWinds ARM CVE-2026-28326에 대한 비파괴적 패치 상태 검사기로, TCP 55555의 gRPC 리스너를 프로브하여 VULNERABLE, PATCHED 또는 INCONCLUSIVE를 보고합니다.
SolarWinds Access Rights Manager(ARM)의 인증되지 않은 원격 코드 실행 취약점인 CVE-2026-28326에 대한 인증되지 않은 패치 상태 확인 스크립트로, 2026-09-17에
ARM 2026.2.1에서
수정되었습니다. 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). Armadin의 Kai Huang이 SolarWinds에 제보했습니다.
해당 벡터의 AV:A는 TCP 55555가 인접 네트워크에서만 도달 가능하다고 가정하는데, 이는 ARM 자체 방화벽 도구가
전제로 하는 상황입니다. 포트가 더 광범위하게 열려 있는 환경에서는 이 점수가 해당 환경의 노출도를
과소평가합니다.
TCP 55555의 ARM gRPC 리모팅 리스너는 클라이언트 인증서로 피어를 인증합니다. 패치되지 않은
빌드는 모든 설치에서 동일한 하드코딩된 키에서 파생된 대체 자격 증명도 허용하는데, 이는 해당 인증을
우회하고 안전하지 않은 역직렬화 경로를 노출시킵니다. 그 결과 기본적으로 LocalSystem으로 실행되는
ARM 서비스 계정 권한으로 코드가 실행됩니다.
이 스크립트는 각 대상에 수정 사항이 존재하는지 보고합니다. 인증 인터셉터가 반환하는 상태 코드를 읽으며,
키를 사용하거나 인증하거나 역직렬화 경로로 데이터를 전송하지 않습니다. VULNERABLE 이외의 결과가
대상이 패치되었다는 것을 그 자체로 의미하지는 않습니다. 취약하지 않은 결과 해석을
참조하십시오.
# 단일 대상 (포트 기본값은 55555)
./cve_2026_28326_check.py arm.example.com
# 포트 명시
./cve_2026_28326_check.py arm.example.com:55555
# 여러 대상
./cve_2026_28326_check.py arm-a.example.com arm-b.example.com
# 목록 스캔, 한 줄에 대상 하나 ('#' 주석 허용), 간략 출력
./cve_2026_28326_check.py -f targets.txt --brief
# 파이프라인용 기계 판독 가능 출력
./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 메시지 본문의 직렬화된
객체에 의해 트리거됩니다. 두 프로브 모두 길이가 0인 본문으로 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 필드로 스캔하며 허프만 또는 동적 테이블
디코딩을 구현하지 않습니다. ARM이 사용하는 gRPC의 C 코어는 이 트레일러를 압축하지 않고
전송합니다. 향후 빌드가 이를 허프만 코딩하면 도구는 partial-response와 함께
INCONCLUSIVE를 보고합니다.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 | any | >= 2026.2.1.7 | PATCHED |
14 UNAVAILABLE | 14 UNAVAILABLE | either | 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) |