SSHole: musl 기반 OpenSSH 인증 우회


요약
CVE-2026-123158은 musl libc 환경에서 OpenSSH의 password authentication을 우회할 수 있는 취약점이다. shadow 필드가 *인 계정과 다른 계정의 modular crypt hash가 함께 존재하고, PAM을 사용하지 않는 OpenSSH 서버에서 password authentication을 허용할 때 발생한다.
공격자는 같은 SSH 연결에서 인증을 두 번 시도한 뒤, 두 번째 시도에 256바이트를 초과하는 비밀번호를 보낸다. musl의 crypt()가 실패하면서 *를 반환하면 OpenSSH가 이를 계정의 shadow 값과 비교해 인증을 허용할 수 있다. OpenSSH 업스트림 패치는 2026-11-05 공개됐다.
영향 조건
다음 조건이 함께 필요하다.
- musl libc 기반 시스템
- PAM을 사용하지 않는 OpenSSH 구성
PasswordAuthentication yes- 대상 계정의
shadow필드가* - 다른 계정에
$5$또는$6$형식의 password hash 존재 - SSH 서비스에 네트워크 접근 가능
| 항목 | 내용 |
|---|---|
| CVE | CVE-2026-123158 |
| 유형 | Password authentication bypass |
| CWE | CWE-394 |
| 공개일 | 2026-11-05 |
| CVSS | 8.1 High |
| 트리거 | 256바이트를 초과하는 비밀번호 |
| 영향 | shadow 값이 *인 계정으로 SSH 인증 가능 |
| 영향 범위 | musl 기반 OpenSSH 구성 |
| 패치 상태 | OpenSSH 업스트림 패치 공개 |
취약점 동작
취약한 경로는 sys_auth_passwd()와 xcrypt()의 조합에서 발생한다.
대상 계정의 shadow 값이 *이면 일반적인 password hash로 처리되지 않는다. 이때 OpenSSH는 다른 계정의 hash에서 salt를 선택해 crypt()를 호출한다. musl의 crypt()는 256바이트를 초과하는 비밀번호 처리에 실패할 때 *를 반환한다.
crypt(long_password, salt) -> "*"
strcmp("*", "*") -> 0
첫 번째 인증 시도에서는 pick_salt()가 다른 계정의 hash를 찾는 과정에서 musl의 static buffer를 덮어쓴다. 두 번째 시도에서는 캐시된 salt가 재사용되며, 대상 계정의 pw_password와 긴 비밀번호에 대한 crypt() 결과가 모두 *가 될 수 있다.
공개 재현 환경은 Alpine 컨테이너에서 victim 계정의 shadow 필드를 *로 설정하고, 다른 계정에 correcthorse password를 부여해 modular crypt hash를 만든다. root 계정 공격에는 PermitRootLogin yes가 추가로 필요하다.
코드 분석
출처: CVE-2026-123158 SSHole PoC
char *pw_password = shadow_pw(pw);
/* victim has shadow "*" */
if (authctxt->valid && pw_password[0] && pw_password[1])
salt = pw_password;
else
salt = NULL;
encrypted_password = xcrypt(password, salt);
return strcmp(encrypted_password, pw_password) == 0;
pw_password가 *이면 salt가 NULL로 전달된다. 이후 xcrypt()가 다른 계정의 hash를 이용해 salt를 선택하고, crypt() 결과를 대상 계정의 *와 직접 비교한다.
출처: CVE-2026-123158 SSHole 재현 코드
FROM alpine:3.15
RUN apk add --no-cache openssh-server \
&& ssh-keygen -A \
&& adduser -D alice && echo 'alice:correcthorse' | chpasswd \
&& adduser -D victim \
&& sed -i 's/^victim:[^:]*/victim:*/' /etc/shadow \
&& printf '%s\n' 'PasswordAuthentication yes' \
'PermitEmptyPasswords no' >> /etc/ssh/sshd_config
이 구성은 잠긴 victim 계정과 유효한 password hash를 가진 alice 계정을 함께 만든다. salt 선택 경로가 동작하려면 다른 계정의 $5$ 또는 $6$ hash가 필요하다.
패치 분석
출처: OpenSSH 업스트림 패치
+#define MIN_PASSWORD_HASH_LEN 4
#define MAX_PASSWORD_LEN 1024
@@
- char *pw_password = authctxt->valid ? shadow_pw(pw) : pw->pw_passwd;
+ char *pw_password = authctxt->valid ?
+ shadow_pw(pw) : xstrdup(pw->pw_passwd);
@@
- return encrypted_password != NULL &&
+ ret = encrypted_password != NULL &&
+ strlen(pw_password) >= MIN_PASSWORD_HASH_LEN &&
strcmp(encrypted_password, pw_password) == 0;
+ freezero(pw_password, strlen(pw_password));
+ return ret;
수정된 sys_auth_passwd()는 hash 문자열 길이가 4바이트 이상인지 먼저 확인한다. 따라서 *처럼 짧은 값이 crypt() 결과와 일치해도 인증이 성공하지 않는다. pw_password를 복사하고 인증 처리 후 freezero()로 정리하는 변경도 포함됐다.
탐지 포인트
musl 기반 OpenSSH와 관련 설정을 확인한다.
ldd "$(command -v sshd)" | grep -E 'musl|glibc'
sshd -T | grep -E 'passwordauthentication|usepam|permitrootlogin'
grep -E '^[^:]+:\*:' /etc/shadow
다음 조건이 함께 나타나면 점검 대상이다.
libc = musl
AND PasswordAuthentication = yes
AND UsePAM = no
AND shadow contains "*"
AND another account has "$5$" or "$6$" hash
root 계정을 대상으로 하려면 PermitRootLogin yes가 추가로 필요하다. PasswordAuthentication no, UsePAM yes, glibc 기반 시스템에서는 이 경로가 성립하지 않는다.
대응
- OpenSSH 배포판 패치 또는 업스트림 수정이 포함된 버전으로 업데이트한다.
- 패치 전에는
PasswordAuthentication no를 적용하고 public key authentication으로 전환한다. - Alpine 및 musl 기반 이미지와 VM의 OpenSSH 패키지 버전을 식별한다.
shadow필드가*인 계정과PermitRootLogin설정을 함께 점검한다.- 장시간 실행 중인 컨테이너는 패치된 이미지로 재빌드하고 교체한다.
- SSH 로그에서 같은 연결의 반복 인증 뒤 성공한 세션과 비정상적으로 긴 password 입력 흔적을 확인한다.
참고 자료
- OpenSSH 업스트림 패치
- Google Goonami detector 패치
- SSHole lab
- SSHole reproduction repository
- CVE-2026-123158 PoC repository