Blog / CVE-Erklärungen

Das Sternchen-Paradox: CVE-2026-123158 und die OpenSSH–musl-Authentifizierungsumgehung

Zwei voneinander unabhängige Systeme wählten beide das Zeichen "*", um "hier gibt es nichts Verwendbares" zu bedeuten. CVE-2026-123158 zeigt genau, wie diese Kollision die Passwort-Authentifizierung von OpenSSH auf musl-basiertem Linux umgeht, was ein Laborlauf mit fixiertem Image-Digest bestätigt hat und was nicht, und welche eine Frage offen blieb.

Promotionsstudium in Informationssicherheit abgeschlossenUniversitäre Verteidigung bestanden · Abschließende Dissertationsverteidigung steht noch aus

Ein Zeichen, zwei unabhängige Bedeutungen, eine Authentifizierungsumgehung

Ich habe zuerst ein falsches Passwort gesendet. Dann habe ich ein einziges, 300 Byte langes Passwort an ein Konto gesendet, für das überhaupt kein Passwort gesetzt war. sshd hat mich hereingelassen.

Zwei Systeme, die noch nie etwas voneinander gehört haben, haben das möglich gemacht, indem beide dasselbe Zeichen gewählt haben, um "hier gibt es nichts Verwendbares" zu bedeuten. /etc/shadow verwendet seit Jahrzehnten ein bloßes Sternchen, um ein Konto zu kennzeichnen, das niemals ein Passwort akzeptieren sollte. musls crypt()-Funktion gibt bei bestimmten internen Fehlern genau dasselbe Zeichen zurück statt des Null-Werts, den POSIX tatsächlich vorschreibt. Keine der beiden Entscheidungen ist für sich genommen falsch. Setzt man sie aber in denselben String-Vergleich, kann sshd vorgetäuscht werden, ein Passwort habe übereingestimmt, obwohl überhaupt nichts verglichen wurde.

Dieser erste Absatz ist zugleich der eine Datenpunkt, den dieser Artikel sich nicht überbewerten lässt: Ein einziges überlanges Passwort, gesendet als erster und einziger Versuch einer neuen Verbindung gegen dasselbe Konto, ohne vorher ein falsches Passwort zu senden, hat im selben Laborlauf nicht authentifiziert. Eine Abfolge hat funktioniert. Die andere nicht. Das ist ein Datenpunkt aus einem Lauf, keine Behauptung, dass jeder frische Passwortversuch gegen ein verwundbares Konto gelingt. Der Unterschied zwischen diesen beiden Ergebnissen und warum er auftrat, wird später direkt und ehrlich behandelt, einschließlich der Teile, die tatsächlich noch ungeklärt sind.

Dieser Artikel geht zunächst den Hintergrund durch: was SSH-Authentifizierung tatsächlich tut, was /etc/shadow speichert, was PAM ist und warum ein Build ohne PAM hier eine Rolle spielt, warum musl nicht glibc ist, und was crypt() bei einem Fehler eigentlich tun soll. Erst wenn das steht, ergeben der Ursachenmechanismus, die Laborbeweise, der Patch und die offenen Fragen wirklich Sinn, ohne dass der Leser irgendetwas einfach glauben muss.

Wer eine musl-basierte Linux-Maschine mit aktivierter Passwort-SSH-Authentifizierung und ohne PAM administriert, für den lautet die Kurzfassung: die Shadow-Datei auf ein bloßes Sternchen prüfen, prüfen, ob irgendetwas anderes auf der Maschine einen $5$- oder $6$-Hash hat, und patchen. Die Langfassung, einschließlich der genauen Erklärung, warum das funktioniert, und dessen, was ein praktischer Laborlauf tatsächlich bestätigt hat und was nicht, ist der Rest dieses Artikels.

Eine Reaktionszeit von einem Tag, und das Advisory-Detail, das die meiste Berichterstattung weglässt

OpenSSH 10.6p2 erschien am 5. November 2026 mit einem einzigen Sicherheitspunkt, und zwar schnell. Damien Millers eigene Release-Ankündigung besagt, dass der Fix einen Tag nach Kenntnisnahme des OpenSSH-Teams herauskam, und argumentiert, dass jüngste Fälle, in denen ähnliche Fehler innerhalb von Stunden unabhängig von KI-Agenten wiederentdeckt wurden, nun bedeuten, dass ein langes Embargo größtenteils Angreifern statt Verteidigern zugutekommt. Das ist die genannte Begründung für den komprimierten Zeitplan, keine Annahme, die dieser Artikel im Namen des Teams trifft. [2]

OpenSSHs Release Notes beschreiben, dass sshd nun "eine Passwort-Authentifizierungsumgehung vermeidet, die einige Linux-Systeme betrifft, die ohne PAM-Unterstützung gebaut und betrieben werden" (PAM, Pluggable Authentication Modules, ist die Schicht, die die meisten Linux-Systeme nutzen, um zu entscheiden, ob ein Login gelingt; einige Abschnitte weiter unten behandeln das ausführlich), und schreiben die Meldung Ozgun Kultekin von Wakefield zu. [3] Der ausführliche technische Write-up des Melders selbst liegt auf sshhole.com unter dem Titel "*sshole" und nennt unabhängig dasselbe 257-1024-Byte-Fenster und dieselbe Zeichenkollision, die dieser Artikel weiter unten vollständig nachzeichnet. [4] Das GitHub-Handle der Seite, oz9un, ist mit dem genannten Namen vereinbar, obwohl nichts in den Unterlagen diese Verbindung explizit bestätigt; das trägt hier keine technische Aussage, ist aber wert, klar benannt statt behauptet zu werden.

Alpine Linux’ eigenes Advisory ist die zweite primäre Quelle, auf die sich dieser gesamte Artikel stützt. Es beschreibt den verwundbaren Kontozustand präzise: ein Shadow-Feld, das wörtlich "*" ist, und vermerkt ausdrücklich, dass ein gesperrtes Konto, dessen Feld "!" ist, nicht betroffen ist. Es formuliert die allgemeine Voraussetzung als die Notwendigkeit mindestens eines weiteren lokalen Kontos mit einem echten Passwort-Hash auf derselben Maschine -- Alpines Standardformat SHA-256/SHA-512, in der Tabelle weiter unten gezeigt -- und sagt, dass ein solches Konto ausreicht, um jedes passwortlose Konto erreichbar zu machen, und beschreibt den Angriff selbst als eine einzige Verbindung erfordernd und ohne Wiederholungsversuche -- eine Formulierung, auf die dieser Artikel später direkt zurückkommt. [5]

CVE-2026-123158 selbst trägt einen CVSS-3.1-Basiswert von 8.1 (Hoch), Vektor AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H, und CWE-394, "Unexpected Status Code or Return Value", was die richtige Klassifizierung für einen Fehler ist, bei dem es im Kern darum geht, einem fehlersignalisierenden Wert zu vertrauen, ohne ihn zu prüfen. CISAs eigene Anreicherung desselben Eintrags bewertet die Exploitation Maturity separat als "poc" und die Automatisierbarkeit als "no". Eine Ausnutzung in freier Wildbahn wurde zum Zeitpunkt dieses Schreibens nicht gemeldet. [1]

Die meiste Sekundärberichterstattung zu dieser CVE verflacht sie zu "ein überlanges Passwort umgeht SSH". Das Advisory selbst ist in drei Punkten präziser, die fast die gesamte Berichterstattung weglässt: Das Konto muss sich genau im Zustand "*" befinden, nicht "!"; etwas anderes auf derselben Maschine muss bereits einen verwendbaren Hash tragen; und das Byte-Fenster ist 257 bis 1024, nicht allgemein "lang". Der Rest dieses Artikels ist genau um diese drei Details aufgebaut, plus das eine, das das Advisory zuversichtlich formuliert, das ein praktischer Laborlauf aber Grund hat zu prüfen statt es einfach zu glauben.

