콘텐츠로 이동

SSHole: musl 기반 OpenSSH 인증 우회

SSHole: musl 기반 OpenSSH 인증 우회 thumbnail
CVE-2026-123158은 musl libc 환경에서 OpenSSH의 password authentication을 우회할 수 있는 취약점이다. shadow 필드가 *인 계정과 다른 계정의 modular crypt hash가 함께 존재하고, PAM을 사용하지 않는 OpenSSH 서버에서 password authentication을 허용할 때 발생한다.

요약

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 입력 흔적을 확인한다.

참고 자료