OpenSSH-Anmeldeumgehung unter Alpine und musl (CVE-2026-123158)

Die „php2shell“-Patches (CVE-2026-109618 und CVE-2026-109619) für PHP am Ende des Lebenszyklus (EOL) sind nun verfügbar. Da die Antworten des Vertriebs aufgrund der hohen Nachfrage derzeit langsamer als gewöhnlich ausfallen, ist der schnellste Weg zu den gepatchten Paketen der Online-Kauf von ELS für Runtimes (wird in einem neuen Tab geöffnet). Lesen Sie die FAQ zu den Patches

Gehen Sie direkt zu einer KI-Zusammenfassung dieses Blogbeitrags

  • Fasse diesen Beitrag mit Perplexity zusammen
  • Fasse diesen Beitrag mit Gemini zusammen

Wichtigste Erkenntnisse

  • Mit einem Passwort von 257 bis 1024 Byte lässt sich bei jedem reinen Schlüsselkonto auf Linux-Systemen anmelden, auf denen sshd mit der musl-libc ohne PAM läuft, vor allem bei Alpine Linux. OpenSSH 10.6p2 enthält die Abhilfe, und der Fehler wird unter der Kennung CVE-2026-123158 erfasst.
  • Der Fehler liegt an der Schnittstelle zwischen zwei Projekten. Seit 2016 verwendet sshd für ein *-Konto das Salt eines anderen Kontos, und die Funktion crypt() von musl gibt bei einem zu langen Passwort die Zeichenfolge „*“ zurück, die mit dem gespeicherten * übereinstimmt. OpenSSH gibt an, dass die eigentliche Ursache außerhalb von sshd liege; der musl-Betreuer bezeichnet dies als fehlerhafte Verwendung durch den Aufrufer.
  • Wir haben das Problem unter Alpine Linux 3.18 nachgestellt: Beim zweiten Versuch innerhalb einer Verbindung gelingt die Anmeldung, ein bcrypt-Hash hebt die Längenbeschränkung auf, und das korrigierte Paket weist jeden Versuch zurück. Aktualisieren Sie sshd und starten Sie den Dienst neu oder deaktivieren Sie die Passwortanmeldung; unter Alpine 3.18 und älteren Versionen bewirkt die Einbindung von sshd_config.d allein keine Wirkung.
  • Alpine Linux 3.18 hat im Mai 2025 das Ende seines Lebenszyklus erreicht und erhielt diesmal ausnahmsweise einen Fix. Der „Endless Lifecycle Support“ (ELS) von TuxCare macht daraus ein Versprechen: Alpine 3.18 ELS erhält denselben Upstream-Commit wie openssh 9.3_p2.tuxcare.els7.

Am 5. November 2026 veröffentlichte das OpenSSH-Team die Version 10.6p2 mit einer kritischen Sicherheitsumgehung. Auf einigen Linux-Systemen kann sich ein Angreifer über SSH bei einem Konto anmelden, das eigentlich niemals ein Passwort akzeptieren sollte. Es gibt kein Passwort, das er erraten könnte. Der Angreifer benötigt lediglich den Namen eines solchen Kontos. In der gängigsten Konfiguration sendet der Angreifer innerhalb derselben Verbindung zunächst ein falsches Passwort und anschließend ein Passwort mit einer Länge zwischen 257 und 1024 Byte. In manchen Konfigurationen funktioniert sogar ein kurzes Passwort, wie wir im Folgenden zeigen.

Der Fehler wird unter der Kennung CVE-2026-123158 (CVSS 3.1-Wert 8,1) geführt. Er wurde von Özgün Kültekin von Wakefield (wakefield.co) entdeckt. Der dahinterstehende Codepfad ist seit 2016 in der portablen OpenSSH-Version enthalten, und zwar von Version 7.3 bis einschließlich 10.6p1.

Dieser Fehler ist ungewöhnlich, da er sich zwischen zwei Projekten befindet. Der Angriff erfordert weder einen Pufferüberlauf noch einen Absturz oder eine Speicherbeschädigung. Es handelt sich um einen Logikfehler, der nur dann auftritt, wenn OpenSSH und die C-Bibliothek „musl“ gemeinsam auf demselben System ausgeführt werden, und die beiden Projekte sind sich nicht einig, wem dieser Fehler zuzuschreiben ist. In den OpenSSH-Versionshinweisen wird dies wie folgt beschrieben.

„Wir gehen davon aus, dass die eigentliche Ursache dieses Fehlers außerhalb von sshd liegt, er jedoch über sshd ausgenutzt werden kann und dort vermieden werden kann.“

Der musl-Betreuer, Rich Felker, sieht das anders.

„Meiner Ansicht nach handelt es sich hierbei nicht um einen Fehler in musl, sondern lediglich um eine fehlerhafte Verwendung durch den Aufrufer.“

In diesem Beitrag sehen wir uns den Code beider Projekte an, zeigen Schritt für Schritt, wie die Anmeldung abläuft, und reproduzieren den Angriff auf Alpine Linux in unserem Labor.

Wer ist betroffen?