Was tatsächlich passiert, wenn Sie ein SSH-Passwort eingeben

SSH existiert, damit eine Maschine eine andere über ein Netzwerk administrieren kann, dem keine der beiden Seiten notwendigerweise vertraut -- verschlüsselt und, im Idealfall, mit einem Nachweis, dass beide Seiten tatsächlich sind, wer sie zu sein behaupten. Ein Client verbindet sich mit sshd, dem serverseitigen Daemon; beide handeln zunächst einen sicheren Kanal aus, und erst wenn dieser Kanal existiert, findet die Authentifizierung statt -- per öffentlichem Schlüssel, per Passwort oder einer anderen konfigurierten Methode -- bevor überhaupt eine Shell oder ein Befehl läuft. Die Passwort-Authentifizierung, die Methode, um die es in dieser gesamten CVE geht, ist ein bestimmter Schritt innerhalb dieses größeren Handshakes, kein separater, daneben angeflanschter Mechanismus.

Auf einem Linux-System liegen Kontoinformationen in /etc/passwd, eine Zeile pro Konto: Benutzername, numerische Benutzer-ID, Home-Verzeichnis, Shell. Das Passwort selbst, genauer ein Hash davon, lag vor Jahrzehnten ebenfalls dort. Heute liegt es in einer separaten Datei, /etc/shadow, nur für root lesbar -- gerade damit die Kompromittierung eines gewöhnlichen Benutzerkontos nicht gleich alle Passwort-Hashes auf der Maschine preisgibt.

Dieses Shadow-Feld ist allerdings nicht immer ein Hash. Es trägt eine kleine Zahl spezieller Werte, die Administratoren, Tools und sshd selbst alle korrekt interpretieren müssen, unten zusammengefasst.

Per Design sollte ein mit einem bloßen Sternchen markiertes Konto bei keiner Eingabe über ein Passwort authentifizierbar sein, weil "*" überhaupt kein Hash-Format ist. Es ist eine Konvention: Dieses Konto existiert, aber kein Passwort wird es je entsperren. Genau auf diese Konvention verlassen sich Administratoren bei reinen Schlüssel-Konten, Dienstidentitäten und Container-Basis-Images, die ein root-Konto mitbringen, bei dem niemand ein Passwort-Login erwartet. Die gesamte Schwachstelle, die dieser Artikel nachzeichnet, ist ein Fall, in dem diese Konvention leise aufgehört hat zu gelten.

Was jeder Wert des Passwortfelds in /etc/shadow bedeutet, und ob er über diesen Bypass erreichbar ist.
PasswortfeldBedeutungBetroffen
$1$... / $2a$/$2b$/$2y$... / $5$... / $6$...crypt() läuft hier normal und erzeugt einen echten Hash zum Vergleich.Ein echter Passwort-Hash (MD5, bcrypt, SHA-256 oder SHA-512)Nein
! oder !...Alpines Advisory nennt das ausdrücklich.Konto gesperrtNein
*Unter den in diesem Artikel genau nachvollzogenen Bedingungen.Kein nutzbares Passwort gesetzt (Konto nur mit Schlüssel)Ja
(leerer String)Wird durch einen separaten, früheren Zweig in sys_auth_passwd() behandelt, vor dem Pfad, den diese CVE betrifft.Kein Passwort erforderlichNein

PAM, und warum ein sshd-Build ohne PAM alles ändert

PAM, Pluggable Authentication Modules, ist die Schicht, die die meisten Linux-Distributionen nutzen, um zu entscheiden, ob ein Anmeldeversuch, SSH eingeschlossen, tatsächlich gelingt. Eine Anwendung wie sshd implementiert die Passwortprüfung normalerweise nicht selbst; sie fragt PAM, und PAM führt aus, welche Module der Administrator für diesen Dienst konfiguriert hat -- pam_unix für lokale Passwörter im üblichen Fall, mit weiteren Modulen für Verzeichnisdienste oder Mehrfaktor-Schemata verfügbar. Diese Indirektion ist der Standard auf glibc-basierten Distributionen wie Debian, Ubuntu, Fedora und RHEL: Auf diesen Systemen ruft sshd überhaupt nie eine Passwort-Hashing-Funktion direkt auf. [2]

Alpine Linux und die meisten musl-basierten Embedded-Builds (OpenWrt, Buildroot, Yocto) kompilieren OpenSSH statt dessen mit --without-pam. Auf diesen Systemen gibt es keine PAM-Schicht, an die delegiert werden könnte. sshd fällt auf seinen eigenen generischen Vergleichscode in auth-passwd.c zurück und ruft crypt(3) direkt gegen den eigenen Shadow-Eintrag des Kontos auf. Diese eine Entscheidung zur Build-Zeit ist die Weggabelung, auf deren einem Zweig diese CVE vollständig lebt: Ein PAM-aktiviertes System erreicht den verwundbaren Vergleich überhaupt nie, nicht weil eine bestimmte Verteidigung es verhindert -- dieser Codepfad läuft auf diesem Build schlicht nie. [2]

Noch ein Detail, das man vor den nächsten beiden Abschnitten wissen sollte, weil der Abschnitt zur Grundursache direkt darauf aufbaut: Für einen Benutzernamen, den sshd für ungültig hält, führt der Non-PAM-Pfad trotzdem einen Vergleich aus -- gegen ein zwischengespeichertes Fake-Passwort --, damit das Ablehnen eines nicht existierenden Kontos ungefähr genauso lange dauert wie das Ablehnen eines falschen Passworts bei einem echten. Das ist eine bewusste, seit langem bestehende Timing-Verteidigung gegen Username-Enumeration, und sie teilt sich genau den Salt-Auswahlcode, der ein "*"-Konto in den falschen Zweig leitet. "Salt-Auswahlcode" bedeutet hier einfach die Logik, die entscheidet, gegen welchen Referenzwert crypt() ein Passwort prüft; was dieser Referenzwert genau ist und wie er fehlgeht, greifen die Mechanismus-Abschnitte weiter unten wieder auf.

Ein gemeinsamer erster Schritt, sshd empfängt eine Anfrage zur Passwort-Authentifizierung, dann zwei Zweige: ein PAM-aktivierter Build, der an den PAM-Stack delegiert und crypt() niemals direkt aufruft, und ein Non-PAM-Build, der eigenen Vergleichscode ausführt und in einem crypt()-Vergleich mit dem gespeicherten Hash endet, markiert als der eine gefährliche Schritt.

01sshd empfängt eine Anfrage zur Passwort-Authentifizierung
PAM-aktivierter Buildglibc-Distributionen: Debian, Ubuntu, Fedora, RHEL
02An den PAM-Stack delegiertpam_unix oder ein ähnliches Modul übernimmt das
03PAM akzeptiert oder lehnt absshd ruft crypt() nie direkt auf
Non-PAM-Build--without-pam: Alpine, musl-basierte Embedded-Builds
02auth-passwd.c führt den eigenen Vergleich von sshd auses existiert keine PAM-Schicht, an die delegiert werden könnte
03crypt()-Vergleich mit dem gespeicherten Hashgenau der Pfad, in dem diese CVE lebt
Der Passwortprüfungscode von sshd verzweigt sich bereits zur Build-Zeit vollständig: Ein PAM-aktivierter Build überlässt die Entscheidung PAM, während ein Non-PAM-Build seinen eigenen Vergleich direkt ausführt, bis hinunter zu einem einzigen konkreten verwundbaren Schritt.

musl, glibc, und warum dieser Fehler Debian nicht betrifft

