diff --git a/examples/describeTicket.py b/examples/describeTicket.py index 4379a2e881..e1cd52e5b6 100755 --- a/examples/describeTicket.py +++ b/examples/describeTicket.py @@ -38,7 +38,7 @@ from impacket.krb5.asn1 import TGS_REP, EncTicketPart, AD_IF_RELEVANT from impacket.krb5.ccache import CCache from impacket.krb5.constants import ChecksumTypes -from impacket.krb5.crypto import Key, _enctype_table, InvalidChecksum, string_to_key +from impacket.krb5.crypto import Key, _enctype_table, InvalidChecksum, string_to_key, generate_kerberos_keys from impacket.ldap.ldaptypes import LDAP_SID PSID = PRPC_SID @@ -290,7 +290,7 @@ def parse_ccache(args): logging.debug("No kvno in ticket, skipping") logging.info(" %-28s: %d" % ("Key version number (kvno)", decodedTicket['ticket']['enc-part']['kvno'])) logging.debug("Handling Kerberos keys") - ekeys = generate_kerberos_keys(args) + ekeys = generate_kerberos_keys(args.rc4, args.aes, args.password, args.hex_pass, args.salt, args.user, args.domain) # copypasta from krbrelayx.py # Select the correct encryption key @@ -630,56 +630,6 @@ def PACparseGroupIds(data): return parsed_tuPAC -def generate_kerberos_keys(args): - # copypasta from krbrelayx.py - # Store Kerberos keys - keys = {} - if args.rc4: - keys[int(constants.EncryptionTypes.rc4_hmac.value)] = unhexlify(args.rc4) - if args.aes: - if len(args.aes) == 64: - keys[int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value)] = unhexlify(args.aes) - else: - keys[int(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value)] = unhexlify(args.aes) - ekeys = {} - for kt, key in keys.items(): - ekeys[kt] = Key(kt, key) - - allciphers = [ - int(constants.EncryptionTypes.rc4_hmac.value), - int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value), - int(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value) - ] - - # Calculate Kerberos keys from specified password/salt - if args.password or args.hex_pass: - if not args.salt and args.user and args.domain: # https://www.thehacker.recipes/ad/movement/kerberos - if args.user.endswith('$'): - args.salt = "%shost%s.%s" % (args.domain.upper(), args.user.rstrip('$').lower(), args.domain.lower()) - else: - args.salt = "%s%s" % (args.domain.upper(), args.user) - for cipher in allciphers: - if cipher == 23 and args.hex_pass: - # RC4 calculation is done manually for raw passwords - md4 = MD4.new() - md4.update(unhexlify(args.hex_pass)) - ekeys[cipher] = Key(cipher, md4.digest()) - logging.debug('Calculated type %s (%d) Kerberos key: %s' % (constants.EncryptionTypes(cipher).name, cipher, hexlify(ekeys[cipher].contents).decode('utf-8'))) - elif args.salt: - # Do conversion magic for raw passwords - if args.hex_pass: - rawsecret = unhexlify(args.hex_pass).decode('utf-16-le', 'replace').encode('utf-8', 'replace') - else: - # If not raw, it was specified from the command line, assume it's not UTF-16 - rawsecret = args.password - ekeys[cipher] = string_to_key(cipher, rawsecret, args.salt) - logging.debug('Calculated type %s (%d) Kerberos key: %s' % (constants.EncryptionTypes(cipher).name, cipher, hexlify(ekeys[cipher].contents).decode('utf-8'))) - else: - logging.debug('Cannot calculate type %s (%d) Kerberos key: salt is None: Missing -s/--salt or (-u/--user and -d/--domain)' % (constants.EncryptionTypes(cipher).name, cipher)) - else: - logging.debug('No password (-p/--password or -hp/--hex_pass supplied, skipping Kerberos keys calculation') - return ekeys - def kerberoast_from_ccache(decodedTGS, spn, username, domain): try: diff --git a/examples/smbserver.py b/examples/smbserver.py index 840aa80f74..b46061ce1c 100755 --- a/examples/smbserver.py +++ b/examples/smbserver.py @@ -39,9 +39,18 @@ parser.add_argument('-comment', action='store', help='share\'s comment to display when asked for shares') parser.add_argument('-username', action="store", help='Username to authenticate clients') parser.add_argument('-password', action="store", help='Password for the Username') + parser.add_argument('-computeraccountname', action="store", help='computer account name to authenticate arbitrary clients with signing via NetLogon') + parser.add_argument('-computeraccounthash', action="store", help='computer account NT hash to authenticate arbitrary clients with signing via NetLogon') + parser.add_argument('-computeraccountaes', action="store", help='computer account AES key to authenticate arbitrary clients with signing via Kerberos') + parser.add_argument('-computeraccountpassword', action="store", help='computer account NT hash to authenticate arbitrary clients with signing via Kerberos') + parser.add_argument('-computeraccountdomain', action="store", help='DC IP/hostname to authenticate arbitrary clients with signing via NetLogon') + parser.add_argument('-dcip', action="store", help='IP of domain controller to authenticate arbitrary clients with signing via NetLogon') parser.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes for the Username, format is LMHASH:NTHASH') parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + parser.add_argument('-readonly', action='store_true', help='Only allow reading of files') + parser.add_argument('-disablekerberos', action='store_true', help='Do not offer Kerberos authentication') + parser.add_argument('-disablentlm', action='store_true', help='Do not offer NTLM authentication') parser.add_argument('-ip', '--interface-address', action='store', default='0.0.0.0', help='ip address of listening interface') parser.add_argument('-port', action='store', default='445', help='TCP port for listening incoming connections (default 445)') parser.add_argument('-smb2support', action='store_true', default=False, help='SMB2 Support (experimental!)') @@ -70,8 +79,10 @@ logging.info('Switching output to file %s' % options.outputfile) server.setLogFile(options.outputfile) - server.addShare(options.shareName.upper(), options.sharePath, comment) + server.addShare(options.shareName.upper(), options.sharePath, comment, readOnly="yes" if options.readonly else "no") server.setSMB2Support(options.smb2support) + server.setKerberosSupport(not options.disablekerberos) + server.setNTLMSupport(not options.disablentlm) # If a user was specified, let's add it to the credentials for the SMBServer. If no user is specified, anonymous # connections will be allowed @@ -91,6 +102,20 @@ server.addCredential(options.username, 0, lmhash, nthash) + # If we want clients to be able to connect to us which enforce signing, we need a computer account to properly setup the connection + # Only works with SMB2 + # FIXME: For NTLM just NT hash is supported for now + required_secure_server_options = [options.computeraccountname, options.computeraccountdomain, options.dcip] + at_least_one_secure_server_options = [options.computeraccounthash, options.computeraccountaes, options.computeraccountpassword] + if any(required_secure_server_options): + if not all(required_secure_server_options): + logging.critical("All of the following options need to be set for accepting signed connections from arbitrary users in the domain: -computeraccountname, -computeraccountdomain, -dcip") + sys.exit(1) + if not any(at_least_one_secure_server_options): + logging.critical("At least one of the following options need to be set for accepting signed connections from arbitrary users in the domain: -computeraccounthash, -computeraccountaes, -computeraccountpassword") + sys.exit(1) + server.setComputerAccount(options.computeraccountname, options.computeraccounthash, options.computeraccountaes, options.computeraccountpassword, options.computeraccountdomain, options.dcip) + # Here you can set a custom SMB challenge in hex format # If empty defaults to '4141414141414141' # (remember: must be 16 hex bytes long) diff --git a/impacket/krb5/asn1.py b/impacket/krb5/asn1.py index 991c72556b..4d69d45099 100644 --- a/impacket/krb5/asn1.py +++ b/impacket/krb5/asn1.py @@ -334,6 +334,15 @@ class AP_REQ(univ.Sequence): _sequence_component('authenticator', 4, EncryptedData()) ) +class GSSAPIHeader_KRB5_AP_REQ(univ.Sequence): + tagSet = univ.Sequence.tagSet.tagImplicitly(tag.Tag(tag.tagClassApplication, tag.tagFormatConstructed, 0)) + componentType = namedtype.NamedTypes( + namedtype.NamedType('tokenOid', univ.ObjectIdentifier()), + # Actualy this is a constant 0x0001, but this decodes as an asn1 boolean + namedtype.NamedType('krb5_ap_req', univ.Boolean()), + namedtype.NamedType('apReq', AP_REQ()), + ) + class AP_REP(univ.Sequence): tagSet = _application_tag(constants.ApplicationTagNumbers.AP_REP.value) componentType = namedtype.NamedTypes( diff --git a/impacket/krb5/crypto.py b/impacket/krb5/crypto.py index ce3cda9ca0..7f47d0dd08 100644 --- a/impacket/krb5/crypto.py +++ b/impacket/krb5/crypto.py @@ -38,7 +38,7 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED # OF THE POSSIBILITY OF SUCH DAMAGE. # -from binascii import unhexlify +from binascii import hexlify, unhexlify from functools import reduce from os import urandom # XXX current status: @@ -61,7 +61,8 @@ from Cryptodome.Protocol.KDF import PBKDF2 from Cryptodome.Util.number import GCD as gcd from six import b, PY3, indexbytes, binary_type - +from impacket.krb5 import constants +import logging def get_random_bytes(lenBytes): # We don't really need super strong randomness here to use PyCrypto.Random @@ -718,3 +719,53 @@ def prfplus(key, pepper, l): e = _get_enctype_profile(enctype) return e.random_to_key(_xorbytes(bytearray(prfplus(key1, pepper1, e.seedsize)), bytearray(prfplus(key2, pepper2, e.seedsize)))) + +def generate_kerberos_keys(rc4=None, aes=None, password=None, hex_pass=None, salt=None, user=None, domain=None): + # copypasta from krbrelayx.py + # Store Kerberos keys + keys = {} + if rc4: + keys[int(constants.EncryptionTypes.rc4_hmac.value)] = unhexlify(rc4) + if aes: + if len(aes) == 64: + keys[int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value)] = unhexlify(aes) + else: + keys[int(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value)] = unhexlify(aes) + ekeys = {} + for kt, key in keys.items(): + ekeys[kt] = Key(kt, key) + + allciphers = [ + int(constants.EncryptionTypes.rc4_hmac.value), + int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value), + int(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value) + ] + + # Calculate Kerberos keys from specified password/salt + if password or hex_pass: + if not salt and user and domain: # https://www.thehacker.recipes/ad/movement/kerberos + if user.endswith('$'): + salt = "%shost%s.%s" % (domain.upper(), user.rstrip('$').lower(), domain.lower()) + else: + salt = "%s%s" % (domain.upper(), user) + for cipher in allciphers: + if cipher == 23 and hex_pass: + # RC4 calculation is done manually for raw passwords + md4 = MD4.new() + md4.update(unhexlify(hex_pass)) + ekeys[cipher] = Key(cipher, md4.digest()) + logging.debug('Calculated type %s (%d) Kerberos key: %s' % (constants.EncryptionTypes(cipher).name, cipher, hexlify(ekeys[cipher].contents).decode('utf-8'))) + elif salt: + # Do conversion magic for raw passwords + if hex_pass: + rawsecret = unhexlify(hex_pass).decode('utf-16-le', 'replace').encode('utf-8', 'replace') + else: + # If not raw, it was specified from the command line, assume it's not UTF-16 + rawsecret = password + ekeys[cipher] = string_to_key(cipher, rawsecret, salt) + logging.debug('Calculated type %s (%d) Kerberos key: %s' % (constants.EncryptionTypes(cipher).name, cipher, hexlify(ekeys[cipher].contents).decode('utf-8'))) + else: + logging.debug('Cannot calculate type %s (%d) Kerberos key: salt is None: Missing -s/--salt or (-u/--user and -d/--domain)' % (constants.EncryptionTypes(cipher).name, cipher)) + else: + logging.debug('No password (-p/--password or -hp/--hex_pass supplied, skipping Kerberos keys calculation') + return ekeys \ No newline at end of file diff --git a/impacket/smb3structs.py b/impacket/smb3structs.py index 556e3e6f06..0eaf0d6ec6 100644 --- a/impacket/smb3structs.py +++ b/impacket/smb3structs.py @@ -538,6 +538,9 @@ class SMB3Packet(SMBPacketBase): ('Data',':=""'), ) + +class Empty(Structure): + pass class SMB2Error(Structure): structure = ( ('StructureSize',' 0: @@ -2209,14 +2243,16 @@ def smbComNtCreateAndX(connId, smbServer, SMBCommand, recvPacket): createOptions = ntCreateAndXParameters['CreateOptions'] if mode & os.O_CREAT == os.O_CREAT: - if createOptions & smb.FILE_DIRECTORY_FILE == smb.FILE_DIRECTORY_FILE: + if createOptions & smb.FILE_DIRECTORY_FILE == smb.FILE_DIRECTORY_FILE and not readOnly: try: # Let's create the directory os.mkdir(pathName) mode = os.O_RDONLY except Exception as e: - smbServer.log("NTCreateAndX: %s,%s,%s" % (pathName, mode, e), logging.ERROR) + smbServer.log("NTCreateAndX: %s,%s,%s" % (pathName, mode, e), logging.ERROR, connData=connData) errorCode = STATUS_ACCESS_DENIED + elif readOnly: + errorCode = STATUS_ACCESS_DENIED if createOptions & smb.FILE_NON_DIRECTORY_FILE == smb.FILE_NON_DIRECTORY_FILE: # If the file being opened is a directory, the server MUST fail the request with # STATUS_FILE_IS_A_DIRECTORY in the Status field of the SMB Header in the server @@ -2239,9 +2275,11 @@ def smbComNtCreateAndX(connId, smbServer, SMBCommand, recvPacket): sock = socket.socket() sock.connect(smbServer.getRegisteredNamedPipes()[str(pathName)]) else: + if readOnly: + mode = os.O_RDONLY fid = os.open(pathName, mode) except Exception as e: - smbServer.log("NTCreateAndX: %s,%s,%s" % (pathName, mode, e), logging.ERROR) + smbServer.log("NTCreateAndX: %s,%s,%s" % (pathName, mode, e), logging.ERROR, connData=connData) # print e fid = 0 errorCode = STATUS_ACCESS_DENIED @@ -2290,7 +2328,7 @@ def smbComNtCreateAndX(connId, smbServer, SMBCommand, recvPacket): if errorCode == STATUS_SUCCESS: # Let's store the fid for the connection - # smbServer.log('Create file %s, mode:0x%x' % (pathName, mode)) + # smbServer.log('Create file %s, mode:0x%x' % (pathName, mode), connData=connData) connData['OpenedFiles'][fakefid] = {} connData['OpenedFiles'][fakefid]['FileHandle'] = fid connData['OpenedFiles'][fakefid]['FileName'] = pathName @@ -2321,12 +2359,13 @@ def smbComOpenAndX(connId, smbServer, SMBCommand, recvPacket): # Get the Tid associated if recvPacket['Tid'] in connData['ConnectedShares']: path = connData['ConnectedShares'][recvPacket['Tid']]['path'] + readOnly = connData['ConnectedShares'][recvPacket['Tid']]["read only"] == "yes" openedFile, mode, pathName, errorCode = openFile(path, decodeSMBString(recvPacket['Flags2'], openAndXData['FileName']), openAndXParameters['DesiredAccess'], openAndXParameters['FileAttributes'], - openAndXParameters['OpenMode']) + openAndXParameters['OpenMode'], readOnly) else: errorCode = STATUS_SMB_BAD_TID @@ -2352,7 +2391,7 @@ def smbComOpenAndX(connId, smbServer, SMBCommand, recvPacket): respParameters['Action'] = 0x3 # Let's store the fid for the connection - # smbServer.log('Opening file %s' % pathName) + # smbServer.log('Opening file %s' % pathName, connData=connData) connData['OpenedFiles'][fid] = {} connData['OpenedFiles'][fid]['FileHandle'] = openedFile connData['OpenedFiles'][fid]['FileName'] = pathName @@ -2414,9 +2453,9 @@ def smbComTreeConnectAndX(connId, smbServer, SMBCommand, recvPacket): connData['ConnectedShares'][tid] = share connData['ConnectedShares'][tid]['shareName'] = path resp['Tid'] = tid - # smbServer.log("Connecting Share(%d:%s)" % (tid,path)) + # smbServer.log("Connecting Share(%d:%s)" % (tid,path), connData=connData) else: - smbServer.log("TreeConnectAndX not found %s" % path, logging.ERROR) + smbServer.log("TreeConnectAndX not found %s" % path, logging.ERROR, connData=connData) errorCode = STATUS_OBJECT_PATH_NOT_FOUND resp['ErrorCode'] = errorCode >> 16 resp['ErrorClass'] = errorCode & 0xff @@ -2478,7 +2517,7 @@ def smbComSessionSetupAndX(connId, smbServer, SMBCommand, recvPacket): mechStr = MechTypes[mechType] else: mechStr = hexlify(mechType) - smbServer.log("Unsupported MechType '%s'" % mechStr, logging.DEBUG) + smbServer.log("Unsupported MechType '%s'" % mechStr, logging.DEBUG, connData=connData) # We don't know the token, we answer back again saying # we just support NTLM. # ToDo: Build this into a SPNEGO_NegTokenResp() @@ -2637,7 +2676,7 @@ def smbComSessionSetupAndX(connId, smbServer, SMBCommand, recvPacket): else: respToken = SPNEGO_NegTokenResp() respToken['NegState'] = b'\x02' - smbServer.log("Could not authenticate user!") + smbServer.log("Could not authenticate user!", connData=connData) if smbServer.auth_callback is not None: try: smbServer.auth_callback( @@ -2684,7 +2723,7 @@ def smbComSessionSetupAndX(connId, smbServer, SMBCommand, recvPacket): if jtr_dump_path != '': writeJohnOutputToFile(ntlm_hash_data['hash_string'], ntlm_hash_data['hash_version'], jtr_dump_path) except: - smbServer.log("Could not write NTLM Hashes to the specified JTR_Dump_Path %s" % jtr_dump_path) + smbServer.log("Could not write NTLM Hashes to the specified JTR_Dump_Path %s" % jtr_dump_path, connData=connData) respData['NativeOS'] = encodeSMBString(recvPacket['Flags2'], smbServer.getServerOS()) respData['NativeLanMan'] = encodeSMBString(recvPacket['Flags2'], smbServer.getServerOS()) @@ -2772,7 +2811,7 @@ def smbComNegotiate(connId, smbServer, SMBCommand, recvPacket): except Exception as e: # No NTLM throw an error - smbServer.log('smbComNegotiate: %s' % e, logging.ERROR) + smbServer.log('smbComNegotiate: %s' % e, logging.ERROR, connData=connData) respSMBCommand['Data'] = struct.pack(' 0: - # Is this GSSAPI NTLM or something else we don't support? - mechType = blob['MechTypes'][0] - if mechType != TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider']: - # Nope, do we know it? - if mechType in MechTypes: - mechStr = MechTypes[mechType] - else: - mechStr = hexlify(mechType) - smbServer.log("Unsupported MechType '%s'" % mechStr, logging.DEBUG) - # We don't know the token, we answer back again saying - # we just support NTLM. - # ToDo: Build this into a SPNEGO_NegTokenResp() - respToken = b'\xa1\x15\x30\x13\xa0\x03\x0a\x01\x03\xa1\x0c\x06\x0a\x2b\x06\x01\x04\x01\x82\x37\x02\x02\x0a' - respSMBCommand['SecurityBufferOffset'] = 0x48 - respSMBCommand['SecurityBufferLength'] = len(respToken) - respSMBCommand['Buffer'] = respToken - - return [respSMBCommand], None, STATUS_MORE_PROCESSING_REQUIRED - elif struct.unpack('B', securityBlob[0:1])[0] == ASN1_SUPPORTED_MECH: - # AUTH packet - blob = SPNEGO_NegTokenResp(securityBlob) - token = blob['ResponseToken'] - else: - # No GSSAPI stuff, raw NTLMSSP - rawNTLM = True - token = securityBlob + respSMBCommand = smb2.SMB2SessionSetup_Response() + respSMBCommand['SecurityBufferOffset'] = 0x48 + respSMBCommand['SecurityBufferLength'] = len(acceptBytes) + respSMBCommand['Buffer'] = acceptBytes + + connData['SignatureEnabled'] = True + connData['SigningSessionKey'] = encryption_key.contents[:16] # MS-SMB2 3.2.5.3.1 + connData['SignSequenceNumber'] = 1 + return respSMBCommand, STATUS_SUCCESS + + @staticmethod + def _ntlm_auth(token, connData, smbServer, rawNTLM): # Here we only handle NTLMSSP, depending on what stage of the # authentication we are, we act on it messageType = struct.unpack(' 0: + if authenticateMessage['user_name'].decode('utf-16le') != "" and (len(smbServer.getCredentials()) > 0 or computerAccountCredentials["username"] != ""): identity = authenticateMessage['user_name'].decode('utf-16le').lower() # Do we have this user's credentials? if identity in smbServer.getCredentials(): @@ -3001,13 +3080,22 @@ def smb2SessionSetup(connId, smbServer, recvPacket): uid, lmhash, nthash = smbServer.getCredentials()[identity] errorCode, sessionKey = computeNTLMv2(identity, lmhash, nthash, smbServer.getSMBChallenge(), - authenticateMessage, connData['CHALLENGE_MESSAGE'], - connData['NEGOTIATE_MESSAGE']) + authenticateMessage, connData['CHALLENGE_MESSAGE'], + connData['NEGOTIATE_MESSAGE']) if sessionKey is not None: connData['SignatureEnabled'] = True connData['SigningSessionKey'] = sessionKey connData['SignSequenceNumber'] = 1 + elif computerAccountCredentials["username"] != "": + # Try to get the session key via NetLogon + netlogon = NetLogon(computerAccountCredentials["dcip"], computerAccountCredentials["username"], computerAccountCredentials["nthash"], computerAccountCredentials["domain"]) + netlogon.setupConnection() + sessionKey, errorCode = netlogon.logonUserAndGetSessionKey(authenticateMessage, smbServer.getSMBChallenge()) + + connData['SignatureEnabled'] = True + connData['SigningSessionKey'] = sessionKey + connData['SignSequenceNumber'] = 1 else: errorCode = STATUS_LOGON_FAILURE else: @@ -3027,6 +3115,8 @@ def smb2SessionSetup(connId, smbServer, recvPacket): respToken = SPNEGO_NegTokenResp() # accept-completed respToken['NegState'] = b'\x00' + if rawNTLM: # raw NTLM does not expect a SPNEGO buffer + respToken = smb2.Empty() smbServer.log('User %s\\%s authenticated successfully' % ( authenticateMessage['host_name'].decode('utf-16le'), authenticateMessage['user_name'].decode('utf-16le'))) @@ -3041,7 +3131,7 @@ def smb2SessionSetup(connId, smbServer, recvPacket): smbServer.log(ntlm_hash_data['hash_string']) if jtr_dump_path != '': writeJohnOutputToFile(ntlm_hash_data['hash_string'], ntlm_hash_data['hash_version'], - jtr_dump_path) + jtr_dump_path) except: smbServer.log("Could not write NTLM Hashes to the specified JTR_Dump_Path %s" % jtr_dump_path) @@ -3069,10 +3159,80 @@ def smb2SessionSetup(connId, smbServer, recvPacket): else: raise Exception("Unknown NTLMSSP MessageType %d" % messageType) - respSMBCommand['SecurityBufferOffset'] = 0x48 respSMBCommand['SecurityBufferLength'] = len(respToken) respSMBCommand['Buffer'] = respToken.getData() + return respSMBCommand, errorCode + + @staticmethod + def generic_negTokenResp(): + accept = SPNEGO_NegTokenResp() + accept['SupportedMech'] = TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider'] + # request-mic + accept['NegState'] = b'\x03' + acceptBytes = accept.getData() + + respSMBCommand = smb2.SMB2SessionSetup_Response() + respSMBCommand['SecurityBufferOffset'] = 0x48 + respSMBCommand['SecurityBufferLength'] = len(acceptBytes) + respSMBCommand['Buffer'] = acceptBytes + return respSMBCommand + + @staticmethod + def smb2SessionSetup(connId, smbServer, recvPacket): + connData = smbServer.getConnectionData(connId, checkStatus=False) + + sessionSetupData = smb2.SMB2SessionSetup(recvPacket['Data']) + + connData['Capabilities'] = sessionSetupData['Capabilities'] + + securityBlob = sessionSetupData['Buffer'] + + rawNTLM = False + authType = None + if struct.unpack('B', securityBlob[0:1])[0] == ASN1_AID: + # NEGOTIATE packet + blob = SPNEGO_NegTokenInit(securityBlob) + token = blob['MechToken'] + if len(blob['MechTypes'][0]) > 0: + # Is this GSSAPI NTLM or something else we don't support? + authType = blob['MechTypes'][0] + supported_mechtypes = [] + if smbServer._SMBSERVER__KerberosSupport and smbServer.getComputerAccountCredentials()["username"]: + # if computer account credentials are provided, we can also use kerberos + supported_mechtypes += [TypesMech['MS KRB5 - Microsoft Kerberos 5'], TypesMech['KRB5 - Kerberos 5'], TypesMech['KRB5 - Kerberos 5 - User to User']] + if smbServer._SMBSERVER__NTLMSupport: + supported_mechtypes += [TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider']] + if authType not in supported_mechtypes: + # Nope, do we know it? + if authType in MechTypes: + mechStr = MechTypes[authType] + else: + mechStr = hexlify(authType) + smbServer.log("Unsupported MechType '%s'" % mechStr, logging.DEBUG, connData=connData) + + return [SMB2Commands.generic_negTokenResp()], None, STATUS_MORE_PROCESSING_REQUIRED + elif struct.unpack('B', securityBlob[0:1])[0] == ASN1_SUPPORTED_MECH: + # AUTH packet + blob = SPNEGO_NegTokenResp(securityBlob) + token = blob['ResponseToken'] + if b'NTLMSSP\x00' in token: + authType = TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider'] + else: + authType = TypesMech['MS KRB5 - Microsoft Kerberos 5'] + elif securityBlob.startswith(b'NTLMSSP\x00'): + # No GSSAPI stuff, raw NTLMSSP + rawNTLM = True + token = securityBlob + authType = TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider'] + else: + smbServer.log("Unknown security blob type (not rawNTLMSSP, nor SPNEGO)", logging.ERROR, connData=connData) + return [SMB2Commands.generic_negTokenResp()], None, STATUS_MORE_PROCESSING_REQUIRED + + if authType in [TypesMech['MS KRB5 - Microsoft Kerberos 5'], TypesMech['KRB5 - Kerberos 5'], TypesMech['KRB5 - Kerberos 5 - User to User']]: + respSMBCommand, errorCode = SMB2Commands._kerberos_auth(token, connData, smbServer) + elif authType == TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider']: + respSMBCommand, errorCode = SMB2Commands._ntlm_auth(token, connData, smbServer, rawNTLM) # From now on, the client can ask for other commands connData['Authenticated'] = True @@ -3106,6 +3266,7 @@ def smb2TreeConnect(connId, smbServer, recvPacket): ## Process here the request, does the share exist? path = recvPacket.getData()[treeConnectRequest['PathOffset']:][:treeConnectRequest['PathLength']] UNCOrShare = path.decode('utf-16le') + smbServer.log("smb2TreeConnect: %s" % UNCOrShare, logging.INFO, connData=connData) # Is this a UNC? if ntpath.ismount(UNCOrShare): @@ -3125,7 +3286,7 @@ def smb2TreeConnect(connId, smbServer, recvPacket): respPacket['TreeID'] = tid smbServer.log("Connecting Share(%d:%s)" % (tid, path)) else: - smbServer.log("SMB2_TREE_CONNECT not found %s" % path, logging.ERROR) + smbServer.log("SMB2_TREE_CONNECT not found %s" % path, logging.ERROR, connData=connData) errorCode = STATUS_OBJECT_PATH_NOT_FOUND respPacket['Status'] = errorCode ## @@ -3160,6 +3321,7 @@ def smb2Create(connId, smbServer, recvPacket): respSMBCommand['Buffer'] = b'\x00' # Get the Tid associated if recvPacket['TreeID'] in connData['ConnectedShares']: + readOnly = connData['ConnectedShares'][recvPacket['TreeID']]["read only"] == "yes" # If we have a rootFid, the path is relative to that fid errorCode = STATUS_SUCCESS if 'path' in connData['ConnectedShares'][recvPacket['TreeID']]: @@ -3171,6 +3333,7 @@ def smb2Create(connId, smbServer, recvPacket): deleteOnClose = False fileName = normalize_path(ntCreateRequest['Buffer'][:ntCreateRequest['NameLength']].decode('utf-16le')) + smbServer.log("smb2Create: %s" % fileName, logging.INFO, connData=connData) if not isInFileJail(path, fileName): LOG.error("Path not in current working directory") @@ -3215,14 +3378,16 @@ def smb2Create(connId, smbServer, recvPacket): createOptions = ntCreateRequest['CreateOptions'] if mode & os.O_CREAT == os.O_CREAT: - if createOptions & smb2.FILE_DIRECTORY_FILE == smb2.FILE_DIRECTORY_FILE: + if createOptions & smb2.FILE_DIRECTORY_FILE == smb2.FILE_DIRECTORY_FILE and not readOnly: try: # Let's create the directory os.mkdir(pathName) mode = os.O_RDONLY except Exception as e: - smbServer.log("SMB2_CREATE: %s,%s,%s" % (pathName, mode, e), logging.ERROR) + smbServer.log("SMB2_CREATE: %s,%s,%s" % (pathName, mode, e), logging.ERROR, connData=connData) errorCode = STATUS_ACCESS_DENIED + elif readOnly: + errorCode = STATUS_ACCESS_DENIED if createOptions & smb2.FILE_NON_DIRECTORY_FILE == smb2.FILE_NON_DIRECTORY_FILE: # If the file being opened is a directory, the server MUST fail the request with # STATUS_FILE_IS_A_DIRECTORY in the Status field of the SMB Header in the server @@ -3245,9 +3410,11 @@ def smb2Create(connId, smbServer, recvPacket): sock = socket.socket() sock.connect(smbServer.getRegisteredNamedPipes()[ensure_str(pathName)]) else: + if readOnly: + mode = os.O_RDONLY fid = os.open(pathName, mode) except Exception as e: - smbServer.log("SMB2_CREATE: %s,%s,%s" % (pathName, mode, e), logging.ERROR) + smbServer.log("SMB2_CREATE: %s,%s,%s" % (pathName, mode, e), logging.ERROR, connData=connData) # print e fid = 0 errorCode = STATUS_ACCESS_DENIED @@ -3288,7 +3455,7 @@ def smb2Create(connId, smbServer, recvPacket): if errorCode == STATUS_SUCCESS: # Let's store the fid for the connection - # smbServer.log('Create file %s, mode:0x%x' % (pathName, mode)) + # smbServer.log('Create file %s, mode:0x%x' % (pathName, mode), connData=connData) connData['OpenedFiles'][fakefid] = {} connData['OpenedFiles'][fakefid]['FileHandle'] = fid connData['OpenedFiles'][fakefid]['FileName'] = pathName @@ -3326,10 +3493,12 @@ def smb2Close(connId, smbServer, recvPacket): # Get the Tid associated if recvPacket['TreeID'] in connData['ConnectedShares']: + readOnly = connData['ConnectedShares'][recvPacket['TreeID']]["read only"] == "yes" if fileID in connData['OpenedFiles']: errorCode = STATUS_SUCCESS fileHandle = connData['OpenedFiles'][fileID]['FileHandle'] pathName = connData['OpenedFiles'][fileID]['FileName'] + smbServer.log("smb2Close: %s" % pathName, logging.INFO, connData=connData) infoRecord = None try: if fileHandle == PIPE_FILE_DESCRIPTOR: @@ -3339,19 +3508,22 @@ def smb2Close(connId, smbServer, recvPacket): infoRecord, errorCode = queryFileInformation(os.path.dirname(pathName), os.path.basename(pathName), smb2.SMB2_FILE_NETWORK_OPEN_INFO) except Exception as e: - smbServer.log("SMB2_CLOSE %s" % e, logging.ERROR) + smbServer.log("SMB2_CLOSE %s" % e, logging.ERROR, connData=connData) errorCode = STATUS_INVALID_HANDLE else: # Check if the file was marked for removal if connData['OpenedFiles'][fileID]['DeleteOnClose'] is True: - try: - if os.path.isdir(pathName): - shutil.rmtree(connData['OpenedFiles'][fileID]['FileName']) - else: - os.remove(connData['OpenedFiles'][fileID]['FileName']) - except Exception as e: - smbServer.log("SMB2_CLOSE %s" % e, logging.ERROR) + if readOnly: errorCode = STATUS_ACCESS_DENIED + else: + try: + if os.path.isdir(pathName): + shutil.rmtree(connData['OpenedFiles'][fileID]['FileName']) + else: + os.remove(connData['OpenedFiles'][fileID]['FileName']) + except Exception as e: + smbServer.log("SMB2_CLOSE %s" % e, logging.ERROR, connData=connData) + errorCode = STATUS_ACCESS_DENIED # Now fill out the response if infoRecord is not None: @@ -3398,6 +3570,7 @@ def smb2QueryInfo(connId, smbServer, recvPacket): if recvPacket['TreeID'] in connData['ConnectedShares']: if fileID in connData['OpenedFiles']: fileName = connData['OpenedFiles'][fileID]['FileName'] + smbServer.log("smb2QueryInfo: %s" % fileName, logging.INFO, connData=connData) if queryInfo['InfoType'] == smb2.SMB2_0_INFO_FILE: if queryInfo['FileInfoClass'] == smb2.SMB2_FILE_INTERNAL_INFO: @@ -3419,7 +3592,7 @@ def smb2QueryInfo(connId, smbServer, recvPacket): infoRecord = None errorCode = STATUS_ACCESS_DENIED else: - smbServer.log("queryInfo not supported (%x)" % queryInfo['InfoType'], logging.ERROR) + smbServer.log("queryInfo not supported (%x)" % queryInfo['InfoType'], logging.ERROR, connData=connData) if infoRecord is not None: respSMBCommand['OutputBufferLength'] = len(infoRecord) @@ -3454,8 +3627,10 @@ def smb2SetInfo(connId, smbServer, recvPacket): # Get the Tid associated if recvPacket['TreeID'] in connData['ConnectedShares']: path = connData['ConnectedShares'][recvPacket['TreeID']]['path'] + readOnly = connData['ConnectedShares'][recvPacket['TreeID']]["read only"] == "yes" if fileID in connData['OpenedFiles']: pathName = connData['OpenedFiles'][fileID]['FileName'] + smbServer.log("smb2SetInfo: %s" % pathName, logging.INFO, connData=connData) if setInfo['InfoType'] == smb2.SMB2_0_INFO_FILE: # The file information is being set @@ -3494,23 +3669,26 @@ def smb2SetInfo(connId, smbServer, recvPacket): newFileName = normalize_path(renameInfo['FileName'].decode('utf-16le')) newPathName = os.path.join(path, newFileName) if not isInFileJail(path, newFileName): - smbServer.log("Path not in current working directory", logging.ERROR) + smbServer.log("Path not in current working directory", logging.ERROR, connData=connData) return [smb2.SMB2Error()], None, STATUS_OBJECT_PATH_SYNTAX_BAD if renameInfo['ReplaceIfExists'] == 0 and os.path.exists(newPathName): return [smb2.SMB2Error()], None, STATUS_OBJECT_NAME_COLLISION - try: - os.rename(pathName, newPathName) - connData['OpenedFiles'][fileID]['FileName'] = newPathName - except Exception as e: - smbServer.log("smb2SetInfo: %s" % e, logging.ERROR) + if readOnly: errorCode = STATUS_ACCESS_DENIED + else: + try: + os.rename(pathName, newPathName) + connData['OpenedFiles'][fileID]['FileName'] = newPathName + except Exception as e: + smbServer.log("smb2SetInfo: %s" % e, logging.ERROR, connData=connData) + errorCode = STATUS_ACCESS_DENIED elif informationLevel == smb2.SMB2_FILE_ALLOCATION_INFO: # See https://github.com/samba-team/samba/blob/master/source3/smbd/smb2_trans2.c#LL5201C8-L5201C39 smbServer.log("Warning: SMB2_FILE_ALLOCATION_INFO not implemented") errorCode = STATUS_SUCCESS else: - smbServer.log('Unknown level for set file info! 0x%x' % informationLevel, logging.ERROR) + smbServer.log('Unknown level for set file info! 0x%x' % informationLevel, logging.ERROR, connData=connData) # UNSUPPORTED errorCode = STATUS_NOT_SUPPORTED # elif setInfo['InfoType'] == smb2.SMB2_0_INFO_FILESYSTEM: @@ -3525,7 +3703,7 @@ def smb2SetInfo(connId, smbServer, recvPacket): # # The underlying object store quota information is being set. # setInfo = queryFsInformation('/', fileName, queryInfo['FileInfoClass']) else: - smbServer.log("setInfo not supported (%x)" % setInfo['InfoType'], logging.ERROR) + smbServer.log("setInfo not supported (%x)" % setInfo['InfoType'], logging.ERROR, connData=connData) else: errorCode = STATUS_INVALID_HANDLE @@ -3557,6 +3735,8 @@ def smb2Write(connId, smbServer, recvPacket): if recvPacket['TreeID'] in connData['ConnectedShares']: if fileID in connData['OpenedFiles']: fileHandle = connData['OpenedFiles'][fileID]['FileHandle'] + fileName = connData['OpenedFiles'][fileID]['FileName'] + smbServer.log("smb2Write: %s" % fileName, logging.INFO, connData=connData) errorCode = STATUS_SUCCESS try: if fileHandle != PIPE_FILE_DESCRIPTOR: @@ -3572,7 +3752,7 @@ def smb2Write(connId, smbServer, recvPacket): respSMBCommand['Count'] = writeRequest['Length'] respSMBCommand['Remaining'] = 0xff except Exception as e: - smbServer.log('SMB2_WRITE: %s' % e, logging.ERROR) + smbServer.log('SMB2_WRITE: %s' % e, logging.ERROR, connData=connData) errorCode = STATUS_ACCESS_DENIED else: errorCode = STATUS_INVALID_HANDLE @@ -3604,6 +3784,8 @@ def smb2Read(connId, smbServer, recvPacket): if recvPacket['TreeID'] in connData['ConnectedShares']: if fileID in connData['OpenedFiles']: fileHandle = connData['OpenedFiles'][fileID]['FileHandle'] + fileName = connData['OpenedFiles'][fileID]['FileName'] + smbServer.log("smb2Read: %s" % fileName, logging.INFO, connData=connData) errorCode = 0 try: if fileHandle != PIPE_FILE_DESCRIPTOR: @@ -3619,7 +3801,7 @@ def smb2Read(connId, smbServer, recvPacket): respSMBCommand['DataRemaining'] = 0 respSMBCommand['Buffer'] = content except Exception as e: - smbServer.log('SMB2_READ: %s ' % e, logging.ERROR) + smbServer.log('SMB2_READ: %s ' % e, logging.ERROR, connData=connData) errorCode = STATUS_ACCESS_DENIED else: errorCode = STATUS_INVALID_HANDLE @@ -3638,13 +3820,16 @@ def smb2Flush(connId, smbServer, recvPacket): # Get the Tid associated if recvPacket['TreeID'] in connData['ConnectedShares']: - if flushRequest['FileID'].getData() in connData['OpenedFiles']: - fileHandle = connData['OpenedFiles'][flushRequest['FileID'].getData()]['FileHandle'] + fileID = flushRequest['FileID'].getData() + if fileID in connData['OpenedFiles']: + fileHandle = connData['OpenedFiles'][fileID]['FileHandle'] + fileName = connData['OpenedFiles'][fileID]['FileName'] + smbServer.log("smb2Flush: %s" % fileName, logging.INFO, connData=connData) errorCode = STATUS_SUCCESS try: os.fsync(fileHandle) except Exception as e: - smbServer.log("SMB2_FLUSH %s" % e, logging.ERROR) + smbServer.log("SMB2_FLUSH %s" % e, logging.ERROR, connData=connData) errorCode = STATUS_ACCESS_DENIED else: errorCode = STATUS_INVALID_HANDLE @@ -3679,6 +3864,8 @@ def smb2QueryDirectory(connId, smbServer, recvPacket): if (fileID in connData['OpenedFiles']) is False: return [smb2.SMB2Error()], None, STATUS_FILE_CLOSED + fileName = connData['OpenedFiles'][fileID]['FileName'] + smbServer.log("smb2QueryDirectory: %s" % fileName, logging.INFO, connData=connData) # If the open is not an open to a directory, the request MUST be failed # with STATUS_INVALID_PARAMETER. @@ -3814,7 +4001,7 @@ def smb2TreeDisconnect(connId, smbServer, recvPacket): # Get the Tid associated if recvPacket['TreeID'] in connData['ConnectedShares']: smbServer.log("Disconnecting Share(%d:%s)" % ( - recvPacket['TreeID'], connData['ConnectedShares'][recvPacket['TreeID']]['shareName'])) + recvPacket['TreeID'], connData['ConnectedShares'][recvPacket['TreeID']]['shareName']), connData=connData) del (connData['ConnectedShares'][recvPacket['TreeID']]) errorCode = STATUS_SUCCESS else: @@ -3863,7 +4050,7 @@ def smb2Ioctl(connId, smbServer, recvPacket): else: respSMBCommand = outputData else: - smbServer.log("Ioctl not implemented command: 0x%x" % ioctlRequest['CtlCode'], logging.DEBUG) + smbServer.log("Ioctl not implemented command: 0x%x" % ioctlRequest['CtlCode'], logging.DEBUG, connData=connData) errorCode = STATUS_INVALID_DEVICE_REQUEST respSMBCommand = smb2.SMB2Error() @@ -3916,7 +4103,7 @@ def fsctlPipeTransceive(connId, smbServer, ioctlRequest): sock.sendall(ioctlRequest['Buffer']) ioctlResponse = sock.recv(ioctlRequest['MaxOutputResponse']) except Exception as e: - smbServer.log('fsctlPipeTransceive: %s ' % e, logging.ERROR) + smbServer.log('fsctlPipeTransceive: %s ' % e, logging.ERROR, connData=connData) errorCode = STATUS_ACCESS_DENIED else: errorCode = STATUS_INVALID_DEVICE_REQUEST @@ -3990,8 +4177,8 @@ def handle(self): session.send_packet(i) except Exception as e: self.__SMB.log("Handle: %s" % e) - # import traceback - # traceback.print_exc() + import traceback + traceback.print_exc() break def finish(self): @@ -4014,6 +4201,13 @@ def __init__(self, server_address, handler_class=SMBSERVERHandler, config_parser self.__challenge = '' self.__log = None + self.__computerAccountName = '' + self.__computerAccountNTHash = '' + self.__computerAccountAES = '' + self.__computerAccountPassword = '' + self.__computerAccountDomain = '' + self.__domainControllerIP = '' + # Our ConfigParser data self.__serverConfig = config_parser @@ -4032,6 +4226,12 @@ def __init__(self, server_address, handler_class=SMBSERVERHandler, config_parser # SMB2 Support flag = default not active self.__SMB2Support = False + # Kerberos Support flag + self.__KerberosSupport = True + + # NTLM Support flag + self.__NTLMSupport = True + # Allow anonymous logon self.__anonymousLogon = True @@ -4334,7 +4534,11 @@ def hookSmb2Command(self, smb2Command, callback): self.__smb2Commands[smb2Command] = callback return originalCommand - def log(self, msg, level=logging.INFO): + def log(self, msg, level=logging.INFO, connData=None): + if connData and connData.get('AUTHENTICATE_MESSAGE'): + domain = connData['AUTHENTICATE_MESSAGE']['domain_name'].decode('utf-16le') or connData['AUTHENTICATE_MESSAGE']['host_name'].decode('utf-16le') + username = connData['AUTHENTICATE_MESSAGE']['user_name'].decode('utf-16le') or "NULL" + msg = f"{domain}\\{username}: " + msg self.__log.log(level, msg) def getServerName(self): @@ -4668,6 +4872,15 @@ def processConfigFile(self, configFile=None): self.__serverName = self.__serverConfig.get('global', 'server_name') self.__serverOS = self.__serverConfig.get('global', 'server_os') self.__serverDomain = self.__serverConfig.get('global', 'server_domain') + + if self.__serverConfig.has_option('global', 'computer_account_name'): + self.__computerAccountName = self.__serverConfig.get('global', 'computer_account_name') + self.__computerAccountNTHash = self.__serverConfig.get('global', 'computer_account_hash') + self.__computerAccountAES = self.__serverConfig.get('global', 'computer_account_aes') + self.__computerAccountPassword = self.__serverConfig.get('global', 'computer_account_password') + self.__computerAccountDomain = self.__serverConfig.get('global', 'computer_account_domain') + self.__domainControllerIP = self.__serverConfig.get('global', 'dcip') + self.__logFile = self.__serverConfig.get('global', 'log_file') if self.__serverConfig.has_option('global', 'challenge'): self.__challenge = unhexlify(self.__serverConfig.get('global', 'challenge')) @@ -4687,6 +4900,16 @@ def processConfigFile(self, configFile=None): else: self.__SMB2Support = False + if self.__serverConfig.has_option("global", "KerberosSupport"): + self.__KerberosSupport = self.__serverConfig.getboolean("global", "KerberosSupport") + else: + self.__KerberosSupport = True + + if self.__serverConfig.has_option("global", "NTLMSupport"): + self.__NTLMSupport = self.__serverConfig.getboolean("global", "NTLMSupport") + else: + self.__NTLMSupport = True + if self.__serverConfig.has_option("global", "anonymous_logon"): self.__anonymousLogon = self.__serverConfig.getboolean("global", "anonymous_logon") else: @@ -4726,6 +4949,24 @@ def addCredential(self, name, uid, lmhash, nthash): pass self.__credentials[name.lower()] = (uid, lmhash, nthash) + def setComputerAccountCredentials(self, username, domain, dcip, nthash="", aes="", password=""): + self.__computerAccountName = username + self.__computerAccountNTHash = nthash + self.__computerAccountAES = aes + self.__computerAccountPassword = password + self.__computerAccountDomain = domain + self.__domainControllerIP = dcip + + def getComputerAccountCredentials(self): + return { + "username": self.__computerAccountName, + "nthash": self.__computerAccountNTHash, + "aes": self.__computerAccountAES, + "password": self.__computerAccountPassword, + "domain": self.__computerAccountDomain, + "dcip": self.__domainControllerIP + } + # For windows platforms, opening a directory is not an option, so we set a void FD VOID_FILE_DESCRIPTOR = -1 @@ -4987,6 +5228,22 @@ def setCredentialsFile(self, logFile): def addCredential(self, name, uid, lmhash, nthash): self.__server.addCredential(name, uid, lmhash, nthash) + def setComputerAccount(self, computer_account_name, computer_account_hash, computer_account_aes, computer_account_password, computer_account_domain, dcip): + # needs to be correct for netlogon to allow us to authenticate the user + self.__smbConfig.set('global', 'server_name', computer_account_name[:-1]) # assume that the computer account ends with a $ + self.__smbConfig.set('global', 'server_domain', computer_account_domain) + + self.__smbConfig.set('global', 'computer_account_name', computer_account_name) + self.__smbConfig.set('global', 'computer_account_hash', computer_account_hash or "") + self.__smbConfig.set('global', 'computer_account_aes', computer_account_aes or "") + self.__smbConfig.set('global', 'computer_account_password', computer_account_password or "") + self.__smbConfig.set('global', 'computer_account_domain', computer_account_domain) + self.__smbConfig.set('global', 'dcip', dcip) + + self.__server.setServerConfig(self.__smbConfig) + self.__server.processConfigFile() + + def setSMB2Support(self, value): if value is True: self.__smbConfig.set("global", "SMB2Support", "True") @@ -4995,8 +5252,104 @@ def setSMB2Support(self, value): self.__server.setServerConfig(self.__smbConfig) self.__server.processConfigFile() + def setNTLMSupport(self, value): + if value is True: + self.__smbConfig.set("global", "NTLMSupport", "True") + else: + self.__smbConfig.set("global", "NTLMSupport", "False") + self.__server.setServerConfig(self.__smbConfig) + self.__server.processConfigFile() + + def setKerberosSupport(self, value): + if value is True: + self.__smbConfig.set("global", "KerberosSupport", "True") + else: + self.__smbConfig.set("global", "KerberosSupport", "False") + self.__server.setServerConfig(self.__smbConfig) + self.__server.processConfigFile() + def getAuthCallback(self): return self.__server.getAuthCallback() def setAuthCallback(self, callback): self.__server.setAuthCallback(callback) + + +# https://gist.github.com/ThePirateWhoSmellsOfSunflowers/f41c334f912ec033d9bbfc7e96308ec6 +class NetLogon: + nrpc_uid = nrpc.MSRPC_UUID_NRPC + syntax = rpcrt.DCERPC.NDRSyntax + authn_level_packet = rpcrt.RPC_C_AUTHN_LEVEL_PKT_PRIVACY # KB5021130 + + def __init__(self, dcip, computer_account_name, computer_account_hash, computer_account_domain, client_challenge=random.randbytes(8), computer_name=None, primary_name=""): + self.dcip = dcip + self.computer_account_name = computer_account_name + if computer_name is not None: + self.computer_name = computer_name + else: + self.computer_name = computer_account_name[:-1] # strip $ + self.computer_account_hash = unhexlify(computer_account_hash) + self.computer_account_domain = computer_account_domain + self.client_challenge = client_challenge + self.primary_name = primary_name + self.authenticator = None + self.dce = None + + def setupConnection(self): + binding_string_nrpc = epm.hept_map(self.dcip, self.nrpc_uid, dataRepresentation=self.syntax, protocol='ncacn_ip_tcp') + rpctransport = transport.DCERPCTransportFactory(binding_string_nrpc) + dce = rpctransport.get_dce_rpc() + dce.connect() + dce.bind(nrpc.MSRPC_UUID_NRPC, transfer_syntax=uuid.bin_to_uuidtup(self.syntax)) + + resp = nrpc.hNetrServerReqChallenge(dce, self.primary_name, self.computer_name + '\x00', self.client_challenge) + serverchall = resp["ServerChallenge"] + sessionKey = nrpc.ComputeSessionKeyStrongKey(None, self.client_challenge, serverchall, self.computer_account_hash) + clientcred = nrpc.ComputeNetlogonCredential(self.client_challenge, sessionKey) + resp = nrpc.hNetrServerAuthenticate3(dce, self.primary_name + '\x00', self.computer_account_name + '\x00', + nrpc.NETLOGON_SECURE_CHANNEL_TYPE.WorkstationSecureChannel, + self.computer_name + '\x00', clientcred, 0x600FFFFF) + + dce.set_credentials(self.computer_account_name, "THIS_IS_IGNORED_IN_IMPACKET", self.computer_account_domain) + dce.set_auth_type(rpcrt.RPC_C_AUTHN_NETLOGON) + dce.set_auth_level(self.authn_level_packet) + + resp = dce.bind(nrpc.MSRPC_UUID_NRPC, alter=1, transfer_syntax=uuid.bin_to_uuidtup(self.syntax)) + + auth = nrpc.ComputeNetlogonAuthenticator(clientcred, sessionKey) + + dce.set_session_key(sessionKey) + resp = nrpc.hNetrLogonGetCapabilities(dce, self.primary_name, self.computer_name, auth) + self.authenticator = resp['ReturnAuthenticator'] + self.dce = dce + + def logonUserAndGetSessionKey(self, authenticateMessage, serverChallenge): + request = nrpc.NetrLogonSamLogonWithFlags() + request['LogonServer'] = '\x00' + request['ComputerName'] = self.computer_name + '\x00' + request['ValidationLevel'] = nrpc.NETLOGON_VALIDATION_INFO_CLASS.NetlogonValidationSamInfo4 + + request['LogonLevel'] = nrpc.NETLOGON_LOGON_INFO_CLASS.NetlogonNetworkTransitiveInformation + request['LogonInformation']['tag'] = nrpc.NETLOGON_LOGON_INFO_CLASS.NetlogonNetworkTransitiveInformation + request['LogonInformation']['LogonNetworkTransitive']['Identity']['LogonDomainName'] = authenticateMessage['domain_name'].decode('utf-16le') + + # MS-APDS: 3.1.5.2 NTLM Network Logon: If the account is a computer account, the subauthentication package is not verified, and the K bit of LogonInformation.LogonNetwork.Identity.ParameterControl is not set, then return STATUS_NOLOGON_WORKSTATION_TRUST_ACCOUNT.<21> + # MS-NRPC: 2.2.1.4.15 NETLOGON_LOGON_IDENTITY_INFO: K=20 + request['LogonInformation']['LogonNetworkTransitive']['Identity']['ParameterControl'] = 2**11 + request['LogonInformation']['LogonNetworkTransitive']['Identity']['UserName'] = authenticateMessage['user_name'].decode('utf-16le') + request['LogonInformation']['LogonNetworkTransitive']['Identity']['Workstation'] = '' + + request['LogonInformation']['LogonNetworkTransitive']['LmChallenge'] = serverChallenge + request['LogonInformation']['LogonNetworkTransitive']['NtChallengeResponse'] = authenticateMessage['ntlm'] + request['LogonInformation']['LogonNetworkTransitive']['LmChallengeResponse'] = authenticateMessage['lanman'] + + request['Authenticator'] = self.authenticator + request['ReturnAuthenticator']['Credential'] = b'\x00'*8 + request['ReturnAuthenticator']['Timestamp'] = 0 + request['ExtraFlags'] = 0 + + resp = self.dce.request(request) + #resp.dump() + + signingKey = ntlm.generateEncryptedSessionKey(resp['ValidationInformation']['ValidationSam4']['UserSessionKey'], authenticateMessage['session_key']) + return signingKey, resp['ErrorCode'] \ No newline at end of file