Der Angriff funktioniert nur, wenn all diese Bedingungen auf demselben Server erfüllt sind.

  • sshd ist mit der musl-libc verknüpft. Alpine Linux ist der bekannteste Fall und bildet zudem die Grundlage für viele Docker-Images, doch betroffen sind nur Images, auf denen sshd ausgeführt wird. Der Fehler ist nicht auf Alpine beschränkt. Jedes Linux-System oder -Gerät, auf dem ein mit musl und ohne PAM kompilierter sshd-Prozess läuft, kann betroffen sein, einschließlich benutzerdefinierter Firmware und selbst kompilierter sshd-Binärdateien.
  • sshd verwendet PAM (das Authentifizierungs-Framework, das die meisten Linux-Distributionen für die Anmeldung nutzen) nicht zur Überprüfung von Passwörtern. Unter Alpine wird das Standardpaket „openssh-server“ ohne PAM kompiliert. Das Paket „openssh-server-pam“, das mit der Option „UsePAM no“ ausgeführt wird, ist ebenfalls davon betroffen.
  • Die Anmeldung per Passwort ist mit „PasswordAuthentication yes“ aktiviert, was der Standardwert ist.
  • Das Zielkonto hat ein einzelnes * als Passwort. Der typische Fall ist ein reiner Schlüsselbenutzer mit * in /etc/shadow. Unter Alpine ohne PAM kann sich ein Konto mit ! selbst mit einem SSH-Schlüssel nicht anmelden, daher ändern Administratoren ! in *, damit die Anmeldung per Schlüssel funktioniert. Auch das Root-Konto im Alpine-Basis-Image hat seit etwa Alpine 3.16 ein *, aber sshd blockiert die Passwort-Anmeldung für root, sofern nicht „PermitRootLogin yes“ gesetzt ist. Benutzer, die über musl-nscd von einem Verzeichnisdienst stammen, haben keinen Eintrag in der shadow-Datei und können ebenfalls ein * als Passwort erhalten.
  • Der erste Eintrag in der Reihenfolge von /etc/passwd, dessen Hash mit $ beginnt, muss einen Hash-Typ verwenden, bei dem musl einen Fehler ausgeben kann. Bei einem normalen $5$- oder $6$-Hash (SHA-256 oder SHA-512 crypt) benötigt der Angreifer 257 bis 1024 Byte, und Alpinens passwd schreibt standardmäßig $6$. Bei einem bcrypt-Hash ($2a$, $2b$ oder $2y$) funktioniert jedes nicht leere Passwort mit einer Länge von bis zu 1024 Byte. Bei einem alten $1$-Hash (MD5), einem yescrypt-$y$-Hash oder gar keinem Hash funktioniert der Angriff nicht.

Auf diesen Systemen ist die Sicherheitslücke nicht vorhanden.

  • Nicht-Linux-Systeme.
  • Linux-Systeme mit glibc, wie RHEL, AlmaLinux, CloudLinux, Debian oder Ubuntu.
  • Linux-Systeme, auf denen sshd Passwörter über PAM überprüft.

Konten, die mit einem „!“ anstelle eines „*“ gesperrt sind, sind ebenfalls sicher, ebenso wie Server mit der Einstellung „PasswordAuthentication no“. Wenn Sie nur die einzelnen Schritte benötigen, springen Sie zum Abschnitt„Was ist nun zu tun?“am Ende des Artikels.

Was ein einzelner Stern in /etc/shadow bedeutet

Unter Linux werden Passwort-Hashes in der Datei /etc/shadow gespeichert. Ein Hash ist eine einseitig verschlüsselte Form des Passworts. Wenn Sie sich anmelden, wandelt das System die von Ihnen eingegebenen Zeichen auf dieselbe Weise in einen Hash um und vergleicht die beiden Ergebnisse. Stimmen diese überein, erhalten Sie Zugriff.

Ein normaler Eintrag sieht so aus:

alice:$6$Qx7Lm2Pa$k3J9...long hash...:20000:0:99999:7:::

Die „6“ am Anfang steht für „SHA-512 crypt“, ein auf SHA-512 basierendes Verfahren zur Passwort-Hashberechnung. Der darauf folgende Teil, „Qx7Lm2Pa“, ist das Salt. Ein Salt ist eine kurze, zufällige Zeichenfolge, die in das Passwort eingemischt wird, sodass zwei Benutzer mit demselben Passwort dennoch unterschiedliche Hashwerte erhalten.

Seit etwa 1980, als die Hash-Werte noch in der Datei /etc/passwd gespeichert waren, fügen Unix-Administratoren ein einzelnes * in dieses Feld ein, wenn sich ein Benutzerkonto niemals mit einem Passwort anmelden darf. Die Logik dahinter ist einfach: Kein Passwort der Welt ergibt als Hash-Wert nur ein *, da ein echter Hash-Wert immer wesentlich länger ist. So kann sich das Konto weiterhin mit einem SSH-Schlüssel anmelden, jedoch niemals mit einem Passwort. Bei dem gesamten Fehler geht es darum, dieses Prinzip zu unterlaufen.