libc, die C-Standardbibliothek, ist die Schicht zwischen einem Programm und dem darunterliegenden Betriebssystemkern: Speicherzuweisung, String-Verarbeitung und, für diese Geschichte, crypt(). Jedes in C geschriebene Linux-Programm linkt gegen eine solche Bibliothek. Die meisten Desktop- und Server-Distributionen nutzen glibc, die GNU C Library. Alpine Linux nutzt stattdessen musl, eine kleinere, unabhängig geschriebene Implementierung derselben Menge standardisierter Schnittstellen.

musl und glibc sind auf Implementierungsebene nicht in allem, was sie bereitstellen, austauschbar. Sie implementieren dieselben Standardschnittstellen, crypt() eingeschlossen, aber nicht immer mit identischem internem Verhalten hinter dieser Schnittstelle. Diese Lücke ist für aufrufenden Anwendungscode normalerweise unsichtbar. Hier hörte sie auf, unsichtbar zu sein.

Alpine liefert OpenSSH gebaut gegen musl. OpenSSH ruft die Standardschnittstelle crypt(3) genau so auf, wie es das bei jeder anderen libc täte. musls eigene Implementierung dieser Schnittstelle, speziell ihre SHA-256- und SHA-512-Codepfade, ist der Ort, an dem das spezifische Fehlverhalten, das die nächsten drei Abschnitte nachzeichnen, tatsächlich lebt -- nicht in OpenSSHs eigenem Code und nicht in Alpines Paketierung davon.

Hier lohnt sich Präzision, weil man es leicht überbewertet: Die Schwachstelle braucht musl ganz konkret, nicht einfach nur "ohne PAM gebaut". OpenSSHs Non-PAM-Vergleichscode existiert in jedem so kompilierten Build, hypothetisch eingeschlossen ein nicht standardmäßiger glibc-Build. Aber das Fehlverhalten, mit dem diese CVE kollidiert -- crypt(), das statt NULL einen einzelnen Zeichen-Sentinel zurückgibt -- ist musls eigene Implementierungsentscheidung. Diese Untersuchung fand kein gleichwertiges Verhalten in glibc, und OpenSSHs eigene Release-Ankündigung stellt ausdrücklich klar, dass die betroffene Population Linux, musl und kein PAM ist -- nicht Nicht-Linux, nicht glibc, nicht PAM-nutzende Systeme. [2]

Ein fünfschichtiger Stack, von oben nach unten: Alpine Linux als Distribution, ein ohne PAM gebautes OpenSSH-sshd, die Bibliotheksschnittstelle crypt(3), musl libcs eigene crypt()-Implementierung, und ganz unten ihr algorithmusspezifischer Hashing-Code, jede Schicht verbunden durch den konkreten Aufruf oder die Konvention, die dazwischen benannt ist. Keine Schicht ist als gefährlich markiert: Diese Untersuchung hat das Fehlverhalten auf Ebene von musls eigenem crypt_sha256.c/crypt_sha512.c-Quellcode nicht eigenständig verifiziert.

Alpine Linux (die Distribution)Liefert OpenSSH gebaut gegen musl, nicht glibc
apk-Paketbau: OpenSSH gegen musl kompiliert
OpenSSH-sshd, gebaut mit --without-pam
C-Bibliotheksaufruf: crypt(attacker_password, stored_hash)
libc-Schnittstelle: crypt(3)
Dünner Dispatcher: statischer Puffer, __crypt_r()
musl libcs eigene crypt()-Implementierung
Dispatch nach Salt-Präfix ($6$, $5$, ...)
Algorithmusspezifischer Hashing-Code: crypt_sha256.c / crypt_sha512.c
Wo musl zwischen dem Authentifizierungscode von OpenSSH und der eigentlichen Passwort-Hashing-Implementierung sitzt.

Was crypt() bei einem Fehler eigentlich tun soll

crypt(3) ist die traditionelle Unix-Schnittstelle, um jenen Einweg-Hash zu berechnen und zu prüfen -- keine umkehrbare Verschlüsselung --, den gespeicherten Shadow-Wert, dem Sie bereits vor zwei Abschnitten begegnet sind. crypt(password, salt) berechnet den Hash, und dieselbe Funktion wird wiederverwendet, um einen Anmeldeversuch zu verifizieren, indem der Hash aus dem versuchten Passwort und dem eigenen gespeicherten Hash des Kontos, als Salt wiederverwendet, neu berechnet wird, und die beiden Ergebnisse dann mit einem einfachen String-Vergleich verglichen werden. Ein Salt existiert, damit zwei Konten mit demselben Passwort nicht auch mit demselben gespeicherten Hash enden.

POSIX legt fest, was crypt() tun soll, wenn bei dieser Berechnung intern etwas schiefgeht: einen Null-Zeiger zurückgeben und errno setzen, um den Grund anzuzeigen. Ein Aufrufer, der auf NULL prüft, weiß, dass die Berechnung fehlgeschlagen ist, und sollte das Ergebnis niemals als gültigen Hash behandeln, um ihn mit irgendetwas anderem zu vergleichen.

musls crypt() folgt diesem Vertrag nicht immer. Was sie statt dessen tut, und warum genau diese eine Abweichung mit /etc/shadows eigenen Konventionen aus dem vorigen Abschnitt kollidiert, ist der Mechanismus, den der Rest dieses Artikels ab jetzt auf Quellcode-Ebene nachzeichnet.

Warum ein gültiges Konto in den Fallback-Salt-Pfad geleitet wird

Hier ist das erste nicht offensichtliche Detail, und es ist ein Logikfehler um ein einziges Zeichen, kein dramatischer. OpenSSHs eigene Salt-Auswahlprüfung in sys_auth_passwd() fragt nicht, ob der gespeicherte Hash eines Kontos der Platzhalter "*" ist. Sie fragt, ob dieser Hash mindestens zwei Zeichen lang ist.

Die absichernde Bedingung lautet `if (authctxt->valid && pw_password[0] && pw_password[1]) salt = pw_password; else salt = NULL;`. Für den einzeichigen String "*" ist pw_password[0] das Sternchen selbst, was wahr (truthy) ist, aber pw_password[1] ist das abschließende NUL-Zeichen des Strings, was in C falsch (falsy) ist. Ein wahres Zeichen. Ein falscher Terminator. Das ist die gesamte Bedingung, und sie ist für diesen Fall bereits falsch. Sie wird zu false ausgewertet, obwohl authctxt->valid wahr ist -- also sogar für ein völlig reales, existierendes Konto. Der Code fällt in denselben salt = NULL-Zweig, der für einen gar nicht existierenden Benutzernamen verwendet wird, nicht weil irgendetwas konkret auf "*" prüft, sondern als Nebeneffekt einer Längenprüfung, die zufällig wie "ist das lang genug, um plausibel ein Hash zu sein" zu lesen ist. [6]

Wenn salt NULL ist, ruft xcrypt() pick_salt() auf, unten vollständig wiedergegeben mit erhaltenem Original-BSD-Lizenzhinweis. pick_salt() hält einen statischen, 32 Byte großen, auf einen Prozess begrenzten Puffer und gibt ihn sofort zurück, sobald er bereits befüllt ist. Beim ersten Aufruf pro Prozess durchläuft sie jedes lokale Konto via setpwent()/getpwent() und kopiert das algorithmusidentifizierende Präfix des ersten gefundenen Hashs, der mit "$" beginnt und ein zweites "$" enthält -- also jeden Hash im Modular-Crypt-Format: MD5 ($1$), bcrypt ($2a$/$2b$/$2y$), SHA-256 ($5$) oder SHA-512 ($6$), nicht spezifisch einen davon. Wenn nichts im System einen verwendbaren Hash hat, fällt sie auf das traditionelle DES-artige Salt "xx" zurück. [7]

