diff --git a/.config/codespell_ignore.txt b/.config/codespell_ignore.txt index ab4d74bac68..7ed3f0caa78 100644 --- a/.config/codespell_ignore.txt +++ b/.config/codespell_ignore.txt @@ -8,6 +8,7 @@ browseable byteorder cace cas +ciph componet comversion cros diff --git a/doc/scapy/layers/smb.rst b/doc/scapy/layers/smb.rst index 0a271d60628..16261c69792 100644 --- a/doc/scapy/layers/smb.rst +++ b/doc/scapy/layers/smb.rst @@ -5,8 +5,6 @@ Scapy provides pretty good support for SMB 2/3 and very partial support of SMB1. You can use the :class:`~scapy.layers.smb2.SMB2_Header` to dissect or build SMB2/3, or :class:`~scapy.layers.smb.SMB_Header` for SMB1. -.. warning:: Encryption is currently not supported in neither the client nor server. - .. _client: SMB 2/3 client @@ -94,6 +92,12 @@ You might be wondering if you can pass the ``HashNT`` of the password of the use If you pay very close attention, you'll notice that in this case we aren't using the :class:`~scapy.layers.spnego.SPNEGOSSP` wrapper. You could have used ``ssp=SPNEGOSSP([t.ssp(1)])``. +**smbclient forcing encryption**: + +.. code:: python + + >>> smbclient("server1.domain.local", "admin", REQUIRE_ENCRYPTION=True) + .. note:: It is also possible to start the :class:`~scapy.layers.smbclient.smbclient` directly from the OS, using the following:: @@ -306,6 +310,15 @@ A share is identified by a ``name`` and a ``path`` (+ an optional description ca readonly=False, ) +**Start a SMB server requiring encryption (two methods)**: + +.. code:: python + + # Method 1: require encryption globally (available in SMB 3.0.0+) + >>> smbserver(..., REQUIRE_ENCRYPTION=True) + # Method 2: for a specific share (only available in SMB 3.1.1+) + >>> smbserver(..., shares=[SMBShare(name="Scapy", path="/tmp", encryptdata=True)]) + .. note:: It is possible to start the :class:`~scapy.layers.smbserver.smbserver` (albeit only in unauthenticated mode) directly from the OS, using the following:: diff --git a/doc/scapy/usage.rst b/doc/scapy/usage.rst index a32792dbeaf..a6e14a84fcc 100644 --- a/doc/scapy/usage.rst +++ b/doc/scapy/usage.rst @@ -798,7 +798,12 @@ Available by default: - HTTP 1.0 - TLS - Kerberos + - LDAP + - SMB - DCE/RPC + - Postgres + - DOIP + - and maybe other protocols if this page isn't up to date. - :py:class:`~scapy.sessions.TLSSession` -> *matches TLS sessions* on the flow. - :py:class:`~scapy.sessions.NetflowSession` -> *resolve Netflow V9 packets* from their NetflowFlowset information objects diff --git a/scapy/layers/ntlm.py b/scapy/layers/ntlm.py index efabef3cc8d..301026383e0 100644 --- a/scapy/layers/ntlm.py +++ b/scapy/layers/ntlm.py @@ -1680,9 +1680,7 @@ def GSS_Accept_sec_context(self, Context: CONTEXT, val=None): EncryptedRandomSessionKey = b"\x00" * 16 else: EncryptedRandomSessionKey = auth_tok.EncryptedRandomSessionKey - ExportedSessionKey = RC4K( - KeyExchangeKey, EncryptedRandomSessionKey - ) + ExportedSessionKey = RC4K(KeyExchangeKey, EncryptedRandomSessionKey) else: ExportedSessionKey = KeyExchangeKey Context.ExportedSessionKey = ExportedSessionKey @@ -1800,6 +1798,7 @@ def _getSessionBaseKey(self, Context, auth_tok): return NTLMv2_ComputeSessionBaseKey( ResponseKeyNT, auth_tok.NtChallengeResponse.NTProofStr ) + log_runtime.debug("NTLMSSP: Bad credentials for %s" % username) return None def _checkLogin(self, Context, auth_tok): diff --git a/scapy/layers/smb.py b/scapy/layers/smb.py index c81451241a2..676021e1d6b 100644 --- a/scapy/layers/smb.py +++ b/scapy/layers/smb.py @@ -67,7 +67,9 @@ ) from scapy.layers.smb2 import ( STATUS_ERREF, + SMB2_Compression_Transform_Header, SMB2_Header, + SMB2_Transform_Header, ) @@ -919,11 +921,9 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs): elif _pkt[0] == 0x13: # LOGON_SAM_USER_RESPONSE try: i = _pkt.index(b"\xff\xff\xff\xff") - NtVersion = ( - NETLOGON_SAM_LOGON_RESPONSE_NT40.fields_desc[-3].getfield( - None, _pkt[i - 4:i] - )[1] - ) + NtVersion = NETLOGON_SAM_LOGON_RESPONSE_NT40.fields_desc[ + -3 + ].getfield(None, _pkt[i - 4 : i])[1] if NtVersion.V1 and not NtVersion.V5: return NETLOGON_SAM_LOGON_RESPONSE_NT40 except Exception: @@ -1013,6 +1013,7 @@ class NETLOGON_SAM_LOGON_RESPONSE_NT40(NETLOGON): # [MS-ADTS] sect 6.3.1.8 + class NETLOGON_SAM_LOGON_RESPONSE(NETLOGON, DNSCompressedPacket): fields_desc = [ LEShortEnumField("OpCode", 0x17, _NETLOGON_opcodes), @@ -1085,8 +1086,7 @@ def pre_dissect(self, s): try: i = s.index(b"\xff\xff\xff\xff") self.fields["NtVersion"] = self.fields_desc[-3].getfield( - self, - s[i - 4:i] + self, s[i - 4 : i] )[1] except Exception: self.NtVersion = 0xB @@ -1098,20 +1098,25 @@ def get_full(self): # [MS-BRWS] sect 2.2 + class BRWS(Packet): fields_desc = [ - ByteEnumField("OpCode", 0x00, { - 0x01: "HostAnnouncement", - 0x02: "AnnouncementRequest", - 0x08: "RequestElection", - 0x09: "GetBackupListRequest", - 0x0A: "GetBackupListResponse", - 0x0B: "BecomeBackup", - 0x0C: "DomainAnnouncement", - 0x0D: "MasterAnnouncement", - 0x0E: "ResetStateRequest", - 0x0F: "LocalMasterAnnouncement", - }), + ByteEnumField( + "OpCode", + 0x00, + { + 0x01: "HostAnnouncement", + 0x02: "AnnouncementRequest", + 0x08: "RequestElection", + 0x09: "GetBackupListRequest", + 0x0A: "GetBackupListResponse", + 0x0B: "BecomeBackup", + 0x0C: "DomainAnnouncement", + 0x0D: "MasterAnnouncement", + 0x0E: "ResetStateRequest", + 0x0F: "LocalMasterAnnouncement", + }, + ), ] def mysummary(self): @@ -1135,6 +1140,7 @@ def default_payload_class(self, payload): # [MS-BRWS] sect 2.2.1 + class BRWS_HostAnnouncement(BRWS): OpCode = 0x01 fields_desc = [ @@ -1157,6 +1163,7 @@ def mysummary(self): # [MS-BRWS] sect 2.2.6 + class BRWS_BecomeBackup(BRWS): OpCode = 0x0B fields_desc = [ @@ -1170,6 +1177,7 @@ def mysummary(self): # [MS-BRWS] sect 2.2.10 + class BRWS_LocalMasterAnnouncement(BRWS_HostAnnouncement): OpCode = 0x0F @@ -1193,6 +1201,10 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs): return SMB_Header if _pkt[:4] == b"\xfeSMB": return SMB2_Header + if _pkt[:4] == b"\xfdSMB": + return SMB2_Transform_Header + if _pkt[:4] == b"\xfcSMB": + return SMB2_Compression_Transform_Header return cls diff --git a/scapy/layers/smb2.py b/scapy/layers/smb2.py index f154ca5be9c..b545a421cff 100644 --- a/scapy/layers/smb2.py +++ b/scapy/layers/smb2.py @@ -268,6 +268,11 @@ 0x0002: "AES-GMAC", } +# [MS-SMB2] sect 2.2.3.1.1 +SMB2_HASH_ALGORITHMS = { + 0x0001: "SHA-512", +} + # sect [MS-SMB2] 2.2.13.1.1 SMB2_ACCESS_FLAGS_FILE = { 0x00000001: "FILE_READ_DATA", @@ -809,9 +814,11 @@ def summary(self): return "S-%s-%s%s" % ( self.Revision, struct.unpack(">Q", b"\x00\x00" + self.IdentifierAuthority.Value)[0], - ("-%s" % "-".join(str(x) for x in self.SubAuthority)) - if self.SubAuthority - else "", + ( + ("-%s" % "-".join(str(x) for x in self.SubAuthority)) + if self.SubAuthority + else "" + ), ) @@ -1740,6 +1747,8 @@ class DirectTCP(NBTSession): class SMB2_Header(Packet): + __slots__ = ["_decrypted"] + name = "SMB2 Header" fields_desc = [ StrFixedLenField("Start", b"\xfeSMB", 4), @@ -1791,6 +1800,11 @@ class SMB2_Header(Packet): (0x0000010C, 0x000F), # STATUS_NOTIFY_ENUM_DIR ) + def __init__(self, *args, **kwargs): + # The parent passes whether this packet was decrypted or not. + self._decrypted = kwargs.pop("_decrypted", False) + super(SMB2_Header, self).__init__(*args, **kwargs) + def guess_payload_class(self, payload): if self.Flags.SMB2_FLAGS_SERVER_TO_REDIR and self.Status != 0x00000000: # Check status for responses @@ -1860,17 +1874,22 @@ def guess_payload_class(self, payload): return SMB2_IOCTL_Request return super(SMB2_Header, self).guess_payload_class(payload) - def sign(self, dialect, SigningSessionKey, SigningAlgorithmId=None, IsClient=None): - # [MS-SMB2] 3.1.4.1 - self.SecuritySignature = b"\x00" * 16 - s = bytes(self) + def _calc_signature( + self, s, dialect, SigningSessionKey, SigningAlgorithmId=None, IsClient=None + ): + """ + This function calculates the signature of a SMB2 packet. + Detail is from [MS-SMB2] 3.1.4.1 + """ if len(s) <= 64: log_runtime.warning("Cannot sign invalid SMB packet !") return s if dialect in [0x0300, 0x0302, 0x0311]: # SMB 3 if dialect == 0x0311: # SMB 3.1.1 - if SigningAlgorithmId is None or IsClient is None: - raise Exception("SMB 3.1.1 needs a SigningAlgorithmId and IsClient") + if IsClient is None: + raise Exception("SMB 3.1.1 needs a IsClient") + if SigningAlgorithmId is None: + SigningAlgorithmId = "AES-CMAC" # AES-128-CMAC else: SigningAlgorithmId = "AES-CMAC" # AES-128-CMAC if "GMAC" in SigningAlgorithmId: @@ -1903,11 +1922,88 @@ def sign(self, dialect, SigningSessionKey, SigningAlgorithmId=None, IsClient=Non sig = sig[:16] else: log_runtime.warning("Unknown SMB Version %s ! Cannot sign." % dialect) - sig = s[:-16] + b"\x00" * 16 - self.SecuritySignature = sig + sig = b"\x00" * 16 + return sig + + def sign(self, dialect, SigningSessionKey, SigningAlgorithmId=None, IsClient=None): + """ + [MS-SMB2] 3.1.4.1 - Signing An Outgoing Message + """ + # Set the current signature to nul + self.SecuritySignature = b"\x00" * 16 + # Calculate the signature + s = bytes(self) + self.SecuritySignature = self._calc_signature( + s, + dialect=dialect, + SigningSessionKey=SigningSessionKey, + SigningAlgorithmId=SigningAlgorithmId, + IsClient=IsClient, + ) # we make sure the payload is static self.payload = conf.raw_layer(load=s[64:]) + def verify( + self, dialect, SigningSessionKey, SigningAlgorithmId=None, IsClient=None + ): + """ + [MS-SMB2] sect 3.2.5.1.3 - Verifying the signature + """ + s = bytes(self) + # Set SecuritySignature to nul + s = s[:48] + b"\x00" * 16 + s[64:] + # Calculate the signature + sig = self._calc_signature( + s, + dialect=dialect, + SigningSessionKey=SigningSessionKey, + SigningAlgorithmId=SigningAlgorithmId, + IsClient=IsClient, + ) + if self.SecuritySignature != sig: + log_runtime.error("SMB signature is invalid !") + raise Exception("ERROR: SMB signature is invalid !") + + def encrypt(self, dialect, EncryptionKey, CipherId): + """ + [MS-SMB2] sect 3.1.4.3 - Encrypting the Message + """ + if dialect < 0x0300: + raise Exception("Encryption is not supported on this SMB dialect !") + elif dialect < 0x0311 and CipherId != "AES-128-CCM": + raise Exception("CipherId is not supported on this SMB dialect !") + + data = bytes(self) + smbt = SMB2_Transform_Header( + OriginalMessageSize=len(self), + SessionId=self.SessionId, + Flags=0x0001, + ) + if "GCM" in CipherId: + from cryptography.hazmat.primitives.ciphers.aead import AESGCM + + nonce = os.urandom(12) + cipher = AESGCM(EncryptionKey) + elif "CCM" in CipherId: + from cryptography.hazmat.primitives.ciphers.aead import AESCCM + + nonce = os.urandom(11) + cipher = AESCCM(EncryptionKey) + else: + raise Exception("Unknown CipherId !") + + # Add nonce to header and build the auth data + smbt.Nonce = nonce + aad = bytes(smbt)[20:] + + # Perform the actual encryption + data = cipher.encrypt(nonce, data, aad) + + # Put the auth tag in the Signature field + smbt.Signature, data = data[-16:], data[:-16] + + return smbt / data + class _SMB2_Payload(Packet): def do_dissect_payload(self, s): @@ -2139,10 +2235,7 @@ class SMB2_Preauth_Integrity_Capabilities(Packet): LEShortEnumField( "", 0x0, - { - # As for today, no other hash algorithm is described by the spec - 0x0001: "SHA-512", - }, + SMB2_HASH_ALGORITHMS, ), count_from=lambda pkt: pkt.HashAlgorithmCount, ), @@ -2224,7 +2317,11 @@ def default_payload_class(self, payload): class SMB2_Netname_Negotiate_Context_ID(Packet): name = "SMB2 Netname Negotiate Context ID" - fields_desc = [StrFieldUtf16("NetName", "")] + fields_desc = [ + StrLenFieldUtf16( + "NetName", "", length_from=lambda pkt: pkt.underlayer.DataLength + ) + ] def default_payload_class(self, payload): return conf.padding_layer @@ -2477,7 +2574,7 @@ class SMB2_Session_Setup_Response(_SMB2_Payload, _NTLMPayloadPacket): { 0x0001: "IS_GUEST", 0x0002: "IS_NULL", - 0x0004: "ENCRYPT_DATE", + 0x0004: "ENCRYPT_DATA", }, ), XLEShortField("SecurityBufferOffset", None), @@ -2896,11 +2993,15 @@ class SMB2_Create_Context(_NTLMPayloadPacket): "pad", b"", length_from=lambda x: ( - x.Next - - max(x.DataBufferOffset + x.DataLen, x.NameBufferOffset + x.NameLen) - ) - if x.Next - else 0, + ( + x.Next + - max( + x.DataBufferOffset + x.DataLen, x.NameBufferOffset + x.NameLen + ) + ) + if x.Next + else 0 + ), ), ] @@ -4252,6 +4353,60 @@ class SMB2_Set_Info_Response(_SMB2_Payload): ) +# sect 2.2.41 + + +class SMB2_Transform_Header(Packet): + name = "SMB2 Transform Header" + fields_desc = [ + StrFixedLenField("Start", b"\xfdSMB", 4), + XStrFixedLenField("Signature", 0, length=16), + XStrFixedLenField("Nonce", b"", length=16), + LEIntField("OriginalMessageSize", 0x0), + LEShortField("Reserved", 0), + LEShortEnumField( + "Flags", + 0x1, + { + 0x0001: "ENCRYPTED", + }, + ), + LELongField("SessionId", 0), + ] + + def decrypt(self, dialect, DecryptionKey, CipherId): + """ + [MS-SMB2] sect 3.2.5.1.1.1 - Decrypting the Message + """ + if not isinstance(self.payload, conf.raw_layer): + raise Exception("No payload to decrypt !") + + if "GCM" in CipherId: + from cryptography.hazmat.primitives.ciphers.aead import AESGCM + + nonce = self.Nonce[:12] + cipher = AESGCM(DecryptionKey) + elif "CCM" in CipherId: + from cryptography.hazmat.primitives.ciphers.aead import AESCCM + + nonce = self.Nonce[:11] + cipher = AESCCM(DecryptionKey) + else: + raise Exception("Unknown CipherId !") + + # Decrypt the data + aad = self.self_build()[20:] + data = cipher.decrypt( + nonce, + self.payload.load + self.Signature, + aad, + ) + return SMB2_Header(data, _decrypted=True) + + +bind_layers(SMB2_Transform_Header, conf.raw_layer) + + # sect 2.2.42.1 @@ -4525,9 +4680,17 @@ def recv(self, x=None): # note: normal StreamSocket takes care of NBTSession / DirectTCP fragments. # this takes care of splitting compounded requests if self.queue: - return self.queue.popleft() - pkt = super(SMBStreamSocket, self).recv(x) - if pkt is not None and SMB2_Header in pkt: + pkt = self.queue.popleft() + else: + pkt = super(SMBStreamSocket, self).recv(x) + # If there are multiple SMB2_Header requests (aka. compounded), + # take the first and store the rest in a queue. + if pkt is not None and ( + SMB2_Header in pkt + or SMB2_Transform_Header in pkt + or SMB2_Compression_Transform_Header in pkt + ): + pkt = self.session.in_pkt(pkt) pay = pkt[SMB2_Header].payload while SMB2_Header in pay: pay = pay[SMB2_Header] @@ -4536,10 +4699,29 @@ def recv(self, x=None): if not pay.NextCommand: break pay = pay.payload - return self.session.in_pkt(pkt) + # Verify the signature if required. + # This happens here because we must have split compounded requests first. + smbh = pkt.getlayer(SMB2_Header) + if ( + smbh + and self.session.Dialect + and self.session.SigningKey + and self.session.SigningRequired + and not smbh._decrypted + ): + smbh.verify( + self.session.Dialect, + self.session.SigningKey, + # SMB 3.1.1 parameters: + SigningAlgorithmId=self.session.SigningAlgorithmId, + IsClient=False, + ) + return pkt - def send(self, x, Compounded=False, **kwargs): - for pkt in self.session.out_pkt(x, Compounded=Compounded): + def send(self, x, Compounded=False, ForceSign=False, ForceEncrypt=False, **kwargs): + for pkt in self.session.out_pkt( + x, Compounded=Compounded, ForceSign=ForceSign, ForceEncrypt=ForceEncrypt + ): return super(SMBStreamSocket, self).send(pkt, **kwargs) @staticmethod @@ -4563,12 +4745,28 @@ def __init__(self, *args, **kwargs): self.CompoundQueue = [] self.Dialect = 0x0202 # Updated by parent self.Credits = 0 - self.SecurityMode = 0 - # Crypto parameters - self.SMBSessionKey = None + self.IsGuest = False + # Crypto parameters. Go read [MS-SMB2] to understand the names. + self.SigningRequired = True + self.SupportsEncryption = False + self.EncryptData = False + self.TreeEncryptData = False + self.SigningKey = None + self.EncryptionKey = None + self.DecryptionKey = None self.PreauthIntegrityHashId = "SHA-512" + self.SupportedCipherIds = [ + "AES-128-CCM", + "AES-128-GCM", + "AES-256-CCM", + "AES-256-GCM", + ] self.CipherId = "AES-128-CCM" - self.SigningAlgorithmId = "AES-CMAC" + self.SupportedSigningAlgorithmIds = [ + "AES-CMAC", + "HMAC-SHA256", + ] + self.SigningAlgorithmId = None self.Salt = os.urandom(32) self.ConnectionPreauthIntegrityHashValue = None self.SessionPreauthIntegrityHashValue = None @@ -4582,18 +4780,22 @@ def __init__(self, *args, **kwargs): # SMB crypto functions @crypto_validator - def computeSMBSessionKey(self): + def computeSMBSessionKeys(self, IsClient=None): + """ + Compute the SigningKey and EncryptionKey (for SMB 3+) + """ if not getattr(self.sspcontext, "SessionKey", None): # no signing key, no session key return # [MS-SMB2] sect 3.3.5.5.3 + # SigningKey if self.Dialect >= 0x0300: if self.Dialect == 0x0311: label = b"SMBSigningKey\x00" - preauth_hash = self.SessionPreauthIntegrityHashValue + context = self.SessionPreauthIntegrityHashValue else: label = b"SMB2AESCMAC\x00" - preauth_hash = b"SmbSign\x00" + context = b"SmbSign\x00" # [MS-SMB2] sect 3.1.4.2 if "256" in self.CipherId: L = 256 @@ -4601,14 +4803,43 @@ def computeSMBSessionKey(self): L = 128 else: raise ValueError - self.SMBSessionKey = SP800108_KDFCTR( + self.SigningKey = SP800108_KDFCTR( self.sspcontext.SessionKey[:16], - label, # label - preauth_hash, # context + label, + context, + L, + ) + # EncryptionKey / DecryptionKey + if self.Dialect == 0x0311: + if IsClient: + label_out = b"SMBC2SCipherKey\x00" + label_in = b"SMBS2CCipherKey\x00" + else: + label_out = b"SMBS2CCipherKey\x00" + label_in = b"SMBC2SCipherKey\x00" + context_out = context_in = self.SessionPreauthIntegrityHashValue + else: + label_out = label_in = b"SMB2AESCCM\x00" + if IsClient: + context_out = b"ServerIn \x00" # extra space per spec + context_in = b"ServerOut\x00" + else: + context_out = b"ServerOut\x00" + context_in = b"ServerIn \x00" + self.EncryptionKey = SP800108_KDFCTR( + self.sspcontext.SessionKey[: L // 8], + label_out, + context_out, + L, + ) + self.DecryptionKey = SP800108_KDFCTR( + self.sspcontext.SessionKey[: L // 8], + label_in, + context_in, L, ) elif self.Dialect <= 0x0210: - self.SMBSessionKey = self.sspcontext.SessionKey[:16] + self.SigningKey = self.sspcontext.SessionKey[:16] else: raise ValueError("Hmmm ? >:(") @@ -4653,19 +4884,29 @@ def in_pkt(self, pkt): """ Incoming SMB packet """ + if SMB2_Transform_Header in pkt: + # Packet is encrypted + pkt = pkt[SMB2_Transform_Header].decrypt( + self.Dialect, + self.DecryptionKey, + CipherId=self.CipherId, + ) + # Signature is verified in SMBStreamSocket return pkt - def out_pkt(self, pkt, Compounded=False): + def out_pkt(self, pkt, Compounded=False, ForceSign=False, ForceEncrypt=False): """ Outgoing SMB packet :param pkt: the packet to send :param Compound: if True, will be stack to be send with the next un-compounded packet + :param ForceSign: if True, force to sign the packet. + :param ForceEncrypt: if True, force to encrypt the packet. Handles: - handle compounded requests (if any): [MS-SMB2] 3.3.5.2.7 - - handles signing (if required) + - handles signing and encryption (if required) """ # Note: impacket and wireshark get crazy on compounded+signature, but # windows+samba tells we're right :D @@ -4687,13 +4928,17 @@ def out_pkt(self, pkt, Compounded=False): if padlen: pkt.add_payload(b"\x00" * padlen) pkt[SMB2_Header].NextCommand = length + padlen - if self.Dialect and self.SMBSessionKey and self.SecurityMode != 0: - # Sign SMB2 ! + if ( + self.Dialect + and self.SigningKey + and (ForceSign or self.SigningRequired and not ForceEncrypt) + ): + # [MS-SMB2] sect 3.2.4.1.1 - Signing smb = pkt[SMB2_Header] smb.Flags += "SMB2_FLAGS_SIGNED" smb.sign( self.Dialect, - self.SMBSessionKey, + self.SigningKey, # SMB 3.1.1 parameters: SigningAlgorithmId=self.SigningAlgorithmId, IsClient=False, @@ -4707,6 +4952,22 @@ def out_pkt(self, pkt, Compounded=False): if self.CompoundQueue: pkt = functools.reduce(lambda x, y: x / y, self.CompoundQueue) / pkt self.CompoundQueue.clear() + if self.EncryptionKey and ( + ForceEncrypt or self.EncryptData or self.TreeEncryptData + ): + # [MS-SMB2] sect 3.1.4.3 - Encrypting the message + smb = pkt[SMB2_Header] + assert not smb.Flags.SMB2_FLAGS_SIGNED + smbt = smb.encrypt( + self.Dialect, + self.EncryptionKey, + CipherId=self.CipherId, + ) + if smb.underlayer: + # If there's an underlayer, replace current SMB header + smb.underlayer.payload = smbt + else: + smb = smbt return [pkt] def process(self, pkt: Packet): diff --git a/scapy/layers/smbclient.py b/scapy/layers/smbclient.py index 27d0b06d463..e31e1a47f01 100644 --- a/scapy/layers/smbclient.py +++ b/scapy/layers/smbclient.py @@ -83,8 +83,8 @@ SMB2_CREATE_REQUEST_LEASE, SMB2_Create_Request, SMB2_Create_Response, - SMB2_Encryption_Capabilities, SMB2_ENCRYPTION_CIPHERS, + SMB2_Encryption_Capabilities, SMB2_Error_Response, SMB2_Header, SMB2_IOCTL_Request, @@ -100,9 +100,9 @@ SMB2_Query_Info_Response, SMB2_Read_Request, SMB2_Read_Response, + SMB2_SIGNING_ALGORITHMS, SMB2_Session_Setup_Request, SMB2_Session_Setup_Response, - SMB2_SIGNING_ALGORITHMS, SMB2_Signing_Capabilities, SMB2_Tree_Connect_Request, SMB2_Tree_Connect_Response, @@ -127,6 +127,7 @@ class SMB_Client(Automaton): All other options (in caps) are optional, and SMB specific: :param REQUIRE_SIGNATURE: set 'Require Signature' + :param REQUIRE_ENCRYPTION: set 'Requite Encryption' :param MIN_DIALECT: minimum SMB dialect. Defaults to 0x0202 (2.0.2) :param MAX_DIALECT: maximum SMB dialect. Defaults to 0x0311 (3.1.1) :param DIALECTS: list of supported SMB2 dialects. @@ -140,7 +141,8 @@ def __init__(self, sock, ssp=None, *args, **kwargs): # Various SMB client arguments self.EXTENDED_SECURITY = kwargs.pop("EXTENDED_SECURITY", True) self.USE_SMB1 = kwargs.pop("USE_SMB1", False) - self.REQUIRE_SIGNATURE = kwargs.pop("REQUIRE_SIGNATURE", False) + self.REQUIRE_SIGNATURE = kwargs.pop("REQUIRE_SIGNATURE", None) + self.REQUIRE_ENCRYPTION = kwargs.pop("REQUIRE_ENCRYPTION", False) self.RETRY = kwargs.pop("RETRY", 0) # optionally: retry n times session setup self.SMB2 = kwargs.pop("SMB2", False) # optionally: start directly in SMB2 self.SERVER_NAME = kwargs.pop("SERVER_NAME", "") @@ -158,7 +160,6 @@ def __init__(self, sock, ssp=None, *args, **kwargs): ] ) # Internal Session information - self.IsGuest = False self.ErrorStatus = None self.NegotiateCapabilities = None self.GUID = RandUUID()._fix() @@ -187,9 +188,8 @@ def __init__(self, sock, ssp=None, *args, **kwargs): self.smb_sock_ready = threading.Event() # Set session options self.session.ssp = ssp - self.session.SecurityMode = kwargs.pop( - "SECURITY_MODE", - 3 if self.REQUIRE_SIGNATURE else int(bool(ssp)), + self.session.SigningRequired = ( + self.REQUIRE_SIGNATURE if self.REQUIRE_SIGNATURE is not None else bool(ssp) ) self.session.Dialect = self.MAX_DIALECT @@ -319,7 +319,11 @@ def on_negotiate_smb2(self): # [MS-SMB2] sect 3.2.4.2.2.2 - SMB2-Only Negotiate pkt = self.smb_header.copy() / SMB2_Negotiate_Protocol_Request( Dialects=self.DIALECTS, - SecurityMode=self.session.SecurityMode, + SecurityMode=( + "SIGNING_ENABLED+SIGNING_REQUIRED" + if self.session.SigningRequired + else "SIGNING_ENABLED" + ), ) if self.MAX_DIALECT >= 0x0210: # "If the client implements the SMB 2.1 or SMB 3.x dialect, ClientGuid @@ -340,25 +344,21 @@ def on_negotiate_smb2(self): "MULTI_CHANNEL", "PERSISTENT_HANDLES", "DIRECTORY_LEASING", + "ENCRYPTION", ] ) - if self.MAX_DIALECT >= 0x0300: - # "If the client implements the SMB 3.x dialect family, the client MUST - # set the Capabilities field as follows" - self.NegotiateCapabilities += "+ENCRYPTION" if self.MAX_DIALECT >= 0x0311: # "If the client implements the SMB 3.1.1 dialect, it MUST do" pkt.NegotiateContexts = [ SMB2_Negotiate_Context() / SMB2_Preauth_Integrity_Capabilities( - # SHA-512 by default - HashAlgorithms=[self.session.PreauthIntegrityHashId], + # As for today, no other hash algorithm is described by the spec + HashAlgorithms=["SHA-512"], Salt=self.session.Salt, ), SMB2_Negotiate_Context() / SMB2_Encryption_Capabilities( - # AES-128-CCM by default - Ciphers=[self.session.CipherId], + Ciphers=self.session.SupportedCipherIds, ), # TODO support compression and RDMA SMB2_Negotiate_Context() @@ -367,8 +367,7 @@ def on_negotiate_smb2(self): ), SMB2_Negotiate_Context() / SMB2_Signing_Capabilities( - # AES-128-CCM by default - SigningAlgorithms=[self.session.SigningAlgorithmId], + SigningAlgorithms=self.session.SupportedSigningAlgorithmIds, ), ] pkt.Capabilities = self.NegotiateCapabilities @@ -416,19 +415,30 @@ def receive_negotiate_response(self, pkt): self.MaxReadSize = pkt.MaxReadSize self.MaxTransactionSize = pkt.MaxTransactionSize self.MaxWriteSize = pkt.MaxWriteSize + # Process SecurityMode + if pkt.SecurityMode.SIGNING_REQUIRED: + self.session.SigningRequired = True + # Process capabilities + if self.session.Dialect >= 0x0300: + self.session.SupportsEncryption = pkt.Capabilities.ENCRYPTION # Process NegotiateContext if self.session.Dialect >= 0x0311 and pkt.NegotiateContextsCount: for ngctx in pkt.NegotiateContexts: if ngctx.ContextType == 0x0002: # SMB2_ENCRYPTION_CAPABILITIES - self.session.CipherId = SMB2_ENCRYPTION_CIPHERS[ - ngctx.Ciphers[0] - ] + if ngctx.Ciphers[0] != 0: + self.session.CipherId = SMB2_ENCRYPTION_CIPHERS[ + ngctx.Ciphers[0] + ] + self.session.SupportsEncryption = True elif ngctx.ContextType == 0x0008: # SMB2_SIGNING_CAPABILITIES self.session.SigningAlgorithmId = ( SMB2_SIGNING_ALGORITHMS[ngctx.SigningAlgorithms[0]] ) + if self.REQUIRE_ENCRYPTION and not self.session.SupportsEncryption: + self.ErrorStatus = "NEGOTIATE FAILURE: encryption." + raise self.NEGO_FAILED() self.update_smbheader(pkt) raise self.NEGOTIATED(ssp_blob) elif SMBNegotiate_Response_Security in pkt: @@ -436,6 +446,10 @@ def receive_negotiate_response(self, pkt): # Never tested. FIXME. probably broken raise self.NEGOTIATED(pkt.Challenge) + @ATMT.state(final=1) + def NEGO_FAILED(self): + self.smb_sock_ready.set() + @ATMT.state() def NEGOTIATED(self, ssp_blob=None): # Negotiated ! We now know the Dialect @@ -448,11 +462,7 @@ def NEGOTIATED(self, ssp_blob=None): ssp_blob, req_flags=( GSS_C_FLAGS.GSS_C_MUTUAL_FLAG - | ( - GSS_C_FLAGS.GSS_C_INTEG_FLAG - if self.session.SecurityMode != 0 - else 0 - ) + | (GSS_C_FLAGS.GSS_C_INTEG_FLAG if self.session.SigningRequired else 0) ), ) return ssp_tuple @@ -498,7 +508,11 @@ def send_setup_andx_request(self, ssp_tuple): # SMB2 pkt = self.smb_header.copy() / SMB2_Session_Setup_Request( Capabilities="DFS", - SecurityMode=self.session.SecurityMode, + SecurityMode=( + "SIGNING_ENABLED+SIGNING_REQUIRED" + if self.session.SigningRequired + else "SIGNING_ENABLED" + ), ) else: # SMB1 extended @@ -562,9 +576,18 @@ def receive_setup_andx_response(self, pkt): self.smb_header.SessionId = pkt.SessionId # SMB1 extended / SMB2 if pkt.Status == 0: # Authenticated - if SMB2_Session_Setup_Response in pkt and pkt.SessionFlags.IS_GUEST: - # We were 'authenticated' in GUEST - self.IsGuest = True + if SMB2_Session_Setup_Response in pkt: + # [MS-SMB2] sect 3.2.5.3.1 + if pkt.SessionFlags.IS_GUEST: + # "If the security subsystem indicates that the session + # was established by a guest user, Session.SigningRequired + # MUST be set to FALSE and Session.IsGuest MUST be set to TRUE." + self.session.IsGuest = True + self.session.SigningRequired = False + elif self.session.Dialect >= 0x0300: + if pkt.SessionFlags.ENCRYPT_DATA or self.REQUIRE_ENCRYPTION: + self.session.EncryptData = True + self.session.SigningRequired = False raise self.AUTHENTICATED(pkt.SecurityBlob) else: if SMB2_Header in pkt: @@ -600,10 +623,7 @@ def AUTHENTICATED(self, ssp_blob=None): if status != GSS_S_COMPLETE: raise ValueError("Internal error: the SSP completed with an error.") # Authentication was successful - self.session.computeSMBSessionKey() - if self.IsGuest: - # When authenticated in Guest, the sessionkey the client has is invalid - self.session.SMBSessionKey = None + self.session.computeSMBSessionKeys(IsClient=True) # DEV: add a condition on AUTHENTICATED with prio=0 @@ -663,7 +683,9 @@ def __init__(self, smbsock, use_ioctl=True, timeout=3): self.ins = smbsock self.timeout = timeout if not self.ins.atmt.smb_sock_ready.wait(timeout=timeout): - self.ins.atmt.session.sspcontext.clifailure() + # If we have a SSP, tell it we failed. + if self.ins.atmt.session.sspcontext: + self.ins.atmt.session.sspcontext.clifailure() raise TimeoutError( "The SMB handshake timed out ! (enable debug=1 for logs)" ) @@ -725,6 +747,12 @@ def tree_connect(self, name): raise ValueError("TreeConnect timed out !") if SMB2_Tree_Connect_Response not in resp: raise ValueError("Failed TreeConnect ! %s" % resp.NTStatus) + # [MS-SMB2] sect 3.2.5.5 + if self.session.Dialect >= 0x0300: + if resp.ShareFlags.ENCRYPT_DATA and self.session.SupportsEncryption: + self.session.TreeEncryptData = True + else: + self.session.TreeEncryptData = False return self.get_TID() def tree_disconnect(self): @@ -1078,6 +1106,11 @@ class smbclient(CLIUtil): :param ST: if provided, the service ticket to use (Kerberos) :param KEY: if provided, the session key associated to the ticket (Kerberos) :param cli: CLI mode (default True). False to use for scripting + + Some additional SMB parameters are available under help(SMB_Client). Some of + them include the following: + + :param REQUIRE_ENCRYPTION: requires encryption. """ def __init__( @@ -1097,6 +1130,7 @@ def __init__( KEY=None, cli=True, # SMB arguments + REQUIRE_ENCRYPTION=False, **kwargs, ): if cli: @@ -1187,6 +1221,7 @@ def __init__( sock, ssp=ssp, debug=debug, + REQUIRE_ENCRYPTION=REQUIRE_ENCRYPTION, **kwargs, ) try: @@ -1229,7 +1264,7 @@ def __init__( "SMB %s" % self.smbsock.session.Dialect, ), repr(self.smbsock.session.sspcontext), - " as GUEST" if self.sock.atmt.IsGuest else "", + " as GUEST" if self.smbsock.session.IsGuest else "", ) ) # Now define some variables for our CLI diff --git a/scapy/layers/smbserver.py b/scapy/layers/smbserver.py index 17338fac3bd..8fba2238c51 100644 --- a/scapy/layers/smbserver.py +++ b/scapy/layers/smbserver.py @@ -76,17 +76,18 @@ FileStreamInformation, NETWORK_INTERFACE_INFO, SECURITY_DESCRIPTOR, + SMB2_CREATE_DURABLE_HANDLE_RESPONSE_V2, + SMB2_CREATE_QUERY_MAXIMAL_ACCESS_RESPONSE, + SMB2_CREATE_QUERY_ON_DISK_ID, SMB2_Cancel_Request, SMB2_Change_Notify_Request, SMB2_Change_Notify_Response, SMB2_Close_Request, SMB2_Close_Response, SMB2_Create_Context, - SMB2_CREATE_DURABLE_HANDLE_RESPONSE_V2, - SMB2_CREATE_QUERY_MAXIMAL_ACCESS_RESPONSE, - SMB2_CREATE_QUERY_ON_DISK_ID, SMB2_Create_Request, SMB2_Create_Response, + SMB2_ENCRYPTION_CIPHERS, SMB2_Echo_Request, SMB2_Echo_Response, SMB2_Encryption_Capabilities, @@ -94,8 +95,8 @@ SMB2_FILEID, SMB2_Header, SMB2_IOCTL_Network_Interface_Info, - SMB2_IOCTL_Request, SMB2_IOCTL_RESP_GET_DFS_Referral, + SMB2_IOCTL_Request, SMB2_IOCTL_Response, SMB2_IOCTL_Validate_Negotiate_Info_Response, SMB2_Negotiate_Context, @@ -108,6 +109,7 @@ SMB2_Query_Info_Response, SMB2_Read_Request, SMB2_Read_Response, + SMB2_SIGNING_ALGORITHMS, SMB2_Session_Logoff_Request, SMB2_Session_Logoff_Response, SMB2_Session_Setup_Request, @@ -155,9 +157,11 @@ class SMBShare: :param path: the path the the folder hosted by the share :param type: (optional) share type per [MS-SRVS] sect 2.2.2.4 :param remark: (optional) a description of the share + :param encryptdata: (optional) whether encryption should be used for this + share. This only applies to SMB 3.1.1. """ - def __init__(self, name, path=".", type=None, remark=""): + def __init__(self, name, path=".", type=None, remark="", encryptdata=False): # Set the default type if type is None: type = 0 # DISKTREE @@ -171,6 +175,7 @@ def __init__(self, name, path=".", type=None, remark=""): self.name = name self.type = type self.remark = remark + self.encryptdata = encryptdata def __repr__(self): type = SRVSVC_SHARE_TYPES[self.type & 0x0FFFFFFF] @@ -202,6 +207,8 @@ class SMB_Server(Automaton): :param ANONYMOUS_LOGIN: mark the clients as anonymous :param GUEST_LOGIN: mark the clients as guest :param REQUIRE_SIGNATURE: set 'Require Signature' + :param REQUIRE_ENCRYPTION: globally require encryption. + You could also make it share-specific on 3.1.1. :param MAX_DIALECT: maximum SMB dialect. Defaults to 0x0311 (3.1.1) :param TREE_SHARE_FLAGS: flags to announce on Tree_Connect_Response :param TREE_CAPABILITIES: capabilities to announce on Tree_Connect_Response @@ -223,7 +230,8 @@ def __init__(self, shares=[], ssp=None, verb=True, readonly=True, *args, **kwarg self.GUEST_LOGIN = kwargs.pop("GUEST_LOGIN", None) self.EXTENDED_SECURITY = kwargs.pop("EXTENDED_SECURITY", True) self.USE_SMB1 = kwargs.pop("USE_SMB1", False) - self.REQUIRE_SIGNATURE = kwargs.pop("REQUIRE_SIGNATURE", False) + self.REQUIRE_SIGNATURE = kwargs.pop("REQUIRE_SIGNATURE", None) + self.REQUIRE_ENCRYPTION = kwargs.pop("REQUIRE_ENCRYPTION", False) self.MAX_DIALECT = kwargs.pop("MAX_DIALECT", 0x0311) self.TREE_SHARE_FLAGS = kwargs.pop( "TREE_SHARE_FLAGS", "FORCE_LEVELII_OPLOCK+RESTRICT_EXCLUSIVE_OPENS" @@ -294,6 +302,8 @@ def __init__(self, shares=[], ssp=None, verb=True, readonly=True, *args, **kwarg self.SMB2 = False self.NegotiateCapabilities = None self.GUID = RandUUID()._fix() + self.NextForceSign = False + self.NextForceEncrypt = False # Compounds are handled on receiving by the StreamSocket, # and on aggregated in a CompoundQueue to be sent in one go self.NextCompound = False @@ -315,9 +325,8 @@ def __init__(self, shares=[], ssp=None, verb=True, readonly=True, *args, **kwarg Automaton.__init__(self, *args, **kwargs) # Set session options self.session.ssp = ssp - self.session.SecurityMode = kwargs.pop( - "SECURITY_MODE", - 3 if self.REQUIRE_SIGNATURE else bool(ssp), + self.session.SigningRequired = ( + self.REQUIRE_SIGNATURE if self.REQUIRE_SIGNATURE is not None else bool(ssp) ) @property @@ -336,7 +345,14 @@ def vprint(self, s=""): print("> %s" % s) def send(self, pkt): - return super(SMB_Server, self).send(pkt, Compounded=self.NextCompound) + ForceSign, ForceEncrypt = self.NextForceSign, self.NextForceEncrypt + self.NextForceSign = self.NextForceEncrypt = False + return super(SMB_Server, self).send( + pkt, + Compounded=self.NextCompound, + ForceSign=ForceSign, + ForceEncrypt=ForceEncrypt, + ) @ATMT.state(initial=1) def BEGIN(self): @@ -433,6 +449,9 @@ def on_negotiate(self, pkt): self.send(resp) return if self.SMB2: # SMB2 + # SecurityMode + if SMB2_Header in pkt and pkt.SecurityMode.SIGNING_REQUIRED: + self.session.SigningRequired = True # Capabilities: [MS-SMB2] 3.3.5.4 self.NegotiateCapabilities = "+".join( [ @@ -449,16 +468,17 @@ def on_negotiate(self, pkt): "MULTI_CHANNEL", "PERSISTENT_HANDLES", "DIRECTORY_LEASING", + "ENCRYPTION", ] ) - if DialectRevision in [0x0300, 0x0302]: - # "if Connection.Dialect is "3.0" or "3.0.2""... - # Note: 3.1.1 uses the ENCRYPT_DATA flag in Tree Connect Response - self.NegotiateCapabilities += "+ENCRYPTION" # Build response resp = self.smb_header.copy() / cls( DialectRevision=DialectRevision, - SecurityMode=self.session.SecurityMode, + SecurityMode=( + "SIGNING_ENABLED+SIGNING_REQUIRED" + if self.session.SigningRequired + else "SIGNING_ENABLED" + ), ServerTime=(time.time() + 11644473600) * 1e7, ServerStartTime=0, MaxTransactionSize=65536, @@ -473,7 +493,27 @@ def on_negotiate(self, pkt): resp.MaxReadSize = 0x800000 resp.MaxWriteSize = 0x800000 # SMB 3.1.1 - if DialectRevision >= 0x0311: + if DialectRevision >= 0x0311 and pkt.NegotiateContextsCount: + # Negotiate context-capabilities + for ngctx in pkt.NegotiateContexts: + if ngctx.ContextType == 0x0002: + # SMB2_ENCRYPTION_CAPABILITIES + for ciph in ngctx.Ciphers: + tciph = SMB2_ENCRYPTION_CIPHERS.get(ciph, None) + if tciph in self.session.SupportedCipherIds: + # Common ! + self.session.CipherId = tciph + self.session.SupportsEncryption = True + break + elif ngctx.ContextType == 0x0008: + # SMB2_SIGNING_CAPABILITIES + for signalg in ngctx.SigningAlgorithms: + tsignalg = SMB2_SIGNING_ALGORITHMS.get(signalg, None) + if tsignalg in self.session.SupportedSigningAlgorithmIds: + # Common ! + self.session.SigningAlgorithmId = tsignalg + break + # Send back the negotiated algorithms resp.NegotiateContexts = [ # Preauth capabilities SMB2_Negotiate_Context() @@ -504,7 +544,11 @@ def on_negotiate(self, pkt): "LEVEL_II_OPLOCKS+LOCK_AND_READ+NT_FIND+" "LWIO+INFOLEVEL_PASSTHRU+LARGE_READX+LARGE_WRITEX" ), - SecurityMode=self.session.SecurityMode, + SecurityMode=( + "SIGNING_ENABLED+SIGNING_REQUIRED" + if self.session.SigningRequired + else "SIGNING_ENABLED" + ), ServerTime=(time.time() + 11644473600) * 1e7, ServerTimeZone=0x3C, ) @@ -534,6 +578,11 @@ def on_negotiate(self, pkt): ) self.send(resp) + @ATMT.state(final=1) + def NEGO_FAILED(self): + self.vprint("SMB Negotiate failed: encryption was not negotiated.") + self.end() + @ATMT.state() def NEGOTIATED(self): pass @@ -550,6 +599,17 @@ def update_smbheader(self, pkt): self.smb_header.CreditCharge = pkt.CreditCharge # If the packet has a NextCommand, set NextCompound to True self.NextCompound = bool(pkt.NextCommand) + # [MS-SMB2] sect 3.3.4.1.1 - "If the request was signed by the client..." + # If the packet was signed, note we must answer with a signed packet. + if ( + not self.session.SigningRequired + and pkt.SecuritySignature != b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + ): + self.NextForceSign = True + # [MS-SMB2] sect 3.3.4.1.4 - "If the message being sent is any response to a + # client request for which Request.IsEncrypted is TRUE" + if pkt[SMB2_Header]._decrypted: + self.NextForceEncrypt = True # [MS-SMB2] sect 3.3.5.2.7.2 # Add SMB2_FLAGS_RELATED_OPERATIONS to the response if present if pkt.Flags.SMB2_FLAGS_RELATED_OPERATIONS: @@ -632,12 +692,19 @@ def on_setup_andx_request(self, pkt, ssp_blob): ): # SMB1 extended / SMB2 if SMB2_Session_Setup_Request in pkt: - # SMB2 resp = self.smb_header.copy() / SMB2_Session_Setup_Response() if self.GUEST_LOGIN: + # "If the security subsystem indicates that the session + # was established by a guest user, Session.SigningRequired + # MUST be set to FALSE and Session.IsGuest MUST be set to TRUE." resp.SessionFlags = "IS_GUEST" + self.session.IsGuest = True + self.session.SigningRequired = False if self.ANONYMOUS_LOGIN: resp.SessionFlags = "IS_NULL" + # [MS-SMB2] sect 3.3.5.5.3 + if self.session.Dialect >= 0x0300 and self.REQUIRE_ENCRYPTION: + resp.SessionFlags += "ENCRYPT_DATA" else: # SMB1 extended resp = ( @@ -672,10 +739,23 @@ def on_setup_andx_request(self, pkt, ssp_blob): ) if status == GSS_S_COMPLETE: # Authentication was successful - self.session.computeSMBSessionKey() + self.session.computeSMBSessionKeys(IsClient=False) self.authenticated = True - # and send + # [MS-SMB2] Note: "Windows-based servers always sign the final session setup + # response when the user is neither anonymous nor guest." + # If not available, it will still be ignored. + self.NextForceSign = True self.send(resp) + # Check whether we must enable encryption from now on + if ( + self.authenticated + and not self.session.IsGuest + and self.session.Dialect >= 0x0300 + and self.REQUIRE_ENCRYPTION + ): + # [MS-SMB2] sect 3.3.5.5.3: from now on, turn encryption on ! + self.session.EncryptData = True + self.session.SigningRequired = False @ATMT.condition(RECEIVED_SETUP_ANDX_REQUEST) def wait_for_next_request(self): @@ -771,7 +851,9 @@ def receive_tree_connect(self, pkt): def send_tree_connect_response(self, pkt, tree_name): self.update_smbheader(pkt) # Check the tree name against the shares we're serving - if not any(x._name == tree_name.lower() for x in self.shares): + try: + share = next(x for x in self.shares if x._name == tree_name.lower()) + except StopIteration: # Unknown tree resp = self.smb_header.copy() / SMB2_Error_Response() resp.Command = "SMB2_TREE_CONNECT" @@ -783,17 +865,32 @@ def send_tree_connect_response(self, pkt, tree_name): self.tree_id += 1 self.smb_header.TID = self.tree_id self.current_trees[self.smb_header.TID] = tree_name + + # Construct ShareFlags + ShareFlags = ( + "AUTO_CACHING+NO_CACHING" + if self.current_tree() == "IPC$" + else self.TREE_SHARE_FLAGS + ) + # [MS-SMB2] sect 3.3.5.7 + if ( + self.session.Dialect >= 0x0311 + and not self.session.EncryptData + and share.encryptdata + ): + if not self.session.SupportsEncryption: + raise Exception("Peer asked for encryption but doesn't support it !") + ShareFlags += "+ENCRYPT_DATA" + self.vprint("Tree Connect on: %s" % tree_name) self.send( - self.smb_header + self.smb_header.copy() / SMB2_Tree_Connect_Response( ShareType="PIPE" if self.current_tree() == "IPC$" else "DISK", - ShareFlags="AUTO_CACHING+NO_CACHING" - if self.current_tree() == "IPC$" - else self.TREE_SHARE_FLAGS, - Capabilities=0 - if self.current_tree() == "IPC$" - else self.TREE_CAPABILITIES, + ShareFlags=ShareFlags, + Capabilities=( + 0 if self.current_tree() == "IPC$" else self.TREE_CAPABILITIES + ), MaximalAccess=self.TREE_MAXIMAL_ACCESS, ) ) @@ -848,7 +945,11 @@ def send_ioctl_response(self, pkt): SMB2_IOCTL_Validate_Negotiate_Info_Response( GUID=self.GUID, DialectRevision=self.session.Dialect, - SecurityMode=self.session.SecurityMode, + SecurityMode=( + "SIGNING_ENABLED+SIGNING_REQUIRED" + if self.session.SigningRequired + else "SIGNING_ENABLED" + ), Capabilities=self.NegotiateCapabilities, ), ) diff --git a/test/scapy/layers/smb2.uts b/test/scapy/layers/smb2.uts index 7bd4969e206..5ecb92a9904 100644 --- a/test/scapy/layers/smb2.uts +++ b/test/scapy/layers/smb2.uts @@ -308,8 +308,7 @@ enc_context = SMB2_Negotiate_Context(ContextType = 2, DataLength = len(enc)) / e comp = SMB2_Compression_Capabilities() comp_context = SMB2_Negotiate_Context(ContextType = 3, DataLength = len(comp)) / comp -netname = SMB2_Netname_Negotiate_Context_ID("192.168.178.21".encode("utf-16le")) -netname_context = SMB2_Negotiate_Context(ContextType = 5, DataLength = len(netname)) / netname +netname_context = SMB2_Negotiate_Context(b'\x05\x00\x1c\x00\x00\x00\x00\x001\x009\x002\x00.\x001\x006\x008\x00.\x001\x007\x008\x00.\x002\x001\x00') pkt = SMB2_Header() / SMB2_Negotiate_Protocol_Request(Dialects=[0x0311], NegotiateContexts=[preauth_context, enc_context, comp_context, netname_context], NegotiateContextsBufferOffset=0x68) diff --git a/test/scapy/layers/smbclientserver.uts b/test/scapy/layers/smbclientserver.uts index 6279735d8eb..101843cbdc6 100644 --- a/test/scapy/layers/smbclientserver.uts +++ b/test/scapy/layers/smbclientserver.uts @@ -259,28 +259,31 @@ with (ROOTPATH / "fileScapy").open("w") as fd: fd.write("Nice\nData") class run_smbserver: - def __init__(self, guest=False, readonly=True): + def __init__(self, guest=False, readonly=True, encryptshare=False, MAX_DIALECT=0x311): self.srv = None self.guest = guest self.readonly = readonly + self.encryptshare = encryptshare + self.MAX_DIALECT = MAX_DIALECT def __enter__(self): if self.guest: - IDENTITIES = None + ssp = None else: - IDENTITIES = { + ssp = SPNEGOSSP([NTLMSSP(IDENTITIES={ "User1": MD4le("Password1"), "Administrator": MD4le("Password2") - } + })]) self.srv = smbserver( - shares=[SMBShare("Scapy", ROOTPATH), SMBShare("test", ROOTPATH)], + shares=[SMBShare("Scapy", ROOTPATH, encryptdata=self.encryptshare), + SMBShare("test", ROOTPATH, encryptdata=self.encryptshare)], iface=conf.loopback_name, debug=4, port=12345, bg=True, readonly=self.readonly, - # NTLMSSP - IDENTITIES=IDENTITIES, + MAX_DIALECT=self.MAX_DIALECT, + ssp=ssp, ) def __exit__(self, exc_type, exc_value, traceback): @@ -290,7 +293,7 @@ class run_smbserver: # define client class run_smbclient: - def __init__(self, user=None, password=None, share=None, list=False, cwd=None, debug=None, maxversion=None): + def __init__(self, user=None, password=None, share=None, list=False, cwd=None, debug=None, maxversion=None, encrypt=False): args = [ "smbclient", ] + (["-L"] if list else []) + [ @@ -308,6 +311,8 @@ class run_smbclient: args.append("-N") if maxversion: args.extend(["-m", maxversion]) + if encrypt: + args.extend(["--client-protection", "encrypt"]) self.args = args self.proc = subprocess.Popen( args, @@ -427,3 +432,51 @@ with run_smbserver(readonly=False): raise finally: cli.close() + += smbserver: SMB 3.0.2 - require global encryption + +LOCALPATH = pathlib.Path(get_temp_dir()) + +nicedata = ("A" * 100 + "\n") * 5 +with open(LOCALPATH / "newCustomFile", "w") as fd: + fd.write(nicedata) + +with run_smbserver(readonly=False, MAX_DIALECT=0x0302): + try: + cli = run_smbclient(user="Administrator", password="Password2", share="test", cwd=LOCALPATH, encrypt=True) + cli.cmd("put newCustomFile") + output = cli.getoutput() + print(output) + assert "putting file newCustomFile" in output[0], "strange output" + assert (ROOTPATH / "newCustomFile").exists(), "file doesn't exist" + with (ROOTPATH / "newCustomFile").open("r") as fd: + assert fd.read() == nicedata, "invalid data" + except Exception: + cli.printdebug() + raise + finally: + cli.close() + += smbserver: SMB 3.1.1 - require share encryption + +LOCALPATH = pathlib.Path(get_temp_dir()) + +nicedata = ("A" * 100 + "\n") * 5 +with open(LOCALPATH / "newCustomFile", "w") as fd: + fd.write(nicedata) + +with run_smbserver(readonly=False, encryptshare=True): + try: + cli = run_smbclient(user="Administrator", password="Password2", share="test", cwd=LOCALPATH) + cli.cmd("put newCustomFile") + output = cli.getoutput() + print(output) + assert "putting file newCustomFile" in output[0], "strange output" + assert (ROOTPATH / "newCustomFile").exists(), "file doesn't exist" + with (ROOTPATH / "newCustomFile").open("r") as fd: + assert fd.read() == nicedata, "invalid data" + except Exception: + cli.printdebug() + raise + finally: + cli.close()