Wie sshd ein Passwort ohne PAM überprüft

Wenn PAM deaktiviert ist, überprüft sshd das Passwort selbstständig in der Funktion sys_auth_passwd() in der Datei auth-passwd.c. Dies ist der Code aus OpenSSH 9.3p2, der Version, die mit Alpine Linux 3.18 ausgeliefert wurde, vor der Korrektur.

/* Just use the supplied fake password if authctxt is invalid */
char *pw_password = authctxt->valid ? shadow_pw(pw) : pw->pw_passwd;
...
/*
 * Encrypt the candidate password using the proper salt, or pass a
 * NULL and let xcrypt pick one.
 */
if (authctxt->valid && pw_password[0] && pw_password[1])
    salt = pw_password;
encrypted_password = xcrypt(password, salt);

/*
 * Authentication is accepted if the encrypted passwords
 * are identical.
 */
return encrypted_password != NULL &&
    strcmp(encrypted_password, pw_password) == 0;

Das ist die klassische Vorgehensweise. Man liest den gespeicherten Hash aus, berechnet den Hash des eingegebenen Passworts unter Verwendung des gespeicherten Hashs als Salt und vergleicht die beiden Zeichenfolgen.

Schauen Sie sich nun die Salt-Zeile an. sshd verwendet den gespeicherten Wert nur dann als Salt, wenn er aus mindestens zwei Zeichen besteht. Ein * besteht nur aus einem Zeichen. Bei einem *-Konto bleibt der Salt-Wert daher NULL, und xcrypt() muss selbst einen Salt auswählen.

Die Änderung von 2016

Warum wählt xcrypt() ein Salt aus? Die Antwort lautet: CVE-2016-6210. Im Jahr 2016 wies ein Forscher nach, dass sshd bei nicht existierenden Benutzern mit einer anderen Geschwindigkeit antwortete. Durch das Senden langer Passwörter und das Messen der Antwortzeit konnte ein Angreifer eine Liste echter Benutzernamen erstellen.

Um diese Lücke zu schließen, modifizierte Darren Tucker sshd so, dass es, wenn kein brauchbares Salt vorhanden ist, den Hash-Typ und das Salt von einem echten Konto auf dem System übernimmt. Der erste Commit, 9286875a „Determine appropriate salt for invalid users“, übernahm das Salt von root. Sechs Tage später sorgte der Commit dbf788b4 „Search users for one with a valid salt“ dafür, dass alle Konten durchsucht wurden. Beide wurden im August 2016 in OpenSSH 7.3 integriert. Die Änderung war für nicht existierende Benutzer gedacht. Doch die oben beschriebene Zwei-Zeichen-Prüfung leitet ein gültiges Konto mit * auf denselben Pfad weiter, sodass auch dieses den ausgeliehenen Salt erhält.

Dies ist die Funktion `pick_salt()` in `openbsd-compat/xcrypt.c`, wie sie in Version 9.3p2 aussieht.

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

Es durchläuft die Konten in der Reihenfolge von /etc/passwd und übernimmt das erste $…$-Salt, das es findet, zum Beispiel $6$Qx7Lm2Pa von alice. Es speichert das Ergebnis, sodass spätere Versuche über dieselbe Verbindung das zwischengespeicherte Salt erhalten. Wenn kein Konto einen $-Hash hat, greift es auf xx zurück, den alten DES-Typ.

Vor dieser Änderung verwendete sshd „xx“ für kurze Einträge wie *. Der alte DES-Algorithmus schlägt bei langen Passwörtern nicht fehl, sondern verwendet lediglich die ersten 8 Zeichen, sodass er niemals * zurückgeben konnte. Nach der Änderung erhält ein *-Konto das Salt eines anderen Benutzers. So entstand das Problem im Jahr 2016. Die Änderung war eine gute Lösung für ein echtes Timing-Problem, und soweit bekannt ist, hat niemand den Nebeneffekt bemerkt.

Was gibt die Funktion `crypt()` von musl bei einem Fehler zurück?

musl ist eine kleine C-Bibliothek, die von Alpine und vielen eingebetteten Systemen verwendet wird. Der SHA-512-Verschlüsselungscode befindet sich in der Datei „src/crypt/crypt_sha512.c“. Er begrenzt die Passwortlänge, um das System vor langsamen, rechenintensiven Anfragen zu schützen.

/* key limit is not part of the original design, added for DoS protection.
 * rounds limit has been lowered (versus the reference/spec), also for DoS
 * protection. runtime is O(klen^2 + klen*rounds) */
#define KEY_MAX 256
...
    /* reject large keys */
    for (i = 0; i <= KEY_MAX && key[i]; i++);
    if (i > KEY_MAX)
        return 0;

Ein Passwort, das länger als 256 Byte ist, wird abgelehnt. Sehen Sie sich nun an, was __crypt_sha512(), die interne Funktion, die von crypt() aufgerufen wird, in diesem Fall zurückgibt.

p = sha512crypt(key, setting, output);
/* self test and stack cleanup */
q = sha512crypt(testkey, testsetting, testbuf);
if (!p || q != testbuf || memcmp(testbuf, testhash, sizeof testhash))
    return "*";