Alpines Advisory formuliert die Voraussetzung als die Notwendigkeit eines weiteren Kontos spezifisch mit einem $5$- oder $6$-Hash. Der Code selbst ist breiter als das: Er akzeptiert den ersten Hash im Modular-Crypt-Format in welcher Reihenfolge getpwent() auch immer die Konten aufzählt, was auch MD5 und bcrypt abdeckt. Diese Lücke zwischen der Formulierung des Advisorys und dem tatsächlichen Verhalten des Codes ist diese Untersuchung eigene Lesart, nichts, was eine der beiden Quellen ausdrücklich feststellt, aber es ist erwähnenswert: Eine Maschine, die anderswo nur MD5- oder bcrypt-Hashes verwendet, würde die reale Voraussetzung trotzdem erfüllen, auch wenn Alpines eigene Formulierung etwas anderes nahelegen mag.

Noch eine Grenze, und sie ist OpenSSHs eigene, ohne Bezug zu musl: auth_password() weist jedes Passwort zurück, das länger als MAX_PASSWORD_LEN ist, definiert als 1024, bevor sys_auth_passwd() und damit crypt() überhaupt erreicht wird. Genau deshalb endet das Fenster des Advisorys bei 1024 Byte statt offen zu bleiben; ein längeres Passwort kommt dem verwundbaren Vergleich nie auch nur nahe. [8]

/* auth-passwd.c, sys_auth_passwd() -- pre-fix */
char *pw_password = authctxt->valid ?
    shadow_pw(pw) : xstrdup(pw->pw_passwd);
...
if (authctxt->valid && pw_password[0] && pw_password[1])
    salt = pw_password;
else
    salt = NULL;
encrypted_password = xcrypt(password, salt);
...
ret = strcmp(encrypted_password, pw_password) == 0;

/* openbsd-compat/xcrypt.c -- Copyright (c) 2003 Ben Lindstrom, BSD license */
static const char *
pick_salt(void)
{
    struct passwd *pw;
    char *passwd = NULL, *p;
    size_t typelen;
    static char salt[32];

    if (salt[0] != '\0')
        return salt;
    strlcpy(salt, "xx", sizeof(salt));
    setpwent();
    while ((pw = getpwent()) != NULL) {
        if (passwd != NULL)
            freezero(passwd, strlen(passwd));
        if ((passwd = shadow_pw(pw)) == NULL)
            continue;
        if (passwd[0] == '$' && (p = strrchr(passwd+1, '$')) != NULL) {
            typelen = p - passwd + 1;
            if (passwd[1] == '2' && strlen(p + 1) >= 22)
                typelen += 1 + 22;
            strlcpy(salt, passwd, MINIMUM(typelen, sizeof(salt)));
            explicit_bzero(passwd, strlen(passwd));
            goto out;
        }
    }
 out:
    if (passwd != NULL)
        freezero(passwd, strlen(passwd));
    endpwent();
    return salt;
}

Wo /etc/shadows "*" auf musls eigenes "*" trifft

An diesem Punkt im Code glaubt sshd eine ganz bestimmte Sache: Der String, den crypt() für ein echtes Passwort zurückgibt, kann niemals gleich dem eigenen Platzhalter-Hash des Kontos sein. Der Angreifer kontrolliert genau die eine Eingabe, die diesen Glauben zerbricht: wie lang sein Passwort ist.

Alpines Advisory beschreibt den verwundbaren Kontozustand unmissverständlich: ein Shadow-Passwortfeld, das wörtlich "*" ist -- die Konvention für ein Konto ohne verwendbaren Passwort-Hash, ein reines Schlüssel-Konto. Es ist ausdrücklich, dass ein gesperrtes Konto, Feld "!", nicht betroffen ist. [5]

musls crypt() gibt in ihren SHA-256- und SHA-512-Hashing-Pfaden den wörtlichen String "*" zurück -- eine String-Literal-Konstante im Nur-Lese-Speicher -- statt NULL, wenn die interne Hash-Verifikation fehlschlägt. Das ist genau der Vertragsbruch, den der vorige Abschnitt beschrieben hat, nun ausgelöst durch eine konkrete Bedingung: ein überlanges Eingabepasswort. Die genaue Pro-Algorithmus-Prüfung innerhalb von crypt_sha256.c und crypt_sha512.c wurde in dieser Untersuchung nicht eigenständig gelesen; was hier dokumentiert ist, beschränkt sich auf Alpines Advisory, den eigenen Write-up des meldenden Forschers und einen musl-Mailinglisten-Thread von Dezember 2024, der dasselbe "*"-bei-Fehler-Verhalten in einem völlig anderen Kontext beschreibt, fast zwei Jahre vor der Existenz dieser CVE. [5] [4] [9] musls eigene crypt.c ist ein dünner Dispatcher und führt selbst keine dieser Prüfungen durch, bestätigt durch direkte Inspektion der Datei. [10]

Der Fehler, der "*" erzeugt, wird durch ein überlanges Eingabepasswort ausgelöst. Alpines Advisory und der CVE-Eintrag geben beide dasselbe Fenster an: 257 bis 1024 Byte. Die untere Grenze ist musls eigene interne Schwelle; die obere Grenze ist, wie vor zwei Abschnitten behandelt, OpenSSHs bereits bestehende MAX_PASSWORD_LEN-Obergrenze, die mit musl überhaupt nichts zu tun hat. [5] [1]

Zusammengesetzt: Ein Angreifer, der ein Passwort in diesem Byte-Bereich gegen ein Konto sendet, dessen gespeicherter Hash "*" ist, bringt crypt() dazu, "*" zu berechnen und zurückzugeben. Der darauf folgende Vergleich ist `strcmp("*", "*")`, der per Definition wahr ist, weil beide Seiten nun exakt derselbe einzeichige String sind. Die Authentifizierung gelingt, für ein Konto, das ausdrücklich so konfiguriert ist, niemals ein Passwort zu akzeptieren.

Keine der beiden Hälften sieht isoliert betrachtet falsch aus, und genau deshalb ist es so leicht zu übersehen. Typsysteme helfen hier nicht: Beide Seiten des Vergleichs sind char*, und "*" ist auf beiden Seiten -- beim Shadow-Lesen und bei crypt() -- ein syntaktisch gewöhnlicher, erfolgreich zurückgegebener String. Für einen statischen Analyzer sieht hier nichts wie eine Fehlerbedingung aus. Und PAM hätte dieses Verhalten durch Zufall abgefangen, über einen schlimmeren Fehler: Auf einem PAM-aktivierten System lässt derselbe musl-Rückgabewert statt dessen pam_unix abstürzen, weil pam_overwrite_string() versucht, in dieses Nur-Lese-String-Literal zu schreiben -- ein Denial of Service statt eines Bypasses, und ein Problem, das bereits unabhängig im Dezember 2024 gemeldet wurde. Non-PAM-Systeme haben keinen äquivalenten zwischengeschalteten Aufruf, sodass dasselbe zugrundeliegende musl-Verhalten einen stillen Authentifizierungserfolg statt eines Absturzes erzeugt. [9]

/* musl libc, src/crypt/crypt.c -- MIT license */
char *crypt(const char *key, const char *salt)
{
    static char buf[128];
    return __crypt_r(key, salt, (struct crypt_data *)buf);
}
/* a thin dispatcher only -- the overlong-key / "*"-on-failure behavior
   lives inside crypt_sha256.c / crypt_sha512.c, not here. That specific
   file was not independently read in this investigation; see the text. */

