evo-erlang-ssh-cve-patch

Identifies and patches CVE-2025-32433 in Erlang/OTP SSH server source code by inserting an authentication-state check in ssh_connection.erl that rejects connection protocol messages (IDs >= 80) receiv

SKILL.md

GitHub

evo-erlang-ssh-cve-patch

Overview

CVE-2025-32433 is a critical (CVSS 10.0) pre-authentication Remote Code Execution vulnerability in Erlang/OTP SSH server. The server fails to validate authentication state before processing SSH connection protocol messages (message IDs >= 80), allowing attackers to execute arbitrary commands without credentials.

Vulnerability Details

  • Root Cause: ssh_connection.erl's handle_msg/4 function processes channel messages (SSH_MSG_CHANNEL_OPEN, SSH_MSG_CHANNEL_REQUEST) without checking if #ssh{authenticated} is true
  • Attack Vector: After completing key exchange, attacker sends SSH_MSG_CHANNEL_OPEN (ID 90) + SSH_MSG_CHANNEL_REQUEST (ID 98) with exec type, bypassing authentication entirely
  • Affected Versions: All Erlang/OTP versions before 27.3.3, 26.2.5.11, 25.3.2.20

The Fix

Add a pattern-matching clause in ssh_connection.erl that catches ANY message when authenticated = false on the server side, returning a disconnect error. This clause must be placed BEFORE any channel message handlers but is naturally placed after any disconnect handler.

Exact Erlang Code to Insert

handle_msg(_Msg, Connection, server, #ssh{authenticated = false}) ->
    %% RFC 4252 Section 6:
    %% Message numbers of 80 and higher are reserved for protocols running
    %% after this authentication protocol, so receiving one of them before
    %% authentication is complete is an error, to which the server MUST
    %% respond by disconnecting.
    {disconnect, {?SSH_DISCONNECT_PROTOCOL_ERROR,
                  "Unauthenticated connection protocol message"}, Connection};

Insertion Point

Insert this clause right before the first handle_msg(#ssh_msg_channel_open_confirmation{... clause, which is the first channel message handler in the function.

Usage

import sys
sys.path.insert(0, '/app/environment/skills/evo-erlang-ssh-cve-patch/scripts')
from utils import find_ssh_connection_erl, check_if_already_patched, apply_cve_2025_32433_patch, validate_patch_applied

# Find the file
file_path = find_ssh_connection_erl('/app/workspace/otp_src_27.3.2')

# Check if already patched
if not check_if_already_patched(file_path):
    apply_cve_2025_32433_patch(file_path)

# Validate
assert validate_patch_applied(file_path), "Patch validation failed!"

Key Technical Details

  • The macro ?SSH_DISCONNECT_PROTOCOL_ERROR is defined in ssh_transport.hrl (value 2)
  • The #ssh{} record with authenticated = false field is in ssh.hrl
  • Both headers are already included by ssh_connection.erl
  • Erlang evaluates function clauses top-to-bottom, so this catch-all must come before specific channel handlers
  • The server atom in the 3rd argument ensures this only affects server-side connections (clients are unaffected)