return p;

Im Fehlerfall gibt die Funktion `crypt()` von musl die Zeichenkette „*“ zurück. Der Code für SHA-256, MD5 und Blowfish verhält sich ebenso, wobei nur SHA-256 und SHA-512 die 256-Byte-Beschränkung aufweisen. musl funktioniert seit 2012 auf diese Weise. POSIX schreibt vor, dass `crypt()` im Fehlerfall einen Nullzeiger zurückgeben und `errno` setzen soll, doch musl hat sich bewusst für eine Zeichenkette entschieden. Im Commit b3c4cc12 wird erklärt, dass „ein großer Teil der Programme, die `crypt` aufrufen, nicht auf einen Fehler vorbereitet ist und nicht überprüft, ob der Rückgabewert null ist“, und das Ergebnis wird als „unvergleichbarer Hash“ bezeichnet.

Das Problem ist, dass * in der Datei /etc/shadow auch den Wert „Anmeldung ohne Passwort“ bedeutet.

musl hat das klassische Muster tatsächlich beibehalten. Sieh dir die letzte Zeile des DES-Codes in crypt_des.c an.

return (setting[0]=='*') ? "x" : "*";

Wenn das Salt selbst mit * beginnt, gibt „musl“ stattdessen x zurück. Daher liefert „crypt(password, „*“)“ niemals „*“ als Ergebnis. Dies schützt vor der klassischen Überprüfung, bei der ein Programm den gespeicherten Wert als Salt verwendet und das Ergebnis anschließend mit eben diesem gespeicherten Wert vergleicht.

Allerdings verwendete sshd den gespeicherten Wert nicht als Salt. Es verwendete stattdessen das Salt von Alice. Musl erkannte $6$…, sodass dieser Schutz nichts nützen konnte.

Ein kleiner Test

Wir haben den Teil der C-Bibliothek selbst mit einem kleinen Testprogramm überprüft. Hier sind die beiden wichtigsten Zeilen, vereinfacht dargestellt. Es berechnet Hash-Werte für Passwörter, die ausschließlich aus dem Buchstaben A bestehen, unter Verwendung eines ausgeliehenen $6$-Salts, und vergleicht das Ergebnis mit *.

r = crypt(long_password, "$6$abcdefgh");  /* salt borrowed from another account */
ok = (r != NULL && strcmp(r, "*") == 0);  /* compare with the "*" in /etc/shadow */

Wir haben es unter Alpine Linux 3.18 (musl) und unter Debian 12 getestet. Unter Debian stammt crypt(), wie bei den meisten modernen glibc-Systemen, aus einer separaten Bibliothek namens libxcrypt.

== musl (Alpine 3.18) ==
len=   8 crypt()=$6$abcdefgh$65STptHV...  match "*": no
len= 256 crypt()=$6$abcdefgh$9SA4XFln...  match "*": no
len= 257 crypt()=*  match "*": YES
len=1024 crypt()=*  match "*": YES
== libxcrypt (Debian 12) ==
len=   8 crypt()=$6$abcdefgh$65STptHV...  match "*": no
len= 256 crypt()=$6$abcdefgh$9SA4XFln...  match "*": no
len= 257 crypt()=$6$abcdefgh$D4Ayo9g5...  match "*": no
len=1024 crypt()=*0  match "*": no

Bei musl entspricht jedes Passwort ab 257 Byte dem Sternchen. Bei libxcrypt wird ein 257-Byte-Passwort normal gehasht. libxcrypt hat eine eigene Grenze von 511 Byte, gibt jedoch bei einem Fehler *0 zurück oder *1, wenn das Salt mit *0 beginnt. Eine aus zwei Zeichen bestehende Fehlerzeichenfolge kann niemals mit einem einzelnen Zeichen * übereinstimmen, sodass der Vergleich – wie erwartet – fehlschlägt.

Der gesamte Ablauf vom Eingeben eines langen Passworts bis zur Anmeldung

Hier ist die vollständige Angriffskette für einen Angreifer, der es auf einen „Key-Only“-Benutzer auf einem Alpine-Server abgesehen hat. Dieser Benutzer hat * in der Datei /etc/shadow, und der erste $-Hash auf dem System ist der $6$-Hash eines anderen Benutzers.

  1. Der Angreifer stellt eine Verbindung über SSH her und fordert als „Key-Only“-Benutzer die Anmeldung per Passwort an.
  2. Der Angreifer sendet ein Passwort mit einer Länge von 300 Byte. Da sshd Passwörter mit einer Länge von bis zu 1024 Byte (MAX_PASSWORD_LEN) akzeptiert, wird der Vorgang fortgesetzt.
  3. sshd liest * aus der Datei /etc/shadow. Da diese nur ein Zeichen enthält, gibt es kein Salt.
  4. Die Funktion `pick_salt()` übernimmt das $6$-Salt des anderen Benutzers.
  5. Der SHA-512-Code von musl erkennt mehr als 256 Byte und schlägt fehl. crypt() gibt „*“ zurück.
  6. Bei diesem ersten Versuch hat sshd das gespeicherte * bereits verloren – aus einem Nebengrund, den wir weiter unten erläutern –, sodass der Vergleich fehlschlägt. Der Angreifer versucht es einfach erneut über dieselbe Verbindung.
  7. Beim zweiten Versuch vergleicht sshd „*“ mit dem gespeicherten „*“. Sie stimmen überein. Die Anmeldung wird akzeptiert.