Zwei Seiten, jede mit einem Sternchen-Glyphen markiert: links die Konvention von /etc/shadow, dass ein bloßes Sternchen bedeutet, dass niemals ein Passwort gültig ist; rechts der Fehler-Sentinel von musls crypt(), der bei bestimmten Fehlern ebenfalls genau dieses Sternchen zurückgibt. Beide Seiten laufen in einem hervorgehobenen Punkt zusammen, der "strcmp von Sternchen und Sternchen gleich null" lautet, mit dem Urteil, dass die Authentifizierung gelingt.

/etc/shadows eigene Konvention

Ein bloßes Sternchen bedeutet: Kein Passwort ist jemals gültig.

Der Fehler-Sentinel von musls crypt()

Gibt bei einem Fehler denselben Charakter statt NULL zurück.

strcmp("*", "*") == 0

TREFFER: AUTHENTIFIZIERUNG ERFOLGREICH

Zwei voneinander unabhängige Konventionen, beide mit einem einzigen Sternchen geschrieben, laufen in demselben Vergleich zusammen.

Was tatsächlich gebaut, fixiert und ausgeführt wurde

Alles in den nächsten beiden Abschnitten stammt aus einem tatsächlichen Lauf, kein Gedankenexperiment: ein digest-fixiertes Alpine-3.24.4-Image, gebaut aus einer modifizierten Kopie eines öffentlichen Proof-of-Concept-Repositorys, ausgeführt am 7. November 2026 und unabhängig gegen seine eigenen hash-dokumentierten Belege erneut verifiziert, bevor irgendetwas von dem Folgenden geschrieben wurde.

Das Basis-Image war alpine:3.24, fixiert auf dessen linux/amd64-Manifest-Digest, statt einem flottierenden Tag vertraut zu werden. Hier wurde nichts der Willkür eines flottierenden Tags überlassen. Das Labor-Image selbst wurde absichtlich mit OpenSSH 10.6p1 gebaut, kompiliert mit --without-pam: der verwundbaren Version vor dem Fix, nicht dem gepatchten 10.6p2. Die Versionen von musl, libcrypto3, shadow und zlib wurden direkt aus den eigenen Paketmetadaten des gebauten Images erfasst, nicht aus einem aktuellen Branch-Index angenähert.

Shadow-Einträge für root, alice, alpine und victim wurden auf "*" gesetzt; bob erhielt einen echten, deterministischen SHA-512-Hash, erzeugt mit einem festen Salt, das über eine Datei statt über ein Kommandozeilenargument eingespeist wurde, als bewusste Negativtest-Kontrolle. sshd_config setzte PasswordAuthentication auf yes und, bewusst gegenüber dem eigenen Standard des öffentlichen Proof of Concept geändert, PermitRootLogin auf no, da root niemals Teil des genehmigten Testumfangs dieses Labors war.

Die Isolation war strenger als Dockers eigene Standardeinstellungen, nicht lockerer: Der Container lief in einem als --internal konfigurierten Bridge-Netzwerk, ohne Standardroute nach außen, Privileged false, CapDrop ALL, und nur CAP_NET_BIND_SERVICE, CAP_SETGID, CAP_SETUID und CAP_SYS_CHROOT wurden zurückgegeben, jedes gegen OpenSSH 10.6p1s eigenen Privilege-Separation-Quellcode begründet statt angenommen, plus no-new-privileges. Kein Passwort und kein Secret erschien in der Kommandozeile irgendeines Prozesses; jede Fixture wurde aus einer Datei gelesen.

Nur alice und bob waren Teil des genehmigten Testplans. Was diese Grenze tatsächlich eingehalten hat und was nicht, ist ein eigener Abschnitt, direkt nach den Ergebnissen platziert statt leise in sie eingeflochten.

Base image     alpine:3.24, pinned to linux/amd64 digest
               sha256:0f49f331d9212aff11b932863be4eb01cdf962969
               aa86dab7d1ee21ac6f5f52b  (resolves to Alpine 3.24.4)
Lab image      sshole-lab:revision3
               sha256:b973cd33dfd98a767b93cd5f95987d817f7b8eeda0
               6e804e173b431ae6540e45
OpenSSH        10.6p1, built --without-pam (the pre-fix version,
               deliberately -- this run tests the vulnerable build)
musl           1.2.6-r3
Capabilities   --cap-drop=ALL
               --cap-add=NET_BIND_SERVICE --cap-add=SETUID
               --cap-add=SETGID --cap-add=SYS_CHROOT
               --security-opt=no-new-privileges
Network        sshole-lab-net, --internal (no default route out)
sshd_config    PasswordAuthentication yes
               PermitRootLogin no

Fall A scheiterte. Fall B gelang. Bob bewegte sich in keine Richtung.

Drei Hypothesen gingen in diesen Lauf ein, ausdrücklich als Dinge formuliert, die zu testen sind, nicht zu bestätigen. Wahr, falsch oder nicht eindeutig waren jeweils ein legitimes, berichtbares Ergebnis von vornherein.

Fall A testete ein einzelnes, nicht vorbereitetes überlanges Passwort: 300 Byte, gesendet als erste und einzige Passwort-Authentifizierungsanfrage einer neuen Verbindung gegen alice, deren Shadow-Feld "*" ist. Es gelang nicht. Das Server-Log zeigt eine Verbindung, eine "Failed password for alice"-Zeile, und das Schließen der Verbindung. Keine "Accepted"-Zeile erscheint irgendwo für diese Verbindung. Dies ist im Dossier als nicht reproduziert vermerkt.

Fall B testete dasselbe Passwort, vorbereitet: ein kurzes, absichtlich falsches Passwort wurde zuerst gesendet, unmittelbar gefolgt vom selben 300-Byte-überlangen Passwort, als zwei aufeinanderfolgende Authentifizierungsanfragen innerhalb einer SSH-Verbindung gegen alice. Es gelang. Das Server-Log zeigt "Failed password for alice ... port 33394 ssh2" unmittelbar gefolgt von "Accepted password for alice ... port 33394 ssh2", beide auf demselben Quellport, von Anfang bis Ende, eine einzige Verbindung. Das Client-Transkript bestätigte den erwarteten Marker der Befehlsausführung als alice. Dies ist als reproduziert vermerkt.

Die Fälle C1 und C2 verwendeten bob, die Kontrolle mit echtem Hash. C1, bobs tatsächliches Passwort, gelang als Sanity-Check der Positivkontrolle. C2, dasselbe identische 300-Byte-überlange Passwort, das gerade gegen alice funktioniert hatte, scheiterte gegen bob: "Accepted password for bob ... ssh2" für das korrekte Passwort, "Failed password for bob ... ssh2" für das überlange. Zusammen bestätigen diese, dass das überlange Passwort allein die Authentifizierung nicht allgemein umgeht; es passt nur zum degenerierten "*"-Platzhalter, genau wie der oben beschriebene Ursachenmechanismus vorhersagt.

Diese vier Ergebnisse sind ein Datenpunkt aus einem bestimmten, digest-fixierten Build, einmal ausgeführt, am 7. November 2026. Sie sind keine Behauptung darüber, wie sich CVE-2026-123158 bei jeder musl- und OpenSSH-Kombination überall verhält; sie sind, was genau dieses Image, genau dieser OpenSSH-10.6p1-Build und genau diese Eingaben in diesem einen Lauf tatsächlich getan haben. Das Dossier erfasst sie bewusst mit diesem angehängten Geltungsbereich, und dieser Artikel behält dieselbe Rahmung bei, statt sie zu verallgemeinern.

Case A -- single-shot, alice, no priming -- not reproduced
  Failed password for alice ... ssh2
  [connection closes; no Accepted line for this connection]

Case B -- primed (short wrong, then overlong, one connection), alice -- reproduced
  Failed password for alice ... port 33394 ssh2
  Accepted password for alice ... port 33394 ssh2

