def _verify(self, check, actual, expected): log_header = "Session: %d, Tree: %d"\ % (self.session.session_id, self.tree_connect_id) if actual != expected: raise SMBException("%s - Secure negotiate failed to verify %s, " "Actual: %s, Expected: %s" % (log_header, check, actual, expected))
def read(self, offset, length, min_length=0, unbuffered=False, wait=True, send=True): """ Reads from an opened file or pipe Supports out of band send function, call this function with send=False to return a tuple of (SMB2ReadRequest, receive_func) instead of sending the the request and waiting for the response. The receive_func can be used to get the response from the server by passing in the Request that was used to sent it out of band. :param offset: The offset to start the read of the file. :param length: The number of bytes to read from the offset. :param min_length: The minimum number of bytes to be read for a successful operation. :param unbuffered: Whether to the server should cache the read data at intermediate layers, only value for SMB 3.0.2 or newer :param wait: If send=True, whether to wait for a response if STATUS_PENDING was received from the server or fail. :param send: Whether to send the request in the same call or return the message to the caller and the unpack function :return: A byte string of the bytes read """ if length > self.connection.max_read_size: raise SMBException("The requested read length %d is greater than " "the maximum negotiated read size %d" % (length, self.connection.max_read_size)) read = SMB2ReadRequest() read['length'] = length read['offset'] = offset read['minimum_count'] = min_length read['file_id'] = self.file_id read['padding'] = b"\x50" if unbuffered: if self.connection.dialect < Dialects.SMB_3_0_2: raise SMBUnsupportedFeature(self.connection.dialect, Dialects.SMB_3_0_2, "SMB2_READFLAG_READ_UNBUFFERED", True) read['flags'].set_flag(ReadFlags.SMB2_READFLAG_READ_UNBUFFERED) if not send: return read, self._read_response log.info("Session: %s, Tree Connect ID: %s - sending SMB2 Read " "Request for file %s" % (self.tree_connect.session.username, self.tree_connect.share_name, self.file_name)) log.debug(str(read)) request = self.connection.send(read, self.tree_connect.session.session_id, self.tree_connect.tree_connect_id) return self._read_response(request, wait)
def test_exception(self): with pytest.raises(SMBException) as exc: raise SMBException("smb error") assert str(exc.value) == "smb error"
def write(self, data, offset=0, write_through=False, unbuffered=False, wait=True, send=True): """ Writes data to an opened file. Supports out of band send function, call this function with send=False to return a tuple of (SMBWriteRequest, receive_func) instead of sending the the request and waiting for the response. The receive_func can be used to get the response from the server by passing in the Request that was used to sent it out of band. :param data: The bytes data to write. :param offset: The offset in the file to write the bytes at :param write_through: Whether written data is persisted to the underlying storage, not valid for SMB 2.0.2. :param unbuffered: Whether to the server should cache the write data at intermediate layers, only value for SMB 3.0.2 or newer :param wait: If send=True, whether to wait for a response if STATUS_PENDING was received from the server or fail. :param send: Whether to send the request in the same call or return the message to the caller and the unpack function :return: The number of bytes written """ data_len = len(data) if data_len > self.connection.max_write_size: raise SMBException("The requested write length %d is greater than " "the maximum negotiated write size %d" % (data_len, self.connection.max_write_size)) write = SMB2WriteRequest() write['length'] = len(data) write['offset'] = offset write['file_id'] = self.file_id write['buffer'] = data if write_through: if self.connection.dialect < Dialects.SMB_2_1_0: raise SMBUnsupportedFeature(self.connection.dialect, Dialects.SMB_2_1_0, "SMB2_WRITEFLAG_WRITE_THROUGH", True) write['flags'].set_flag(WriteFlags.SMB2_WRITEFLAG_WRITE_THROUGH) if unbuffered: if self.connection.dialect < Dialects.SMB_3_0_2: raise SMBUnsupportedFeature(self.connection.dialect, Dialects.SMB_3_0_2, "SMB2_WRITEFLAG_WRITE_UNBUFFERED", True) write['flags'].set_flag(WriteFlags.SMB2_WRITEFLAG_WRITE_UNBUFFERED) if not send: return write, self._write_response log.info("Session: %s, Tree Connect: %s - sending SMB2 Write Request " "for file %s" % (self.tree_connect.session.username, self.tree_connect.share_name, self.file_name)) log.debug(str(write)) request = self.connection.send(write, self.tree_connect.session.session_id, self.tree_connect.tree_connect_id) return self._write_response(request, wait)
def connect(self): log.debug("Decoding SPNEGO token containing supported auth mechanisms") context = spnego.client(self.username, self.password, service='cifs', hostname=self.connection.server_name, options=spnego.NegotiateOptions.session_key) self.connection.preauth_session_table[self.session_id] = self in_token = self.connection.gss_negotiate_token while not context.complete or not in_token: try: out_token = context.step(in_token) except spnego.exceptions.SpnegoError as err: raise SMBAuthenticationError( "Failed to authenticate with server: %s" % str(err.message)) if not out_token: break session_setup = SMB2SessionSetupRequest() session_setup[ 'security_mode'] = self.connection.client_security_mode session_setup['buffer'] = out_token log.info("Sending SMB2_SESSION_SETUP request message") request = self.connection.send(session_setup, sid=self.session_id, credit_request=256) log.info("Receiving SMB2_SESSION_SETUP response message") try: response = self.connection.receive(request) except MoreProcessingRequired as exc: mid = request.message['message_id'].get_value() del self.connection.outstanding_requests[mid] response = exc.header # If this is the first time we received the actual session_id, update the preauth table with the server # assigned id. session_id = response['session_id'].get_value() if self.session_id < 0: del self.connection.preauth_session_table[self.session_id] self.connection.preauth_session_table[session_id] = self self.session_id = session_id setup_response = SMB2SessionSetupResponse() setup_response.unpack(response['data'].get_value()) in_token = setup_response['buffer'].get_value() status = response['status'].get_value() if status == NtStatus.STATUS_MORE_PROCESSING_REQUIRED: log.info("More processing is required for SMB2_SESSION_SETUP") preauth_value = self.connection.preauth_session_table.pop( response['message_id'].get_value()) self.preauth_integrity_hash_value.append(preauth_value) log.info("Setting session id to %s" % self.session_id) self._connected = True # Move the session from the preauth table to the actual session table. self.connection.session_table[ self.session_id] = self.connection.preauth_session_table.pop( self.session_id) # session_key is the first 16 bytes, padded 0 if less than 16 self.session_key = context.session_key[:16].ljust(16, b"\x00") if self.connection.dialect >= Dialects.SMB_3_1_1: preauth_hash = b"\x00" * 64 hash_al = self.connection.preauth_integrity_hash_id for hash_list in [ self.connection.preauth_integrity_hash_value, self.preauth_integrity_hash_value ]: for message in hash_list: preauth_hash = hash_al(preauth_hash + message).digest() self.signing_key = self._smb3kdf(self.session_key, b"SMBSigningKey\x00", preauth_hash) self.application_key = self._smb3kdf(self.session_key, b"SMBAppKey\x00", preauth_hash) self.encryption_key = self._smb3kdf(self.session_key, b"SMBC2SCipherKey\x00", preauth_hash) self.decryption_key = self._smb3kdf(self.session_key, b"SMBS2CCipherKey\x00", preauth_hash) elif self.connection.dialect >= Dialects.SMB_3_0_0: self.signing_key = self._smb3kdf(self.session_key, b"SMB2AESCMAC\x00", b"SmbSign\x00") self.application_key = self._smb3kdf(self.session_key, b"SMB2APP\x00", b"SmbRpc\x00") self.encryption_key = self._smb3kdf(self.session_key, b"SMB2AESCCM\x00", b"ServerIn \x00") self.decryption_key = self._smb3kdf(self.session_key, b"SMB2AESCCM\x00", b"ServerOut\x00") else: self.signing_key = self.session_key self.application_key = self.session_key flags = setup_response['session_flags'] if flags.has_flag(SessionFlags.SMB2_SESSION_FLAG_ENCRYPT_DATA ) or self.require_encryption: # make sure the connection actually supports encryption if not self.connection.supports_encryption: raise SMBException( "SMB encryption is required but the connection does not support it" ) self.encrypt_data = True self.signing_required = False # encryption covers signing else: self.encrypt_data = False if flags.has_flag(SessionFlags.SMB2_SESSION_FLAG_IS_GUEST) or \ flags.has_flag(SessionFlags.SMB2_SESSION_FLAG_IS_NULL): self.session_key = None self.signing_key = None self.application_key = None self.encryption_key = None self.decryption_key = None if self.signing_required or self.encrypt_data: self.session_id = None raise SMBException( "SMB encryption or signing was required but session was authenticated as a guest " "which does not support encryption or signing") if self.signing_required: log.info( "Verifying the SMB Setup Session signature as auth is successful" ) self.connection.verify_signature(response, self.session_id, force=True)
def connect(self): log.debug("Decoding SPNEGO token containing supported auth mechanisms") token, rdata = decoder.decode(self.connection.gss_negotiate_token, asn1Spec=InitialContextToken()) server_mechs = list( token['innerContextToken']['negTokenInit']['mechTypes'] ) if MechTypes.MS_KRB5 in server_mechs and MechTypes.KRB5: log.debug("Both MS_KRB5 and KRB5 received in the initial SPNGEO " "token, removing MS_KRB5 to avoid duplication of work") server_mechs.remove(MechTypes.MS_KRB5) # loop through the Mechs until we get a successful auth response = session_key = None errors = {} for mech in server_mechs: mech_key = "Unknown (%s)" % str(mech) for name, value in vars(MechTypes).items(): if isinstance(value, ObjectIdentifier) and value == mech: mech_key = "%s (%s)" % (name, str(value)) break log.info("Attempting auth with mech %s" % mech_key) try: response, session_key = self._authenticate_session(mech) break except Exception as exc: log.warning("Failed auth for mech %s: %s" % (mech_key, str(exc))) errors[str(mech_key)] = str(exc) pass if response is None: raise SMBAuthenticationError("Failed to authenticate with server: " "%s" % str(errors)) log.info("Setting session id to %s" % self.session_id) setup_response = SMB2SessionSetupResponse() setup_response.unpack(response['data'].get_value()) self._connected = True # TODO: remove from preauth session table and move to session_table self.connection.session_table[self.session_id] = self # session_key is the first 16 bytes, padded 0 if less than 16 padding_len = 16 - len(session_key) if len(session_key) < 16 else 0 session_key += b"\x00" * padding_len self.session_key = session_key[:16] if self.connection.dialect >= Dialects.SMB_3_1_1: preauth_hash = b"\x00" * 64 hash_al = self.connection.preauth_integrity_hash_id for message in self.preauth_integrity_hash_value: preauth_hash = hash_al(preauth_hash + message.pack()).digest() self.signing_key = self._smb3kdf(self.session_key, b"SMBSigningKey\x00", preauth_hash) self.application_key = self._smb3kdf(self.session_key, b"SMBAppKey\x00", preauth_hash) self.encryption_key = self._smb3kdf(self.session_key, b"SMBC2SCipherKey\x00", preauth_hash) self.decryption_key = self._smb3kdf(self.session_key, b"SMBS2CCipherKey\x00", preauth_hash) elif self.connection.dialect >= Dialects.SMB_3_0_0: self.signing_key = self._smb3kdf(self.session_key, b"SMB2AESCMAC\x00", b"SmbSign\x00") self.application_key = self._smb3kdf(self.session_key, b"SMB2APP\x00", b"SmbRpc\x00") self.encryption_key = self._smb3kdf(self.session_key, b"SMB2AESCCM\x00", b"ServerIn \x00") self.decryption_key = self._smb3kdf(self.session_key, b"SMB2AESCCM\x00", b"ServerOut\x00") else: self.signing_key = self.session_key self.application_key = self.session_key flags = setup_response['session_flags'] if flags.has_flag(SessionFlags.SMB2_SESSION_FLAG_ENCRYPT_DATA) or \ self.require_encryption: # make sure the connection actually supports encryption if not self.connection.supports_encryption: raise SMBException("SMB encryption is required but the " "connection does not support it") self.encrypt_data = True self.signing_required = False # encryption covers signing else: self.encrypt_data = False if flags.has_flag(SessionFlags.SMB2_SESSION_FLAG_IS_GUEST) or \ flags.has_flag(SessionFlags.SMB2_SESSION_FLAG_IS_NULL): self.session_key = None self.signing_key = None self.application_key = None self.encryption_key = None self.decryption_key = None if self.signing_required or self.encrypt_data: self.session_id = None raise SMBException("SMB encryption or signing was required " "but session was authenticated as a guest " "which does not support encryption or " "signing") if self.signing_required: log.info("Verifying the SMB Setup Session signature as auth is " "successful") self.connection._verify(response, self.session_id, True)
def _verify_dialect_negotiate(self): log_header = "Session: %s, Tree: %s" \ % (self.session.username, self.share_name) log.info("%s - Running secure negotiate process" % log_header) if not self.session.signing_key: # This will only happen if we authenticated with the guest or anonymous user. raise SMBException( 'Cannot verify negotiate information without a session signing key. Authenticate with ' 'a non-guest or anonymous account or set require_secure_negotiate=False to disable the ' 'negotiation info verification checks.') dialect = self.session.connection.dialect if dialect >= Dialects.SMB_3_1_1: # SMB 3.1.1+ uses the negotiation info to generate the signing key so doesn't need this extra exchange. return ioctl_request = SMB2IOCTLRequest() ioctl_request['ctl_code'] = \ CtlCode.FSCTL_VALIDATE_NEGOTIATE_INFO ioctl_request['file_id'] = b"\xff" * 16 val_neg = SMB2ValidateNegotiateInfoRequest() val_neg['capabilities'] = \ self.session.connection.client_capabilities val_neg['guid'] = self.session.connection.client_guid val_neg['security_mode'] = \ self.session.connection.client_security_mode val_neg['dialects'] = \ self.session.connection.negotiated_dialects ioctl_request['buffer'] = val_neg ioctl_request['max_output_response'] = len(val_neg) ioctl_request['flags'] = IOCTLFlags.SMB2_0_IOCTL_IS_FSCTL log.info("%s - Sending Secure Negotiate Validation message" % log_header) log.debug(ioctl_request) request = self.session.connection.send(ioctl_request, sid=self.session.session_id, tid=self.tree_connect_id, force_signature=True) log.info("%s - Receiving secure negotiation response" % log_header) try: response = self.session.connection.receive(request) except (FileClosed, InvalidDeviceRequest, NotSupported) as e: # https://docs.microsoft.com/en-us/archive/blogs/openspecification/smb3-secure-dialect-negotiation # Older dialects may respond with these exceptions, this is expected and we only want to fail if # they are not signed. Check that header signature was signed, fail if it wasn't. The signature, if # present, would have been verified when the connection received the data. if e.header['signature'].get_value() == b'\x00' * 16: raise return # If we received an actual response we want to validate the info provided matches with what was negotiated. ioctl_resp = SMB2IOCTLResponse() ioctl_resp.unpack(response['data'].get_value()) log.debug(ioctl_resp) log.info("%s - Unpacking secure negotiate response info" % log_header) val_resp = SMB2ValidateNegotiateInfoResponse() val_resp.unpack(ioctl_resp['buffer'].get_value()) log.debug(val_resp) self._verify("server capabilities", val_resp['capabilities'].get_value(), self.session.connection.server_capabilities.get_value()) self._verify("server guid", val_resp['guid'].get_value(), self.session.connection.server_guid) self._verify("server security mode", val_resp['security_mode'].get_value(), self.session.connection.server_security_mode) self._verify("server dialect", val_resp['dialect'].get_value(), self.session.connection.dialect) log.info("Session: %d, Tree: %d - Secure negotiate complete" % (self.session.session_id, self.tree_connect_id))