Aus diesem Grund liegt das Angriffsfenster bei 257 bis 1024 Byte, wenn das entlehnte Salt den Wert $5$ oder $6$ hat. Unterhalb von 257 Byte hasht musl das Passwort wie gewohnt. Oberhalb von 1024 Byte lehnt sshd es vor dem Hashen ab. Im CVE-Text heißt es zwar „zwischen 256 und 1024 Zeichen“, doch das tatsächliche Fenster beginnt bei 257 Byte.

Bei bcrypt spielt die Länge keine Rolle

Es gibt noch einen weiteren Pfad, der weder in der Alpine-Sicherheitsempfehlung noch in der CVE aufgeführt ist. Wir haben ihn bei der Überprüfung des Codes entdeckt. pick_salt() schneidet den Hash an seinem letzten $ ab. Ein bcrypt-Hash sieht so aus: $2b$10$, gefolgt von 53 Zeichen, ohne $ zwischen dem Salt und dem Hash. Daher behält sshd nur $2b$10$ bei und verliert das Salt selbst. musl lehnt diese Einstellung ab und gibt im Fehlerfall erneut „*“ zurück. Diesmal spielt die Passwortlänge keine Rolle.

Upstream hat diesen Salt-Fehler am 30. Oktober 2026 im Commit 84f8f466 „Behebung der Generierung eines falschen Passwort-Salts für bcrypt-Hashes“ behoben.

+            /*
+             * bcrypt ($2*$NN$) has no '$' between its 22 character
+             * salt and the hash, so keep the salt too or crypt(3)
+             * will reject the setting without doing any work.
+             */
+            if (passwd[1] == '2' && strlen(p + 1) >= 22)
+                    typelen += 1 + 22;

In der Commit-Meldung ist von einem Timing-Problem die Rede, doch unter musl sind die Auswirkungen weitaus gravierender. Dieser Commit befindet sich im Master-Zweig und ist nicht Teil von 10.6p2. Die Umgehungslösung für 10.6p2 blockiert diesen Pfad weiterhin, da ein * kürzer als 4 Zeichen ist.

Wir haben es unter Alpine 3.18 mit einem bcrypt-Hash als erstem $-Hash auf dem System getestet. Mit dem alten Paket konnte sich jedes nicht leere Passwort mit einer Länge von 1 bis 1024 Byte bereits beim zweiten Versuch beim *-Konto anmelden. Das korrigierte Paket hat alle Versuche abgewehrt. Da das Programm „passwd“ von Alpine standardmäßig $6$ schreibt, muss dieser Pfad einen bcrypt-Hash enthalten, der von einem Administrator oder einem Tool dort abgelegt wurde.

Bilder von Alpenwolken

In der Sicherheitsmitteilung von Alpine wird der Benutzer „alpine“ in den offiziellen „tiny-cloud“-Images der Versionen 3.18.11 bis 3.24.3 genannt. Dieser Benutzer verfügt über *. Ein neues Image ist nicht gefährdet, da noch kein Konto über einen echten Passwort-Hash verfügt. Es wird erst gefährdet, sobald ein Konto einen $6$-Hash erhält, während die Passwort-Anmeldung aktiviert ist. Images, die über „cloud-init“ konfiguriert wurden, das „PasswordAuthentication no“ schreibt, sind nicht betroffen. Der Forscher weist darauf hin, dass der Benutzer „alpine“ auf den offiziellen AWS- und Azure-Images „doas“ (das Alpine-Äquivalent zu „sudo“) ohne Passwort ausführen kann, sodass eine Anmeldung als „alpine“ dort denselben Status wie „root“ hat.

Wir haben es auf Alpine nachgebildet.

Der „crypt()“-Test zeigt den Teil, der zur C-Bibliothek gehört. Wir wollten außerdem sehen, ob uns ein echter sshd den Zugriff gewährt, und haben daher eine kleine Testumgebung aufgebaut.

Wir haben zwei Bilder verwendet:

  • Alpine Linux 3.18 mit openssh-server 9.3_p2-r3, dem letzten Alpine-Paket vor der Korrektur.
  • Alpine Linux 3.18 mit openssh-server 9.3_p2-r4, dem korrigierten Paket.

In beiden Fällen haben wir einem Konto mit dem Alpine-eigenen Tool „passwd“ ein echtes Passwort zugewiesen. Anschließend haben wir ein Konto nur mit Schlüssel (*) und ein gesperrtes Konto (! ) hinzugefügt und versucht, uns mit Passwörtern unterschiedlicher Länge anzumelden. „LOGGED IN“ bedeutet, dass der zweite Versuch innerhalb derselben Verbindung akzeptiert wurde und sshd einen Befehl für uns als dieser Benutzer ausgeführt hat.

                                   Alpine 3.18     Alpine 3.18
                                   9.3_p2-r3       9.3_p2-r4
                                   (before fix)    (fixed)