Case C1 -- bob, real password, positive control -- reproduced
  Accepted password for bob ... ssh2

Case C2 -- bob, same overlong password, negative control -- reproduced
  Failed password for bob ... ssh2

Eine Tabelle mit vier Testfällen aus einem Laborlauf. Fall A, ein einzelnes, nicht vorbereitetes überlanges Passwort gegen alice, protokollierte ein fehlgeschlagenes Passwort und eine geschlossene Verbindung: nicht reproduziert. Fall B, dasselbe Passwort, zuvor durch einen kurzen falschen Versuch vorbereitet, protokollierte ein fehlgeschlagenes Passwort, gefolgt von einem akzeptierten Passwort: reproduziert. Fall C1, bobs echtes Passwort, protokollierte als Baseline ein akzeptiertes Passwort. Fall C2, dasselbe überlange Passwort gegen bob, protokollierte als Negativkontrolle ein fehlgeschlagenes Passwort. Eine separat verfolgte offene Frage vermerkt, dass ungeklärt bleibt, warum die Vorbereitung in diesem Lauf auf Quellcode-Ebene eine Rolle spielte.

FallVersuchProtokolliertes ErgebnisLesart
Fall AEinzelversuch, ohne VorbereitungFailed password → Verbindung geschlossenNicht reproduziert
Fall BVorbereitet: falsch, dann überlangFailed password → Accepted passwordReproduziert
Fall C1Bobs echtes PasswortAccepted passwordReproduziert (Baseline)
Fall C2Dasselbe überlange Passwort gegen bobFailed passwordReproduziert (Negativkontrolle)
Offene Frage (C23)

Warum die Vorbereitung in diesem Lauf eine Rolle spielte, ist auf Quellcode-Ebene weiterhin ungeklärt: pick_salt() liefert bei jedem Aufruf synchron ein Salt zurück, und nichts in der eigenen Logik verlangt offensichtlich eine vorherige Vorbereitung.

Vier Laborergebnisse aus einem Lauf mit fixiertem Digest, in der eigenen Logsprache des Servers, nicht als Bestanden/Nicht-bestanden-Tabelle.

Was dieser Lauf nicht beweist, und die root-Versuche, die niemand offenlegte

Ein Laborlauf ist nur so vertrauenswürdig wie das, was er offen zugibt, nicht geprüft zu haben. Dieser gibt drei konkrete Dinge offen zu, statt einen sauberen Satz Ergebnisse innerhalb des Geltungsbereichs als Ersatz für einen sauberen Prozess stehen zu lassen.

Das erste ist ein Befund zum Geltungsbereich, kein Mechanismusbefund. Dieselbe Beweisdatei, die Fall As Einzelversuch gegen alice aufzeichnete, enthält auch, früher im selben Erfassungsfenster, eine nicht offengelegte Sequenz gegen root: eine "Failed none for root"-Zeile, dann zwei aufeinanderfolgende "Failed password for root ... ssh2"-Zeilen innerhalb einer Verbindung, dann das Schließen der Verbindung. Keine "Accepted"-Zeile für root erscheint irgendwo. Der übermittelte Wert hinter diesen beiden Versuchen ist unbekannt, und dieser Bericht wird nicht danach raten. root war nie Teil des genehmigten Geltungsbereichs dieses Labors -- nur alice und bob -- auf jeder Ebene der Genehmigung dieser Aufgabe, und die eigene Zusammenfassung des Laufbetreibers darüber, was getestet wurde, erwähnte es nie.

Nichts wurde kompromittiert: Beide Versuche gegen root schlugen fehl, und PermitRootLogin no wurde in der tatsächlichen, wirksamen Konfiguration des Daemons für diesen Lauf bestätigt. Eine Abweichung vom Geltungsbereich, die trotzdem scheitert, bleibt eine Abweichung vom Geltungsbereich. Genau deshalb ist der Laborstatus dieses Dossiers als "partial" statt "reproduced" vermerkt -- eine Unterscheidung, die es wert ist, beibehalten zu werden, selbst wo die Beweise innerhalb des Geltungsbereichs sonst sauber aussehen. Warum die Sequenz überhaupt stattfand -- ob eine breitere Standard-Benutzernamenliste, eingebaut in das Tooling, statt der im Plan vorgesehenen, geltungsbereichsbeschränkten benutzerdefinierten Befehle lief, oder etwas ganz anderes -- ist eine Frage, die derjenige, der das Labor betrieben hat, direkt beantworten muss, keine, über die hier spekuliert wird.

Die zweite Lücke ist eine Messbeschränkung, keine des Geltungsbereichs. Fall Bs Test führte eine gleichzeitige Prozess-ID-Polling-Schleife aus, speziell um direkte Beweise dafür zu erfassen, ob derselbe Betriebssystemprozess sowohl den Vorbereitungsversuch als auch den überlangen Versuch bediente -- Beweise, die direkt auf die offene Frage im nächsten Abschnitt einwirken würden. Sie sammelte während des Austauschs genau eine verwertbare Probe. Das ist mit Prozesskontinuität über beide Versuche hinweg vereinbar, kein durchgehender Vorher-Nachher-Vergleich über die Lücke dazwischen. Hinweisend. Nicht schlüssig.

Die dritte Lücke betrifft den eigenen Netzwerkpfad des Labors, nicht die Schwachstelle selbst. Der Plan sah die Veröffentlichung des Labors auf 127.0.0.1:8222 vor; der Laufbetreiber berichtet, dass dieses Binding keine funktionierende Host-Konnektivität lieferte, nachdem der Container auch an ein --internal-Netzwerk ohne Egress angeschlossen war, und verwendete statt dessen einen zweiten, wegwerfbaren Test-Client-Container im selben internen Netzwerk, um das Labor über seinen Container-DNS-Namen zu erreichen. Die eigene Inspect-Ausgabe des Containers zeigt weiterhin, dass das angeforderte Port-Binding in HostConfig.PortBindings akzeptiert wurde, während NetworkSettings.Ports derselben Datei, das aufzeichnet, was Docker zur Laufzeit tatsächlich etabliert hat statt was nur angefordert wurde, leer gelesen wird: `{"22/tcp": []}`. Das erhärtet den Bericht. Es beweist ihn nicht abschließend. Niemand in diesem Lauf hat tatsächlich versucht, von außerhalb des Containers eine Verbindung zu 127.0.0.1:8222 herzustellen, um zu bestätigen, dass es wirklich nicht live war.

Nichts davon ändert, was Fall A, B, C1 oder C2 gezeigt haben. Es ändert, wie viel Gewicht dieser eine Lauf als Präzedenzfall für den nächsten tragen kann, und die ehrliche Antwort ist: weniger, als ein vollständig geltungsbereichskonformer Lauf hätte, speziell beim root-Konto-Punkt.

Der Fix berührt musl nicht. Er fügt einem Vergleich eine Bedingung hinzu.

Der fixierende Commit, 4353fdc43ef646c43a8dda7025cc1a898caf1482, "avoid password authn bypass", verfasst von Damien Miller am 5. November 2026, berührt musl nicht, berührt pick_salt() nicht und berührt das Überlang-Passwort-Verhalten nicht, das all das auslöst. [11]

Er fügt `#define MIN_PASSWORD_HASH_LEN 4` neben dem bestehenden MAX_PASSWORD_LEN in auth-passwd.c hinzu und ändert die endgültige Authentifizierungsentscheidung so, dass zusätzlich zur unveränderten strcmp()-Prüfung `strlen(pw_password) >= MIN_PASSWORD_HASH_LEN` erforderlich ist, wobei pw_password der eigene gespeicherte Hash des Kontos ist, niemals die vom Angreifer beeinflusste Seite des Vergleichs. [11]