key-only (*), 8 bytes              denied          denied
key-only (*), 256 bytes            denied          denied
key-only (*), 257 bytes            LOGGED IN       denied
key-only (*), 1024 bytes           LOGGED IN       denied
key-only (*), 1025 bytes           denied          denied
locked (!), 300 bytes              denied          denied
root (*), default config           denied          denied
root (*), PermitRootLogin yes      LOGGED IN       denied
key-only (*), 1 byte, with bcrypt  LOGGED IN       denied
real password                      accepted        accepted

Auf dem manipulierten System schlug jeder Angriff fehl, während das echte Passwort weiterhin wie zuvor funktionierte. Der normale SSH-Client sendet höchstens 1023 Byte, sodass ein längeres eingegebenes Passwort einfach auf die Länge des Angriffsfensters gekürzt wird. Für die Zeilen mit 1024 und 1025 Byte verwendeten wir einen kleinen Client, der genau diese Länge sendet. Bei 1025 Byte lehnte sshd das Passwort ab, bevor es gehasht wurde. Bei der bcrypt-Zeile war der erste $-Hash auf dem System ein $2b$-Hash statt eines $6$-Hashs.

Warum jeder Angriff zwei Versuche erforderte

Bei jedem anfälligen Durchlauf in der Tabelle schlug der erste Passwortversuch fehl, während der zweite Versuch innerhalb derselben Verbindung erfolgreich war. Der erste Versuch kann ein beliebiges falsches Passwort mit einer Länge von bis zu 1024 Byte sein. Wir haben den Grund dafür mit einem kleinen Testprogramm bestätigt, das protokolliert, was sshd an strcmp() übergibt. Beim ersten Versuch verglich sshd „*“ mit dem, was an der Adresse des gespeicherten * verblieben war – in unserem Labor war dies eine leere Zeichenkette. Beim zweiten Versuch verglich es „*“ mit „*“.

Die Ursache ist ein Pufferproblem in sshd. shadow_pw() gab einen Zeiger auf den Puffer zurück, den die Funktion getspnam() von musl bei jeder Abfrage wiederverwendet. Beim ersten Versuch wird pick_salt() aufgerufen, das in diesemselben Puffer nach anderen Konten sucht und anschließend den entlehnten Hash mit explicit_bzero() löscht. Somit ist das *, auf das sshd noch zeigte, nicht mehr vorhanden. Ab dem zweiten Versuch wird das Salt zwischengespeichert, es findet keine weitere Suche statt, und * stimmt mit * überein. Der Workaround in 10.6p2 behebt auch dieses Problem, wie wir im Abschnitt „Workaround“ zeigen.

Der zweite Versuch ist nur erforderlich, wenn das * aus der Datei /etc/shadow stammt und sshd mit den Standardeinstellungen läuft. In unserer Testumgebung wurde ein Konto mit einem * in der Datei /etc/passwd und ohne Eintrag in /etc/shadow bereits beim allerersten Versuch akzeptiert. Bei der sshd-Option „PermitEmptyPasswords yes“, die die Anmeldung von Konten mit leerem Passwort zulässt, wurde sogar ein * in /etc/shadow bereits beim ersten Passwortversuch akzeptiert.

Benutzer von Verzeichnisdiensten verhalten sich wahrscheinlich ähnlich wie im Fall von /etc/passwd, aber das haben wir nicht getestet. Dies passt zu beiden öffentlichen Quellen. In der Sicherheitsempfehlung von Alpine heißt es, dass der Angriff über eine einzige Verbindung erfolgt und keine Wiederholungsversuche erfordert, während auf der Seite des Forschers steht, dass ein * in der Datei „shadow“ zunächst einen falschen Versuch erfordert.

sshd lässt standardmäßig 6 Versuche pro Verbindung zu (MaxAuthTries), sodass der zusätzliche Versuch einen Angreifer nicht aufhält. Im Fall von /etc/shadow verbleiben somit ein fehlgeschlagener Versuch und ein akzeptierter Versuch vom selben Verbindungsport.

Failed password for keyonly from 127.0.0.1 port 53734 ssh2
Accepted password for keyonly from 127.0.0.1 port 53734 ssh2

Ist es also ein OpenSSH-Fehler oder ein musl-Fehler?

Das OpenSSH-Team gibt an, dass die eigentliche Ursache außerhalb von sshd liegt, und dafür gibt es gute Gründe. Bei der seit Jahrzehnten verwendeten klassischen Überprüfung wird das eingegebene Passwort mit dem gespeicherten Wert als Salt gehasht und das Ergebnis mit eben diesem Wert verglichen. musl gewährleistet die Sicherheit dieses Verfahrens, da seine Fehlerzeichenfolge niemals mit dem übergebenen Salt übereinstimmt. Auf glibc-Systemen ist derselbe sshd-Code ebenfalls sicher.

Es gibt jedoch auch Argumente für die gegenteilige Sichtweise. Seit 2016 berechnet sshd den Hashwert eines Eintrags mit einem Zeichen unter Verwendung des Salts eines anderen Kontos und vergleicht das Ergebnis weiterhin mit dem gespeicherten Wert; dabei behielt es einen Zeiger auf einen C-Bibliothekspuffer bei, der sich im Hintergrund geändert hatte. Die Schutzfunktion von musl konnte hier nichts ausrichten. Rich Felker sieht das so und bezeichnet es als „eine fehlerhafte Verwendung durch den Aufrufer“. Er schrieb außerdem, dass der dahinterstehende Mechanismus „sehr wahrscheinlich im kommenden Release-Zyklus“ von musl geändert werde.

Für diesen Fehler waren also drei Teile erforderlich, und jedes einzelne sieht für sich genommen in Ordnung aus:

  • Das * kennzeichnet ein Konto ohne Passwortanmeldung – eine Unix-Konvention aus der Zeit um 1980, als die Hash-Werte noch in der Datei /etc/passwd gespeichert waren.
  • Die Fehlermeldung „*“ und die 256-Byte-Beschränkung in musl aus dem Jahr 2012.
  • Das „borrowed salt“ in sshd, aus dem Jahr 2016.

Kein einzelnes Projekt ist allein für den gesamten Fehler verantwortlich, was auch erklärt, warum er 10 Jahre lang unentdeckt blieb. Man muss das Zusammenspiel der einzelnen Komponenten betrachten, und der Fehler tritt nur auf einem Musl-System ohne PAM auf. Der Forscher entdeckte ihn, indem er sshd mit verschiedenen C-Bibliotheken und PAM-Einstellungen kompilierte und Tausende ungewöhnlicher Anmeldeversuche testete.

Die Korrektur in OpenSSH

Damien Miller hat die Abhilfe geschrieben. Im V_10_6-Zweig ist dies der Commit 90f9c2f7 „avoid password authn bypass“, und im Master-Zweig ist dieselbe Änderung der Commit 4353fdc4. Der Hauptteil ist sehr klein.

+#define MIN_PASSWORD_HASH_LEN    4
 #define MAX_PASSWORD_LEN 1024
...
-    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;

Ein echter Passwort-Hash ist niemals kürzer als 4 Zeichen. Wenn der gespeicherte Wert nun *, !, x oder ein anderes ebenso kurzes Zeichen ist, schlägt der Hash-Vergleich immer fehl, unabhängig davon, was crypt() zurückgibt. Dies blockiert alle in diesem Beitrag beschriebenen Vorgehensweisen, einschließlich der bcrypt-Methode. Der separate Fall für leere Passwörter, der durch PermitEmptyPasswords gesteuert wird, bleibt unverändert.

Der Patch ändert außerdem die Funktion `shadow_pw()` so, dass sie eine mit `xstrdup()` erstellte Kopie des Shadow-Eintrags zurückgibt und diese Kopie nach der Verwendung löscht. Dadurch wird das Pufferproblem beseitigt, das den beiden Versuchen zugrunde lag.

Auf der „openssh-unix-dev“-Mailingliste erläuterte Damien Miller die Entscheidung. Er schrieb: „Wir sind gut aufgestellt, um schnell eine Abhilfemaßnahme bereitzustellen.“ Zum Zeitpunkt der Erstellung dieses Artikels gibt musl bei Fehlern immer noch „*“ zurück, doch der Betreuer geht davon aus, diesen Mechanismus im nächsten Release-Zyklus zu ändern.

Was nun zu tun ist

sshd aktualisieren und neu starten

Unter Alpine führen Sie das Update und den Neustart wie folgt durch. Dies ist der Befehl aus dem Alpine-Hinweis. Mit dem Flag -a kann „apk“ Pakete ersetzen oder auf eine ältere Version zurücksetzen, um sie an die Repositories anzupassen. Überprüfen Sie daher, welche Änderungen dadurch vorgenommen werden.

apk upgrade -U -a
rc-service sshd restart

OpenSSH 9.3p2 startet für jede neue Verbindung eine neue Instanz von /usr/sbin/sshd, sodass in unserem Labor bei neuen Anmeldungen der korrigierte Code sofort nach der Installation des Pakets verwendet wurde. Starten Sie sshd dennoch neu, damit der Listening-Prozess ebenfalls die neue Binärdatei ausführt. In einem Container sollten Sie das Image auf Basis einer korrigierten Version neu erstellen und den Container neu starten. TuxCare-ELS-Kunden, die Alpine Linux 3.18 verwenden, erhalten den Fix aus dem ELS-Repository.
ELS entdecken →

Falls Sie das Update heute nicht durchführen können, deaktivieren Sie die Passwortanmeldung.