Da "*" und "!" beide exakt ein Zeichen lang sind, ist `strlen(pw_password) >= 4` für jedes Konto falsch, das dieser Fehler je hätte erreichen können, und das `&&` bricht ab, bevor strcmp() überhaupt läuft, unabhängig davon, was crypt() für eine beliebige Eingabe zurückgibt, lang oder kurz.

Dieselbe Änderung am Speicherbesitz -- eine Heap-Kopie via xstrdup() und freezero() statt eines Zeigers, den der Aufrufer nicht besitzt -- wurde über auth-passwd.c, openbsd-compat/port-uw.c (den UnixWare-spezifischen Äquivalenzpfad) und pick_salt() sowie shadow_pw() in xcrypt.c hinweg angewendet. Die neue Längenprüfung selbst landete jedoch nur in auth-passwd.cs generischem Pfad, nicht in port-uw.cs separatem Vergleich. UnixWare ist kein musl-Ziel, sodass diese spezifische Kollision über diesen Pfad nicht auftreten kann -- was der Grund sein könnte, warum die Prüfung dort nicht als notwendig galt; das ist diese Untersuchung eigene plausible Lesart, nichts, was der Commit ausdrücklich feststellt. [11]

Die Commit-Nachricht selbst besagt, dass der Bypass "einige Konfigurationen und Laufzeitkonfigurationen betrifft" und dass das Team "die Grundursache nicht als innerhalb von OpenSSH" ansieht, während es "gut ausgestattet ist, sie zu vermeiden". Diese Rahmung passt zu dem, was dieser Fix tatsächlich tut: Er blockiert genau die oben beschriebene Kollision -- ein einzeichiger Sentinel, der mit einem einzeichigen Shadow-Platzhalter kollidiert --, indem er dem gespeicherten Hash eine Mindestlänge setzt, statt zu validieren, dass der berechnete Hash für seinen behaupteten Algorithmus wohlgeformt ist. Würde musl, oder jede musl-libc-kompatible Software, gegen die verlinkt wird, jemals auf einem anderen Fehlerpfad eine andere kurze, nicht platzhalterartige Zeichenkette von vier oder mehr Zeichen zurückgeben, würde diese spezifische Prüfung das nicht abfangen. Diese Untersuchung fand keinen derzeit existierenden solchen Pfad; das ist eine Grenze dessen, was der Fix garantiert, kein bekannter Restfehler. [11]

/* auth-passwd.c, commit 4353fdc -- post-fix */
#define MIN_PASSWORD_HASH_LEN 4
...
ret = encrypted_password != NULL &&
    strlen(pw_password) >= MIN_PASSWORD_HASH_LEN &&
    strcmp(encrypted_password, pw_password) == 0;

Alpine hat nicht jeden Branch sofort auf 10.6p2 gehoben

Alpine hat nicht jeden unterstützten Branch auf OpenSSH 10.6p2 gehoben. Es hat den Fix in die jeweils bereits laufende Version jedes Branches zurückportiert, was gängige Distributionspraxis ist: die bestehende Paketversion des Branches beibehalten, den spezifischen Defekt patchen.

Alpines Security Tracker listet sieben branchspezifische, behobene Versionen statt einer einzigen flottenweiten Nummer. Jeder derzeit unterstützte Branch hat das Backport erhalten. [12]

Wer eine Flotte prüft, sollte den Branch mit der Tabelle unten abgleichen, statt irgendwo nach dem wörtlichen String "10.6p2" zu suchen. Eine 3.21-main-Maschine mit dem zurückportierten 9.9_p2-r1 ist gepatcht, auch wenn `sshd -V` niemals 10.6 ausgibt.

Alpines eigene, pro Branch fixierte, behobene OpenSSH-Paketversionen.
BranchBehobene Paketversion
edge-main10.6_p2-r0
3.24-main10.3_p1-r2
3.23-main10.2_p1-r1
3.22-main10.0_p1-r11
3.21-main9.9_p2-r1
3.20-main9.7_p1-r6
3.19-main9.6_p1-r3

Was Sie jetzt sofort in Ihren Logs prüfen sollten

Alpines eigenes Advisory gibt Erkennungs- und Milderungshinweise, die konkret genug sind, um noch heute Abend zu handeln, nicht die allgemeine Empfehlung, "Ihre Logs zu überprüfen".

Die Erkennungslogik lautet: Jedes Konto identifizieren, dessen Shadow-Passwortfeld exakt "*" ist, dann in den eigenen Logs des Daemons nach einem erfolgreichen Passwort-Login für genau diese Konten suchen -- der String "Accepted password for" ist der Marker, nach dem gesucht werden soll. Daneben doas-Nutzung, kürzlich angelegte Benutzer, neue Dateien und Änderungen an authorized_keys prüfen. Wenn ein Passwort-Login gegen ein passwortloses Konto während des Expositionsfensters nicht ausgeschlossen werden kann, ist die eigene Position des Advisorys, die Maschine als kompromittiert zu behandeln. [5]

Die Milderung ist entweder ein Paket-Upgrade gefolgt von einem Daemon-Neustart, da das alleinige Upgrade des Pakets den bereits laufenden Prozess nicht ersetzt, oder das vollständige Deaktivieren der Passwort-Authentifizierung als Übergangslösung, validiert, bevor der Neustart wirksam wird. [5]

Eine root-spezifische Nuance, die es wert ist, präzise formuliert zu werden: Alpines Standardeinstellung PermitRootLogin prohibit-password schützt root bereits vor diesem Bypass, sofern ein Administrator nicht ausdrücklich PermitRootLogin yes gesetzt hat, selbst wenn root selbst im Basis-Image als root:* ausgeliefert wird. Dieser Standard deckt jedoch nicht jeden Weg zu root ab: Jedes andere, passwort-login-fähige Konto, das eskalieren kann, etwa über doas, schafft weiterhin reale Exposition, selbst während roots eigener direkter Passwort-Login geschützt bleibt. [5]

# Package upgrade, then restart -- upgrading alone does not
# replace the already-running daemon
apk upgrade -U -a
rc-service sshd restart

# Or: disable password authentication entirely
#   PasswordAuthentication no   (in sshd_config)
sshd -t
rc-service sshd restart

# Detection: look for a successful password login on an account
# that should never have one
grep "Accepted password for" /var/log/messages
# then cross-reference each username against /etc/shadow for a
# password field that is exactly "*"

Warum scheiterte dasselbe Passwort beim ersten Versuch und funktionierte beim zweiten?

Warum scheiterte genau dasselbe überlange Passwort beim ersten Mal und gelang beim zweiten Mal, gegen dasselbe Konto, im selben Lauf?

Alpines eigenes Advisory besagt, dass der Angriff "eine Verbindung benötigt... und keine Wiederholungsversuche braucht". Beide unabhängig voneinander verfassten Proof-of-Concept-Skripte, die für dieses Dossier überprüft wurden -- jenes, das für den Laborbau bereitgestellt wurde, und ein separates, auf das direkt in den eigenen CNA-Referenzen des CVE-Eintrags verwiesen wird -- verwenden trotzdem zwei Passwort-Authentifizierungsversuche innerhalb einer Verbindung: zunächst ein kurzes falsches Passwort, dann das überlange, und beide führen das in ihren eigenen Codekommentaren auf das Vorbereiten eines zwischengespeicherten Salts vor dem eigentlichen Versuch zurück. Das ist nicht notwendigerweise ein Widerspruch. Beide nutzen trotzdem einen einzigen SSH-Transport, und "keine Wiederholungsversuche" beschreibt am plausibelsten, dass der Angriff deterministisch ist und kein wiederholtes probabilistisches Raten benötigt, statt zu behaupten, dass genau eine Authentifizierungsnachricht ausreicht. Aber es klärt die Frage auch nicht in die andere Richtung. [5] [13] [14]