Dadurch wird die Passwortanmeldung für alle Benutzer deaktiviert, nicht nur für die gefährdeten Konten. Stellen Sie sicher, dass jeder, der Zugriff benötigt, über einen funktionierenden SSH-Schlüssel verfügt, und lassen Sie Ihre aktuelle Sitzung geöffnet. Seien Sie vorsichtig mit dem Drop-in-Ordner /etc/ssh/sshd_config.d. Die Standardkonfiguration von Alpine liest diesen erst ab Version 3.19 aus, sodass bei Version 3.18 und älter eine Drop-in-Datei allein keine Wirkung zeigt. Diese Befehle aus dem Alpine-Hinweis schreiben die Drop-in-Datei und ändern zudem die Datei /etc/ssh/sshd_config selbst, wobei eine Sicherungskopie unter /etc/ssh/sshd_config.bak angelegt wird.

mkdir -p /etc/ssh/sshd_config.d
echo 'PasswordAuthentication no' > /etc/ssh/sshd_config.d/00-no-passwords.conf
sed -i.bak -e '1i PasswordAuthentication no' \
  -e 's/^\(\s*PasswordAuthentication[^y#]*\)yes/\1no/i' \
  /etc/ssh/sshd_config
sshd -t && rc-service sshd restart
sshd -T | grep -i passwordauthentication

Der letzte Befehl sollte „passwordauthentication no“ ausgeben. Ist dies nicht der Fall, ist die Passwortauthentifizierung weiterhin aktiviert.

Achten Sie auf Anmeldungen, die nicht stattfinden sollten

Suchen Sie in /var/log/messages, der Standard-Syslog-Datei unter Alpine, nach Einträgen mit dem Wort „Accepted“ für Zeilen, die ein * enthalten. Im Fall von /etc/shadow zeigte unser Labor immer direkt davor eine Zeile mit dem Wort „Failed“ an, die vom selben Verbindungsport stammte. Verlassen Sie sich nicht allein auf dieses Paar. In anderen Konfigurationen kann der erste Versuch bereits erfolgreich sein, ein echter Benutzer, der sich vertippt, hinterlässt dasselbe Paar, viele Container führen keinen Syslog-Daemon aus und protokollieren nichts, und ein Angreifer mit Root-Rechten kann die Protokolle bereinigen. Überprüfen Sie außerdem auf neue Benutzer, neue Schlüssel in `authorized_keys` und die Verwendung von `doas`. Wenn Sie eine Anmeldung nicht ausschließen können, behandeln Sie den Rechner als kompromittiert.

Ältere und versteckte Systeme überprüfen

Ältere Docker-Images und eingebettete Geräte verwenden manchmal OpenSSH auf musl, und niemand aktualisiert sie. Überprüfen Sie auch diese.

Warum TuxCare hier eine wichtige Rolle spielt

Die Upstream-Umgehungslösung wurde in OpenSSH 10.6p2 implementiert. Viele Produktionssysteme verwenden jedoch nicht die neueste OpenSSH-Version, sondern die Version, die vor Jahren mit ihrer Distribution ausgeliefert wurde. Alpine Linux 3.18 wird mit OpenSSH 9.3p2 ausgeliefert und hat im Mai 2025 das Ende seiner Lebensdauer erreicht.

Alpine hat das sehr gut gehandhabt. Am Tag der Bekanntgabe veröffentlichten sie korrigierte Pakete für jeden Zweig bis zurück zu Version 3.16, auch für Zweige, deren Lebensdauer bereits abgelaufen war. Für Alpine 3.18 ist das 9.3_p2-r4, das korrigierte Paket in unserem Labor. Allerdings verspricht Alpine keine Korrekturen für Zweige, deren Lebensdauer abgelaufen ist. Dieses Mal haben sie eine bereitgestellt, aber wie es beim nächsten Fehler aussieht, kann niemand sagen.

TuxCare Endless Lifecycle Support (ELS) macht daraus ein Versprechen. Für Alpine Linux 3.18 ELS hat unser Team denselben Upstream-Commit auf OpenSSH 9.3p2 zurückportiert und ihn noch am selben Tag, an dem die Upstream-Version veröffentlicht wurde, integriert. Er wird zusätzlich zu allen 22 Patches unseres ELS-Pakets angewendet und entspricht dem Code von Alpines eigenem „pw.patch“. Das korrigierte Paket lautet „openssh 9.3_p2.tuxcare.els7“ und ist für x86_64 und aarch64 verfügbar.
ELS entdecken →

Dieser Fehler zeigt auch, warum es nicht ausreicht, nur die Sicherheitshinweise eines einzelnen Projekts zu verfolgen. Der Fehler betrifft Code sowohl aus musl als auch aus OpenSSH, die Abhilfe findet sich in OpenSSH, und die betroffenen Systeme sind musl-basierte Server, Container und eingebettete Geräte. Jemand muss all diese Aspekte im Blick behalten und einen getesteten Patch für die Versionen bereitstellen, die bei den Kunden noch im Einsatz sind. Das ist die Aufgabe unseres Sicherheitsteams.

Schließen Sie sich 4.500+ Linux- und Open-Source-Experten an!

2x im Monat. Kein Spam.

Inhaltsübersicht
Holen Sie sich die Open-Source-Sicherheitslösungen, die Sie brauchen

Schließen Sie sich 4.500+ Linux- und Open-Source-Experten an!

2x im Monat. Kein Spam.