Auch das Nachvollziehen von pick_salt()s eigenem Code löst das nicht auf. Die Funktion sucht und liefert bei ihrer ersten Ausführung synchron, innerhalb desselben Aufrufs, ein verwendbares Salt zurück. Nichts in dieser Logik erfordert offensichtlich, dass bereits ein früherer Aufruf stattgefunden haben muss, bevor ein langes Passwort gegen ein frisch gefundenes Salt abgeglichen werden kann. Nach der eigenen Logik des Codes sollte ein einzelnes, nicht vorbereitetes überlanges Passwort, gesendet bei der allerersten Authentifizierungsanfrage einer Verbindung, ebenfalls pick_salt() erreichen, ein Salt zurückbekommen und gegen dieses durch crypt() laufen -- genau wie es der zweite Versuch in beiden bekannten Proofs of Concept tut.

Was der Laborlauf hinzugefügt hat, ist real, und es ist genau ein Datenpunkt: Einzelversuch scheiterte (Fall A), vorbereiteter gelang (Fall B), in diesem einen konkreten Build. Das ist hash-gestützter Beweis, dass die Vorbereitung in diesem einen Lauf eine Rolle spielte. Es ist keine Erklärung, warum. Das obige pick_salt()-Rätsel bleibt von diesem Ergebnis unberührt. "Vorbereitet funktioniert, Einzelversuch nicht" ist mit mehreren Erklärungen vereinbar, die diese Untersuchung nicht eigenständig bestätigt hat: ein Detail der Lebensdauer von OpenSSHs eigenen Privilege-Separation-Prozessen (sshd teilt die Authentifizierungsarbeit jeder Verbindung zwischen einem privilegierten Monitor-Prozess und einem unprivilegierten Worker-Prozess auf; diese Untersuchung hat nicht nachvollzogen, wie lange jeder von beiden tatsächlich lebt), speziell, welcher dieser beiden Prozesse pick_salt() ausführt und ob seine Lebensdauer einen einzelnen Authentifizierungsversuch oder die gesamte Verbindung umspannt; irgendein anderes zustandsbehaftetes Verhalten, das weder die Autoren der Proofs of Concept noch diese Untersuchung identifiziert haben; oder etwas, das spezifisch für genau diesen Build und dieses Timing ist und sich möglicherweise überhaupt nicht verallgemeinern lässt.

Also: Diese Frage ist nach diesem Laborlauf auf Ebene des Quellcode-Mechanismus weiterhin offen, genau wie davor. Behandeln Sie die Aufteilung "vorbereiteter Erfolg, Einzelversuch-Scheitern" als einen Datenpunkt, der es wert ist, ernst genommen zu werden, nicht als eine in irgendeine Richtung geklärte Erklärung. Der nächste nützliche Schritt, sollte jemand das weiterverfolgen, ist, OpenSSHs genaues Privilege-Separation-Prozessmodell für diesen spezifischen Build nachzuvollziehen, oder den Einzelversuchsfall unter anderem Timing erneut auszuführen, um zu sehen, ob das Ergebnis Bestand hat.

Zwei kleinere offene Fäden, benannt statt erraten: musls eigener Quellcode für die genaue Überlang-Schlüssel-Prüfung innerhalb von crypt_sha256.c und crypt_sha512.c wurde in dieser Untersuchung nie eigenständig gelesen, nur durch das Advisory und den Mailinglisten-Bericht von 2024 beschrieben. Und ob das GitHub-Handle hinter dem Write-up auf sshhole.com dieselbe Person ist, der OpenSSHs Release Notes namentlich Anerkennung zuschreiben, ist plausibel, angesichts des übereinstimmenden technischen Inhalts, aber durch nichts in den Unterlagen bestätigt. Keines der beiden trägt irgendeine hier gemachte Aussage.

Die Lektion betrifft Schnittstellenverträge, nicht musl oder OpenSSH

Patchen Sie musl, wenn es einen Fix liefert. Patchen Sie OpenSSH, das bereits einen hat. Keine der beiden Anweisungen ist hier die eigentliche technische Lektion.

Der Schnittstellenvertrag von crypt() verspricht NULL bei einem Fehler. musls eigene Implementierung dieses Vertrags hält dieses Versprechen nicht immer, und das Versprechen, auf das sich /etc/shadows "*"-Konvention separat verlässt -- dass dieses bestimmte Zeichen niemals als echtes crypt()-Ergebnis auftaucht --, erwies sich als vollständig davon abhängig, dass das erste Versprechen Bestand hat. Kein Teilsystem ist für sich genommen falsch: /etc/shadows "*"-Konvention geht musl zeitlich voraus und hat nichts mit ihm zu tun, und musls Wahl des Fehler-Sentinels hat nichts mit OpenSSH oder Shadow-Dateien zu tun. Der Fehler lebt vollständig in der Lücke zwischen ihnen, in einer Annahme, die niemand aufgeschrieben hatte, weil sie einfach immer gehalten hatte.

CWE-394, die Klassifizierung, die diese CVE tatsächlich trägt, ist "Unexpected Status Code or Return Value", und genau das ist es. Es ist zugleich ein in zwei getrennten Sinnen "harmlos aussehender" Fehler: Ein "*"-Shadow-Feld sieht per Design inert aus, und musls "*"-bei-Fehler-Verhalten war bereits unabhängig im Dezember 2024 gemeldet worden, in einem völlig anderen Kontext, pam_unix zum Absturz bringend, fast zwei Jahre bevor irgendjemand es mit einem OpenSSH-Bypass in Verbindung brachte. Derselbe zugrundeliegende Vertragsbruch, zwei sehr unterschiedliche Konsequenzen, vollständig davon abhängig, welcher Aufrufer ihn gerade berührte.

Die praktische Version für jeden, der einen Vergleich wie diesen pflegt: Ein Schnittstellenvertrag ist nur so stark wie die Bereitschaft jedes Aufrufers, ihn zu prüfen, nicht so stark wie der Wortlaut des Vertrags selbst. Wenn Sie einen berechneten Wert mit einem gespeicherten vergleichen und eine der beiden Seiten einen dokumentierten "hier ist etwas schiefgegangen"-Sentinel hat, prüfen Sie, ob dieser Sentinel jemals auch auf dem "hier gibt es nichts zu sehen"-Wert der anderen Seite landen könnte, bevor Sie diesem Vergleich überhaupt vertrauen. Das ist keine musl-spezifische oder OpenSSH-spezifische Prüfung. Es ist die eigentliche Invariante, die dieser gesamten CVE offenbar fehlte.

Das ist genau die Art von Grenzfall, die ein sicherer Code-Review finden soll: nicht der offensichtliche Injection-Punkt, sondern der Vergleich, den niemand zu hinterfragen für nötig hielt, weil beide Seiten für sich genommen immer saubere aussahen. Wenn Ihr eigener Stack einen Authentifizierungs- oder Vergleichspfad auf C-Ebene hat, der noch nie Funktion für Funktion so nachvollzogen wurde, wie dieser hier nachvollzogen wurde, ist das genau die Lücke, die ein Code-Level-Review schließen soll.

Quellen

Soll das auch in Ihrem Produkt geprüft werden?

Security Code-Review

Möchten Sie lernen, solche Schwachstellen selbst zu finden?

Secure-Code-Review-Kurs im Einzeltraining
Alle Artikel

08Kontakt

Sie bringen etwas mit Nutzerdaten heraus?

Senden Sie mir eine kurze Beschreibung. Ich antworte innerhalb von drei Tagen mit Rückfragen oder einem Angebotsentwurf; den Preis vereinbaren wir vor Beginn schriftlich.