Skip to content

Commit f6ba322

Browse files
authoredFeb 3, 2025··
SMB: Implement encryption, cleanup RequireSignature (#4643)
Signed-By: gpotter2 <10530980+gpotter2@users.noreply.github.com>
1 parent b4dbb19 commit f6ba322

File tree

10 files changed

+622
-143
lines changed

10 files changed

+622
-143
lines changed
 

‎.config/codespell_ignore.txt

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ browseable
88
byteorder
99
cace
1010
cas
11+
ciph
1112
componet
1213
comversion
1314
cros

‎doc/scapy/layers/smb.rst

+15-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ Scapy provides pretty good support for SMB 2/3 and very partial support of SMB1.
55

66
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.
77

8-
.. warning:: Encryption is currently not supported in neither the client nor server.
9-
108
.. _client:
119

1210
SMB 2/3 client
@@ -94,6 +92,12 @@ You might be wondering if you can pass the ``HashNT`` of the password of the use
9492
9593
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)])``.
9694

95+
**smbclient forcing encryption**:
96+
97+
.. code:: python
98+
99+
>>> smbclient("server1.domain.local", "admin", REQUIRE_ENCRYPTION=True)
100+
97101
.. note::
98102

99103
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
306310
readonly=False,
307311
)
308312
313+
**Start a SMB server requiring encryption (two methods)**:
314+
315+
.. code:: python
316+
317+
# Method 1: require encryption globally (available in SMB 3.0.0+)
318+
>>> smbserver(..., REQUIRE_ENCRYPTION=True)
319+
# Method 2: for a specific share (only available in SMB 3.1.1+)
320+
>>> smbserver(..., shares=[SMBShare(name="Scapy", path="/tmp", encryptdata=True)])
321+
309322
.. note::
310323

311324
It is possible to start the :class:`~scapy.layers.smbserver.smbserver` (albeit only in unauthenticated mode) directly from the OS, using the following::

‎doc/scapy/usage.rst

+5
Original file line numberDiff line numberDiff line change
@@ -798,7 +798,12 @@ Available by default:
798798
- HTTP 1.0
799799
- TLS
800800
- Kerberos
801+
- LDAP
802+
- SMB
801803
- DCE/RPC
804+
- Postgres
805+
- DOIP
806+
- and maybe other protocols if this page isn't up to date.
802807
- :py:class:`~scapy.sessions.TLSSession` -> *matches TLS sessions* on the flow.
803808
- :py:class:`~scapy.sessions.NetflowSession` -> *resolve Netflow V9 packets* from their NetflowFlowset information objects
804809

‎scapy/layers/ntlm.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -1680,9 +1680,7 @@ def GSS_Accept_sec_context(self, Context: CONTEXT, val=None):
16801680
EncryptedRandomSessionKey = b"\x00" * 16
16811681
else:
16821682
EncryptedRandomSessionKey = auth_tok.EncryptedRandomSessionKey
1683-
ExportedSessionKey = RC4K(
1684-
KeyExchangeKey, EncryptedRandomSessionKey
1685-
)
1683+
ExportedSessionKey = RC4K(KeyExchangeKey, EncryptedRandomSessionKey)
16861684
else:
16871685
ExportedSessionKey = KeyExchangeKey
16881686
Context.ExportedSessionKey = ExportedSessionKey
@@ -1800,6 +1798,7 @@ def _getSessionBaseKey(self, Context, auth_tok):
18001798
return NTLMv2_ComputeSessionBaseKey(
18011799
ResponseKeyNT, auth_tok.NtChallengeResponse.NTProofStr
18021800
)
1801+
log_runtime.debug("NTLMSSP: Bad credentials for %s" % username)
18031802
return None
18041803

18051804
def _checkLogin(self, Context, auth_tok):

‎scapy/layers/smb.py

+31-19
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@
6767
)
6868
from scapy.layers.smb2 import (
6969
STATUS_ERREF,
70+
SMB2_Compression_Transform_Header,
7071
SMB2_Header,
72+
SMB2_Transform_Header,
7173
)
7274

7375

@@ -919,11 +921,9 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs):
919921
elif _pkt[0] == 0x13: # LOGON_SAM_USER_RESPONSE
920922
try:
921923
i = _pkt.index(b"\xff\xff\xff\xff")
922-
NtVersion = (
923-
NETLOGON_SAM_LOGON_RESPONSE_NT40.fields_desc[-3].getfield(
924-
None, _pkt[i - 4:i]
925-
)[1]
926-
)
924+
NtVersion = NETLOGON_SAM_LOGON_RESPONSE_NT40.fields_desc[
925+
-3
926+
].getfield(None, _pkt[i - 4 : i])[1]
927927
if NtVersion.V1 and not NtVersion.V5:
928928
return NETLOGON_SAM_LOGON_RESPONSE_NT40
929929
except Exception:
@@ -1013,6 +1013,7 @@ class NETLOGON_SAM_LOGON_RESPONSE_NT40(NETLOGON):
10131013

10141014
# [MS-ADTS] sect 6.3.1.8
10151015

1016+
10161017
class NETLOGON_SAM_LOGON_RESPONSE(NETLOGON, DNSCompressedPacket):
10171018
fields_desc = [
10181019
LEShortEnumField("OpCode", 0x17, _NETLOGON_opcodes),
@@ -1085,8 +1086,7 @@ def pre_dissect(self, s):
10851086
try:
10861087
i = s.index(b"\xff\xff\xff\xff")
10871088
self.fields["NtVersion"] = self.fields_desc[-3].getfield(
1088-
self,
1089-
s[i - 4:i]
1089+
self, s[i - 4 : i]
10901090
)[1]
10911091
except Exception:
10921092
self.NtVersion = 0xB
@@ -1098,20 +1098,25 @@ def get_full(self):
10981098

10991099
# [MS-BRWS] sect 2.2
11001100

1101+
11011102
class BRWS(Packet):
11021103
fields_desc = [
1103-
ByteEnumField("OpCode", 0x00, {
1104-
0x01: "HostAnnouncement",
1105-
0x02: "AnnouncementRequest",
1106-
0x08: "RequestElection",
1107-
0x09: "GetBackupListRequest",
1108-
0x0A: "GetBackupListResponse",
1109-
0x0B: "BecomeBackup",
1110-
0x0C: "DomainAnnouncement",
1111-
0x0D: "MasterAnnouncement",
1112-
0x0E: "ResetStateRequest",
1113-
0x0F: "LocalMasterAnnouncement",
1114-
}),
1104+
ByteEnumField(
1105+
"OpCode",
1106+
0x00,
1107+
{
1108+
0x01: "HostAnnouncement",
1109+
0x02: "AnnouncementRequest",
1110+
0x08: "RequestElection",
1111+
0x09: "GetBackupListRequest",
1112+
0x0A: "GetBackupListResponse",
1113+
0x0B: "BecomeBackup",
1114+
0x0C: "DomainAnnouncement",
1115+
0x0D: "MasterAnnouncement",
1116+
0x0E: "ResetStateRequest",
1117+
0x0F: "LocalMasterAnnouncement",
1118+
},
1119+
),
11151120
]
11161121

11171122
def mysummary(self):
@@ -1135,6 +1140,7 @@ def default_payload_class(self, payload):
11351140

11361141
# [MS-BRWS] sect 2.2.1
11371142

1143+
11381144
class BRWS_HostAnnouncement(BRWS):
11391145
OpCode = 0x01
11401146
fields_desc = [
@@ -1157,6 +1163,7 @@ def mysummary(self):
11571163

11581164
# [MS-BRWS] sect 2.2.6
11591165

1166+
11601167
class BRWS_BecomeBackup(BRWS):
11611168
OpCode = 0x0B
11621169
fields_desc = [
@@ -1170,6 +1177,7 @@ def mysummary(self):
11701177

11711178
# [MS-BRWS] sect 2.2.10
11721179

1180+
11731181
class BRWS_LocalMasterAnnouncement(BRWS_HostAnnouncement):
11741182
OpCode = 0x0F
11751183

@@ -1193,6 +1201,10 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs):
11931201
return SMB_Header
11941202
if _pkt[:4] == b"\xfeSMB":
11951203
return SMB2_Header
1204+
if _pkt[:4] == b"\xfdSMB":
1205+
return SMB2_Transform_Header
1206+
if _pkt[:4] == b"\xfcSMB":
1207+
return SMB2_Compression_Transform_Header
11961208
return cls
11971209

11981210

‎scapy/layers/smb2.py

+305-44
Large diffs are not rendered by default.

‎scapy/layers/smbclient.py

+71-36
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@
8383
SMB2_CREATE_REQUEST_LEASE,
8484
SMB2_Create_Request,
8585
SMB2_Create_Response,
86-
SMB2_Encryption_Capabilities,
8786
SMB2_ENCRYPTION_CIPHERS,
87+
SMB2_Encryption_Capabilities,
8888
SMB2_Error_Response,
8989
SMB2_Header,
9090
SMB2_IOCTL_Request,
@@ -100,9 +100,9 @@
100100
SMB2_Query_Info_Response,
101101
SMB2_Read_Request,
102102
SMB2_Read_Response,
103+
SMB2_SIGNING_ALGORITHMS,
103104
SMB2_Session_Setup_Request,
104105
SMB2_Session_Setup_Response,
105-
SMB2_SIGNING_ALGORITHMS,
106106
SMB2_Signing_Capabilities,
107107
SMB2_Tree_Connect_Request,
108108
SMB2_Tree_Connect_Response,
@@ -127,6 +127,7 @@ class SMB_Client(Automaton):
127127
All other options (in caps) are optional, and SMB specific:
128128
129129
:param REQUIRE_SIGNATURE: set 'Require Signature'
130+
:param REQUIRE_ENCRYPTION: set 'Requite Encryption'
130131
:param MIN_DIALECT: minimum SMB dialect. Defaults to 0x0202 (2.0.2)
131132
:param MAX_DIALECT: maximum SMB dialect. Defaults to 0x0311 (3.1.1)
132133
:param DIALECTS: list of supported SMB2 dialects.
@@ -140,7 +141,8 @@ def __init__(self, sock, ssp=None, *args, **kwargs):
140141
# Various SMB client arguments
141142
self.EXTENDED_SECURITY = kwargs.pop("EXTENDED_SECURITY", True)
142143
self.USE_SMB1 = kwargs.pop("USE_SMB1", False)
143-
self.REQUIRE_SIGNATURE = kwargs.pop("REQUIRE_SIGNATURE", False)
144+
self.REQUIRE_SIGNATURE = kwargs.pop("REQUIRE_SIGNATURE", None)
145+
self.REQUIRE_ENCRYPTION = kwargs.pop("REQUIRE_ENCRYPTION", False)
144146
self.RETRY = kwargs.pop("RETRY", 0) # optionally: retry n times session setup
145147
self.SMB2 = kwargs.pop("SMB2", False) # optionally: start directly in SMB2
146148
self.SERVER_NAME = kwargs.pop("SERVER_NAME", "")
@@ -158,7 +160,6 @@ def __init__(self, sock, ssp=None, *args, **kwargs):
158160
]
159161
)
160162
# Internal Session information
161-
self.IsGuest = False
162163
self.ErrorStatus = None
163164
self.NegotiateCapabilities = None
164165
self.GUID = RandUUID()._fix()
@@ -187,9 +188,8 @@ def __init__(self, sock, ssp=None, *args, **kwargs):
187188
self.smb_sock_ready = threading.Event()
188189
# Set session options
189190
self.session.ssp = ssp
190-
self.session.SecurityMode = kwargs.pop(
191-
"SECURITY_MODE",
192-
3 if self.REQUIRE_SIGNATURE else int(bool(ssp)),
191+
self.session.SigningRequired = (
192+
self.REQUIRE_SIGNATURE if self.REQUIRE_SIGNATURE is not None else bool(ssp)
193193
)
194194
self.session.Dialect = self.MAX_DIALECT
195195

@@ -319,7 +319,11 @@ def on_negotiate_smb2(self):
319319
# [MS-SMB2] sect 3.2.4.2.2.2 - SMB2-Only Negotiate
320320
pkt = self.smb_header.copy() / SMB2_Negotiate_Protocol_Request(
321321
Dialects=self.DIALECTS,
322-
SecurityMode=self.session.SecurityMode,
322+
SecurityMode=(
323+
"SIGNING_ENABLED+SIGNING_REQUIRED"
324+
if self.session.SigningRequired
325+
else "SIGNING_ENABLED"
326+
),
323327
)
324328
if self.MAX_DIALECT >= 0x0210:
325329
# "If the client implements the SMB 2.1 or SMB 3.x dialect, ClientGuid
@@ -340,25 +344,21 @@ def on_negotiate_smb2(self):
340344
"MULTI_CHANNEL",
341345
"PERSISTENT_HANDLES",
342346
"DIRECTORY_LEASING",
347+
"ENCRYPTION",
343348
]
344349
)
345-
if self.MAX_DIALECT >= 0x0300:
346-
# "If the client implements the SMB 3.x dialect family, the client MUST
347-
# set the Capabilities field as follows"
348-
self.NegotiateCapabilities += "+ENCRYPTION"
349350
if self.MAX_DIALECT >= 0x0311:
350351
# "If the client implements the SMB 3.1.1 dialect, it MUST do"
351352
pkt.NegotiateContexts = [
352353
SMB2_Negotiate_Context()
353354
/ SMB2_Preauth_Integrity_Capabilities(
354-
# SHA-512 by default
355-
HashAlgorithms=[self.session.PreauthIntegrityHashId],
355+
# As for today, no other hash algorithm is described by the spec
356+
HashAlgorithms=["SHA-512"],
356357
Salt=self.session.Salt,
357358
),
358359
SMB2_Negotiate_Context()
359360
/ SMB2_Encryption_Capabilities(
360-
# AES-128-CCM by default
361-
Ciphers=[self.session.CipherId],
361+
Ciphers=self.session.SupportedCipherIds,
362362
),
363363
# TODO support compression and RDMA
364364
SMB2_Negotiate_Context()
@@ -367,8 +367,7 @@ def on_negotiate_smb2(self):
367367
),
368368
SMB2_Negotiate_Context()
369369
/ SMB2_Signing_Capabilities(
370-
# AES-128-CCM by default
371-
SigningAlgorithms=[self.session.SigningAlgorithmId],
370+
SigningAlgorithms=self.session.SupportedSigningAlgorithmIds,
372371
),
373372
]
374373
pkt.Capabilities = self.NegotiateCapabilities
@@ -416,26 +415,41 @@ def receive_negotiate_response(self, pkt):
416415
self.MaxReadSize = pkt.MaxReadSize
417416
self.MaxTransactionSize = pkt.MaxTransactionSize
418417
self.MaxWriteSize = pkt.MaxWriteSize
418+
# Process SecurityMode
419+
if pkt.SecurityMode.SIGNING_REQUIRED:
420+
self.session.SigningRequired = True
421+
# Process capabilities
422+
if self.session.Dialect >= 0x0300:
423+
self.session.SupportsEncryption = pkt.Capabilities.ENCRYPTION
419424
# Process NegotiateContext
420425
if self.session.Dialect >= 0x0311 and pkt.NegotiateContextsCount:
421426
for ngctx in pkt.NegotiateContexts:
422427
if ngctx.ContextType == 0x0002:
423428
# SMB2_ENCRYPTION_CAPABILITIES
424-
self.session.CipherId = SMB2_ENCRYPTION_CIPHERS[
425-
ngctx.Ciphers[0]
426-
]
429+
if ngctx.Ciphers[0] != 0:
430+
self.session.CipherId = SMB2_ENCRYPTION_CIPHERS[
431+
ngctx.Ciphers[0]
432+
]
433+
self.session.SupportsEncryption = True
427434
elif ngctx.ContextType == 0x0008:
428435
# SMB2_SIGNING_CAPABILITIES
429436
self.session.SigningAlgorithmId = (
430437
SMB2_SIGNING_ALGORITHMS[ngctx.SigningAlgorithms[0]]
431438
)
439+
if self.REQUIRE_ENCRYPTION and not self.session.SupportsEncryption:
440+
self.ErrorStatus = "NEGOTIATE FAILURE: encryption."
441+
raise self.NEGO_FAILED()
432442
self.update_smbheader(pkt)
433443
raise self.NEGOTIATED(ssp_blob)
434444
elif SMBNegotiate_Response_Security in pkt:
435445
# Non-extended SMB1
436446
# Never tested. FIXME. probably broken
437447
raise self.NEGOTIATED(pkt.Challenge)
438448

449+
@ATMT.state(final=1)
450+
def NEGO_FAILED(self):
451+
self.smb_sock_ready.set()
452+
439453
@ATMT.state()
440454
def NEGOTIATED(self, ssp_blob=None):
441455
# Negotiated ! We now know the Dialect
@@ -448,11 +462,7 @@ def NEGOTIATED(self, ssp_blob=None):
448462
ssp_blob,
449463
req_flags=(
450464
GSS_C_FLAGS.GSS_C_MUTUAL_FLAG
451-
| (
452-
GSS_C_FLAGS.GSS_C_INTEG_FLAG
453-
if self.session.SecurityMode != 0
454-
else 0
455-
)
465+
| (GSS_C_FLAGS.GSS_C_INTEG_FLAG if self.session.SigningRequired else 0)
456466
),
457467
)
458468
return ssp_tuple
@@ -498,7 +508,11 @@ def send_setup_andx_request(self, ssp_tuple):
498508
# SMB2
499509
pkt = self.smb_header.copy() / SMB2_Session_Setup_Request(
500510
Capabilities="DFS",
501-
SecurityMode=self.session.SecurityMode,
511+
SecurityMode=(
512+
"SIGNING_ENABLED+SIGNING_REQUIRED"
513+
if self.session.SigningRequired
514+
else "SIGNING_ENABLED"
515+
),
502516
)
503517
else:
504518
# SMB1 extended
@@ -562,9 +576,18 @@ def receive_setup_andx_response(self, pkt):
562576
self.smb_header.SessionId = pkt.SessionId
563577
# SMB1 extended / SMB2
564578
if pkt.Status == 0: # Authenticated
565-
if SMB2_Session_Setup_Response in pkt and pkt.SessionFlags.IS_GUEST:
566-
# We were 'authenticated' in GUEST
567-
self.IsGuest = True
579+
if SMB2_Session_Setup_Response in pkt:
580+
# [MS-SMB2] sect 3.2.5.3.1
581+
if pkt.SessionFlags.IS_GUEST:
582+
# "If the security subsystem indicates that the session
583+
# was established by a guest user, Session.SigningRequired
584+
# MUST be set to FALSE and Session.IsGuest MUST be set to TRUE."
585+
self.session.IsGuest = True
586+
self.session.SigningRequired = False
587+
elif self.session.Dialect >= 0x0300:
588+
if pkt.SessionFlags.ENCRYPT_DATA or self.REQUIRE_ENCRYPTION:
589+
self.session.EncryptData = True
590+
self.session.SigningRequired = False
568591
raise self.AUTHENTICATED(pkt.SecurityBlob)
569592
else:
570593
if SMB2_Header in pkt:
@@ -600,10 +623,7 @@ def AUTHENTICATED(self, ssp_blob=None):
600623
if status != GSS_S_COMPLETE:
601624
raise ValueError("Internal error: the SSP completed with an error.")
602625
# Authentication was successful
603-
self.session.computeSMBSessionKey()
604-
if self.IsGuest:
605-
# When authenticated in Guest, the sessionkey the client has is invalid
606-
self.session.SMBSessionKey = None
626+
self.session.computeSMBSessionKeys(IsClient=True)
607627

608628
# DEV: add a condition on AUTHENTICATED with prio=0
609629

@@ -663,7 +683,9 @@ def __init__(self, smbsock, use_ioctl=True, timeout=3):
663683
self.ins = smbsock
664684
self.timeout = timeout
665685
if not self.ins.atmt.smb_sock_ready.wait(timeout=timeout):
666-
self.ins.atmt.session.sspcontext.clifailure()
686+
# If we have a SSP, tell it we failed.
687+
if self.ins.atmt.session.sspcontext:
688+
self.ins.atmt.session.sspcontext.clifailure()
667689
raise TimeoutError(
668690
"The SMB handshake timed out ! (enable debug=1 for logs)"
669691
)
@@ -725,6 +747,12 @@ def tree_connect(self, name):
725747
raise ValueError("TreeConnect timed out !")
726748
if SMB2_Tree_Connect_Response not in resp:
727749
raise ValueError("Failed TreeConnect ! %s" % resp.NTStatus)
750+
# [MS-SMB2] sect 3.2.5.5
751+
if self.session.Dialect >= 0x0300:
752+
if resp.ShareFlags.ENCRYPT_DATA and self.session.SupportsEncryption:
753+
self.session.TreeEncryptData = True
754+
else:
755+
self.session.TreeEncryptData = False
728756
return self.get_TID()
729757

730758
def tree_disconnect(self):
@@ -1078,6 +1106,11 @@ class smbclient(CLIUtil):
10781106
:param ST: if provided, the service ticket to use (Kerberos)
10791107
:param KEY: if provided, the session key associated to the ticket (Kerberos)
10801108
:param cli: CLI mode (default True). False to use for scripting
1109+
1110+
Some additional SMB parameters are available under help(SMB_Client). Some of
1111+
them include the following:
1112+
1113+
:param REQUIRE_ENCRYPTION: requires encryption.
10811114
"""
10821115

10831116
def __init__(
@@ -1097,6 +1130,7 @@ def __init__(
10971130
KEY=None,
10981131
cli=True,
10991132
# SMB arguments
1133+
REQUIRE_ENCRYPTION=False,
11001134
**kwargs,
11011135
):
11021136
if cli:
@@ -1187,6 +1221,7 @@ def __init__(
11871221
sock,
11881222
ssp=ssp,
11891223
debug=debug,
1224+
REQUIRE_ENCRYPTION=REQUIRE_ENCRYPTION,
11901225
**kwargs,
11911226
)
11921227
try:
@@ -1229,7 +1264,7 @@ def __init__(
12291264
"SMB %s" % self.smbsock.session.Dialect,
12301265
),
12311266
repr(self.smbsock.session.sspcontext),
1232-
" as GUEST" if self.sock.atmt.IsGuest else "",
1267+
" as GUEST" if self.smbsock.session.IsGuest else "",
12331268
)
12341269
)
12351270
# Now define some variables for our CLI

‎scapy/layers/smbserver.py

+130-29
Original file line numberDiff line numberDiff line change
@@ -76,26 +76,27 @@
7676
FileStreamInformation,
7777
NETWORK_INTERFACE_INFO,
7878
SECURITY_DESCRIPTOR,
79+
SMB2_CREATE_DURABLE_HANDLE_RESPONSE_V2,
80+
SMB2_CREATE_QUERY_MAXIMAL_ACCESS_RESPONSE,
81+
SMB2_CREATE_QUERY_ON_DISK_ID,
7982
SMB2_Cancel_Request,
8083
SMB2_Change_Notify_Request,
8184
SMB2_Change_Notify_Response,
8285
SMB2_Close_Request,
8386
SMB2_Close_Response,
8487
SMB2_Create_Context,
85-
SMB2_CREATE_DURABLE_HANDLE_RESPONSE_V2,
86-
SMB2_CREATE_QUERY_MAXIMAL_ACCESS_RESPONSE,
87-
SMB2_CREATE_QUERY_ON_DISK_ID,
8888
SMB2_Create_Request,
8989
SMB2_Create_Response,
90+
SMB2_ENCRYPTION_CIPHERS,
9091
SMB2_Echo_Request,
9192
SMB2_Echo_Response,
9293
SMB2_Encryption_Capabilities,
9394
SMB2_Error_Response,
9495
SMB2_FILEID,
9596
SMB2_Header,
9697
SMB2_IOCTL_Network_Interface_Info,
97-
SMB2_IOCTL_Request,
9898
SMB2_IOCTL_RESP_GET_DFS_Referral,
99+
SMB2_IOCTL_Request,
99100
SMB2_IOCTL_Response,
100101
SMB2_IOCTL_Validate_Negotiate_Info_Response,
101102
SMB2_Negotiate_Context,
@@ -108,6 +109,7 @@
108109
SMB2_Query_Info_Response,
109110
SMB2_Read_Request,
110111
SMB2_Read_Response,
112+
SMB2_SIGNING_ALGORITHMS,
111113
SMB2_Session_Logoff_Request,
112114
SMB2_Session_Logoff_Response,
113115
SMB2_Session_Setup_Request,
@@ -155,9 +157,11 @@ class SMBShare:
155157
:param path: the path the the folder hosted by the share
156158
:param type: (optional) share type per [MS-SRVS] sect 2.2.2.4
157159
:param remark: (optional) a description of the share
160+
:param encryptdata: (optional) whether encryption should be used for this
161+
share. This only applies to SMB 3.1.1.
158162
"""
159163

160-
def __init__(self, name, path=".", type=None, remark=""):
164+
def __init__(self, name, path=".", type=None, remark="", encryptdata=False):
161165
# Set the default type
162166
if type is None:
163167
type = 0 # DISKTREE
@@ -171,6 +175,7 @@ def __init__(self, name, path=".", type=None, remark=""):
171175
self.name = name
172176
self.type = type
173177
self.remark = remark
178+
self.encryptdata = encryptdata
174179

175180
def __repr__(self):
176181
type = SRVSVC_SHARE_TYPES[self.type & 0x0FFFFFFF]
@@ -202,6 +207,8 @@ class SMB_Server(Automaton):
202207
:param ANONYMOUS_LOGIN: mark the clients as anonymous
203208
:param GUEST_LOGIN: mark the clients as guest
204209
:param REQUIRE_SIGNATURE: set 'Require Signature'
210+
:param REQUIRE_ENCRYPTION: globally require encryption.
211+
You could also make it share-specific on 3.1.1.
205212
:param MAX_DIALECT: maximum SMB dialect. Defaults to 0x0311 (3.1.1)
206213
:param TREE_SHARE_FLAGS: flags to announce on Tree_Connect_Response
207214
: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
223230
self.GUEST_LOGIN = kwargs.pop("GUEST_LOGIN", None)
224231
self.EXTENDED_SECURITY = kwargs.pop("EXTENDED_SECURITY", True)
225232
self.USE_SMB1 = kwargs.pop("USE_SMB1", False)
226-
self.REQUIRE_SIGNATURE = kwargs.pop("REQUIRE_SIGNATURE", False)
233+
self.REQUIRE_SIGNATURE = kwargs.pop("REQUIRE_SIGNATURE", None)
234+
self.REQUIRE_ENCRYPTION = kwargs.pop("REQUIRE_ENCRYPTION", False)
227235
self.MAX_DIALECT = kwargs.pop("MAX_DIALECT", 0x0311)
228236
self.TREE_SHARE_FLAGS = kwargs.pop(
229237
"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
294302
self.SMB2 = False
295303
self.NegotiateCapabilities = None
296304
self.GUID = RandUUID()._fix()
305+
self.NextForceSign = False
306+
self.NextForceEncrypt = False
297307
# Compounds are handled on receiving by the StreamSocket,
298308
# and on aggregated in a CompoundQueue to be sent in one go
299309
self.NextCompound = False
@@ -315,9 +325,8 @@ def __init__(self, shares=[], ssp=None, verb=True, readonly=True, *args, **kwarg
315325
Automaton.__init__(self, *args, **kwargs)
316326
# Set session options
317327
self.session.ssp = ssp
318-
self.session.SecurityMode = kwargs.pop(
319-
"SECURITY_MODE",
320-
3 if self.REQUIRE_SIGNATURE else bool(ssp),
328+
self.session.SigningRequired = (
329+
self.REQUIRE_SIGNATURE if self.REQUIRE_SIGNATURE is not None else bool(ssp)
321330
)
322331

323332
@property
@@ -336,7 +345,14 @@ def vprint(self, s=""):
336345
print("> %s" % s)
337346

338347
def send(self, pkt):
339-
return super(SMB_Server, self).send(pkt, Compounded=self.NextCompound)
348+
ForceSign, ForceEncrypt = self.NextForceSign, self.NextForceEncrypt
349+
self.NextForceSign = self.NextForceEncrypt = False
350+
return super(SMB_Server, self).send(
351+
pkt,
352+
Compounded=self.NextCompound,
353+
ForceSign=ForceSign,
354+
ForceEncrypt=ForceEncrypt,
355+
)
340356

341357
@ATMT.state(initial=1)
342358
def BEGIN(self):
@@ -433,6 +449,9 @@ def on_negotiate(self, pkt):
433449
self.send(resp)
434450
return
435451
if self.SMB2: # SMB2
452+
# SecurityMode
453+
if SMB2_Header in pkt and pkt.SecurityMode.SIGNING_REQUIRED:
454+
self.session.SigningRequired = True
436455
# Capabilities: [MS-SMB2] 3.3.5.4
437456
self.NegotiateCapabilities = "+".join(
438457
[
@@ -449,16 +468,17 @@ def on_negotiate(self, pkt):
449468
"MULTI_CHANNEL",
450469
"PERSISTENT_HANDLES",
451470
"DIRECTORY_LEASING",
471+
"ENCRYPTION",
452472
]
453473
)
454-
if DialectRevision in [0x0300, 0x0302]:
455-
# "if Connection.Dialect is "3.0" or "3.0.2""...
456-
# Note: 3.1.1 uses the ENCRYPT_DATA flag in Tree Connect Response
457-
self.NegotiateCapabilities += "+ENCRYPTION"
458474
# Build response
459475
resp = self.smb_header.copy() / cls(
460476
DialectRevision=DialectRevision,
461-
SecurityMode=self.session.SecurityMode,
477+
SecurityMode=(
478+
"SIGNING_ENABLED+SIGNING_REQUIRED"
479+
if self.session.SigningRequired
480+
else "SIGNING_ENABLED"
481+
),
462482
ServerTime=(time.time() + 11644473600) * 1e7,
463483
ServerStartTime=0,
464484
MaxTransactionSize=65536,
@@ -473,7 +493,27 @@ def on_negotiate(self, pkt):
473493
resp.MaxReadSize = 0x800000
474494
resp.MaxWriteSize = 0x800000
475495
# SMB 3.1.1
476-
if DialectRevision >= 0x0311:
496+
if DialectRevision >= 0x0311 and pkt.NegotiateContextsCount:
497+
# Negotiate context-capabilities
498+
for ngctx in pkt.NegotiateContexts:
499+
if ngctx.ContextType == 0x0002:
500+
# SMB2_ENCRYPTION_CAPABILITIES
501+
for ciph in ngctx.Ciphers:
502+
tciph = SMB2_ENCRYPTION_CIPHERS.get(ciph, None)
503+
if tciph in self.session.SupportedCipherIds:
504+
# Common !
505+
self.session.CipherId = tciph
506+
self.session.SupportsEncryption = True
507+
break
508+
elif ngctx.ContextType == 0x0008:
509+
# SMB2_SIGNING_CAPABILITIES
510+
for signalg in ngctx.SigningAlgorithms:
511+
tsignalg = SMB2_SIGNING_ALGORITHMS.get(signalg, None)
512+
if tsignalg in self.session.SupportedSigningAlgorithmIds:
513+
# Common !
514+
self.session.SigningAlgorithmId = tsignalg
515+
break
516+
# Send back the negotiated algorithms
477517
resp.NegotiateContexts = [
478518
# Preauth capabilities
479519
SMB2_Negotiate_Context()
@@ -504,7 +544,11 @@ def on_negotiate(self, pkt):
504544
"LEVEL_II_OPLOCKS+LOCK_AND_READ+NT_FIND+"
505545
"LWIO+INFOLEVEL_PASSTHRU+LARGE_READX+LARGE_WRITEX"
506546
),
507-
SecurityMode=self.session.SecurityMode,
547+
SecurityMode=(
548+
"SIGNING_ENABLED+SIGNING_REQUIRED"
549+
if self.session.SigningRequired
550+
else "SIGNING_ENABLED"
551+
),
508552
ServerTime=(time.time() + 11644473600) * 1e7,
509553
ServerTimeZone=0x3C,
510554
)
@@ -534,6 +578,11 @@ def on_negotiate(self, pkt):
534578
)
535579
self.send(resp)
536580

581+
@ATMT.state(final=1)
582+
def NEGO_FAILED(self):
583+
self.vprint("SMB Negotiate failed: encryption was not negotiated.")
584+
self.end()
585+
537586
@ATMT.state()
538587
def NEGOTIATED(self):
539588
pass
@@ -550,6 +599,17 @@ def update_smbheader(self, pkt):
550599
self.smb_header.CreditCharge = pkt.CreditCharge
551600
# If the packet has a NextCommand, set NextCompound to True
552601
self.NextCompound = bool(pkt.NextCommand)
602+
# [MS-SMB2] sect 3.3.4.1.1 - "If the request was signed by the client..."
603+
# If the packet was signed, note we must answer with a signed packet.
604+
if (
605+
not self.session.SigningRequired
606+
and pkt.SecuritySignature != b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"
607+
):
608+
self.NextForceSign = True
609+
# [MS-SMB2] sect 3.3.4.1.4 - "If the message being sent is any response to a
610+
# client request for which Request.IsEncrypted is TRUE"
611+
if pkt[SMB2_Header]._decrypted:
612+
self.NextForceEncrypt = True
553613
# [MS-SMB2] sect 3.3.5.2.7.2
554614
# Add SMB2_FLAGS_RELATED_OPERATIONS to the response if present
555615
if pkt.Flags.SMB2_FLAGS_RELATED_OPERATIONS:
@@ -632,12 +692,19 @@ def on_setup_andx_request(self, pkt, ssp_blob):
632692
):
633693
# SMB1 extended / SMB2
634694
if SMB2_Session_Setup_Request in pkt:
635-
# SMB2
636695
resp = self.smb_header.copy() / SMB2_Session_Setup_Response()
637696
if self.GUEST_LOGIN:
697+
# "If the security subsystem indicates that the session
698+
# was established by a guest user, Session.SigningRequired
699+
# MUST be set to FALSE and Session.IsGuest MUST be set to TRUE."
638700
resp.SessionFlags = "IS_GUEST"
701+
self.session.IsGuest = True
702+
self.session.SigningRequired = False
639703
if self.ANONYMOUS_LOGIN:
640704
resp.SessionFlags = "IS_NULL"
705+
# [MS-SMB2] sect 3.3.5.5.3
706+
if self.session.Dialect >= 0x0300 and self.REQUIRE_ENCRYPTION:
707+
resp.SessionFlags += "ENCRYPT_DATA"
641708
else:
642709
# SMB1 extended
643710
resp = (
@@ -672,10 +739,23 @@ def on_setup_andx_request(self, pkt, ssp_blob):
672739
)
673740
if status == GSS_S_COMPLETE:
674741
# Authentication was successful
675-
self.session.computeSMBSessionKey()
742+
self.session.computeSMBSessionKeys(IsClient=False)
676743
self.authenticated = True
677-
# and send
744+
# [MS-SMB2] Note: "Windows-based servers always sign the final session setup
745+
# response when the user is neither anonymous nor guest."
746+
# If not available, it will still be ignored.
747+
self.NextForceSign = True
678748
self.send(resp)
749+
# Check whether we must enable encryption from now on
750+
if (
751+
self.authenticated
752+
and not self.session.IsGuest
753+
and self.session.Dialect >= 0x0300
754+
and self.REQUIRE_ENCRYPTION
755+
):
756+
# [MS-SMB2] sect 3.3.5.5.3: from now on, turn encryption on !
757+
self.session.EncryptData = True
758+
self.session.SigningRequired = False
679759

680760
@ATMT.condition(RECEIVED_SETUP_ANDX_REQUEST)
681761
def wait_for_next_request(self):
@@ -771,7 +851,9 @@ def receive_tree_connect(self, pkt):
771851
def send_tree_connect_response(self, pkt, tree_name):
772852
self.update_smbheader(pkt)
773853
# Check the tree name against the shares we're serving
774-
if not any(x._name == tree_name.lower() for x in self.shares):
854+
try:
855+
share = next(x for x in self.shares if x._name == tree_name.lower())
856+
except StopIteration:
775857
# Unknown tree
776858
resp = self.smb_header.copy() / SMB2_Error_Response()
777859
resp.Command = "SMB2_TREE_CONNECT"
@@ -783,17 +865,32 @@ def send_tree_connect_response(self, pkt, tree_name):
783865
self.tree_id += 1
784866
self.smb_header.TID = self.tree_id
785867
self.current_trees[self.smb_header.TID] = tree_name
868+
869+
# Construct ShareFlags
870+
ShareFlags = (
871+
"AUTO_CACHING+NO_CACHING"
872+
if self.current_tree() == "IPC$"
873+
else self.TREE_SHARE_FLAGS
874+
)
875+
# [MS-SMB2] sect 3.3.5.7
876+
if (
877+
self.session.Dialect >= 0x0311
878+
and not self.session.EncryptData
879+
and share.encryptdata
880+
):
881+
if not self.session.SupportsEncryption:
882+
raise Exception("Peer asked for encryption but doesn't support it !")
883+
ShareFlags += "+ENCRYPT_DATA"
884+
786885
self.vprint("Tree Connect on: %s" % tree_name)
787886
self.send(
788-
self.smb_header
887+
self.smb_header.copy()
789888
/ SMB2_Tree_Connect_Response(
790889
ShareType="PIPE" if self.current_tree() == "IPC$" else "DISK",
791-
ShareFlags="AUTO_CACHING+NO_CACHING"
792-
if self.current_tree() == "IPC$"
793-
else self.TREE_SHARE_FLAGS,
794-
Capabilities=0
795-
if self.current_tree() == "IPC$"
796-
else self.TREE_CAPABILITIES,
890+
ShareFlags=ShareFlags,
891+
Capabilities=(
892+
0 if self.current_tree() == "IPC$" else self.TREE_CAPABILITIES
893+
),
797894
MaximalAccess=self.TREE_MAXIMAL_ACCESS,
798895
)
799896
)
@@ -848,7 +945,11 @@ def send_ioctl_response(self, pkt):
848945
SMB2_IOCTL_Validate_Negotiate_Info_Response(
849946
GUID=self.GUID,
850947
DialectRevision=self.session.Dialect,
851-
SecurityMode=self.session.SecurityMode,
948+
SecurityMode=(
949+
"SIGNING_ENABLED+SIGNING_REQUIRED"
950+
if self.session.SigningRequired
951+
else "SIGNING_ENABLED"
952+
),
852953
Capabilities=self.NegotiateCapabilities,
853954
),
854955
)

‎test/scapy/layers/smb2.uts

+1-2
Original file line numberDiff line numberDiff line change
@@ -308,8 +308,7 @@ enc_context = SMB2_Negotiate_Context(ContextType = 2, DataLength = len(enc)) / e
308308
comp = SMB2_Compression_Capabilities()
309309
comp_context = SMB2_Negotiate_Context(ContextType = 3, DataLength = len(comp)) / comp
310310

311-
netname = SMB2_Netname_Negotiate_Context_ID("192.168.178.21".encode("utf-16le"))
312-
netname_context = SMB2_Negotiate_Context(ContextType = 5, DataLength = len(netname)) / netname
311+
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')
313312

314313
pkt = SMB2_Header() / SMB2_Negotiate_Protocol_Request(Dialects=[0x0311], NegotiateContexts=[preauth_context, enc_context, comp_context, netname_context], NegotiateContextsBufferOffset=0x68)
315314

‎test/scapy/layers/smbclientserver.uts

+61-8
Original file line numberDiff line numberDiff line change
@@ -259,28 +259,31 @@ with (ROOTPATH / "fileScapy").open("w") as fd:
259259
fd.write("Nice\nData")
260260

261261
class run_smbserver:
262-
def __init__(self, guest=False, readonly=True):
262+
def __init__(self, guest=False, readonly=True, encryptshare=False, MAX_DIALECT=0x311):
263263
self.srv = None
264264
self.guest = guest
265265
self.readonly = readonly
266+
self.encryptshare = encryptshare
267+
self.MAX_DIALECT = MAX_DIALECT
266268

267269
def __enter__(self):
268270
if self.guest:
269-
IDENTITIES = None
271+
ssp = None
270272
else:
271-
IDENTITIES = {
273+
ssp = SPNEGOSSP([NTLMSSP(IDENTITIES={
272274
"User1": MD4le("Password1"),
273275
"Administrator": MD4le("Password2")
274-
}
276+
})])
275277
self.srv = smbserver(
276-
shares=[SMBShare("Scapy", ROOTPATH), SMBShare("test", ROOTPATH)],
278+
shares=[SMBShare("Scapy", ROOTPATH, encryptdata=self.encryptshare),
279+
SMBShare("test", ROOTPATH, encryptdata=self.encryptshare)],
277280
iface=conf.loopback_name,
278281
debug=4,
279282
port=12345,
280283
bg=True,
281284
readonly=self.readonly,
282-
# NTLMSSP
283-
IDENTITIES=IDENTITIES,
285+
MAX_DIALECT=self.MAX_DIALECT,
286+
ssp=ssp,
284287
)
285288

286289
def __exit__(self, exc_type, exc_value, traceback):
@@ -290,7 +293,7 @@ class run_smbserver:
290293
# define client
291294

292295
class run_smbclient:
293-
def __init__(self, user=None, password=None, share=None, list=False, cwd=None, debug=None, maxversion=None):
296+
def __init__(self, user=None, password=None, share=None, list=False, cwd=None, debug=None, maxversion=None, encrypt=False):
294297
args = [
295298
"smbclient",
296299
] + (["-L"] if list else []) + [
@@ -308,6 +311,8 @@ class run_smbclient:
308311
args.append("-N")
309312
if maxversion:
310313
args.extend(["-m", maxversion])
314+
if encrypt:
315+
args.extend(["--client-protection", "encrypt"])
311316
self.args = args
312317
self.proc = subprocess.Popen(
313318
args,
@@ -427,3 +432,51 @@ with run_smbserver(readonly=False):
427432
raise
428433
finally:
429434
cli.close()
435+
436+
= smbserver: SMB 3.0.2 - require global encryption
437+
438+
LOCALPATH = pathlib.Path(get_temp_dir())
439+
440+
nicedata = ("A" * 100 + "\n") * 5
441+
with open(LOCALPATH / "newCustomFile", "w") as fd:
442+
fd.write(nicedata)
443+
444+
with run_smbserver(readonly=False, MAX_DIALECT=0x0302):
445+
try:
446+
cli = run_smbclient(user="Administrator", password="Password2", share="test", cwd=LOCALPATH, encrypt=True)
447+
cli.cmd("put newCustomFile")
448+
output = cli.getoutput()
449+
print(output)
450+
assert "putting file newCustomFile" in output[0], "strange output"
451+
assert (ROOTPATH / "newCustomFile").exists(), "file doesn't exist"
452+
with (ROOTPATH / "newCustomFile").open("r") as fd:
453+
assert fd.read() == nicedata, "invalid data"
454+
except Exception:
455+
cli.printdebug()
456+
raise
457+
finally:
458+
cli.close()
459+
460+
= smbserver: SMB 3.1.1 - require share encryption
461+
462+
LOCALPATH = pathlib.Path(get_temp_dir())
463+
464+
nicedata = ("A" * 100 + "\n") * 5
465+
with open(LOCALPATH / "newCustomFile", "w") as fd:
466+
fd.write(nicedata)
467+
468+
with run_smbserver(readonly=False, encryptshare=True):
469+
try:
470+
cli = run_smbclient(user="Administrator", password="Password2", share="test", cwd=LOCALPATH)
471+
cli.cmd("put newCustomFile")
472+
output = cli.getoutput()
473+
print(output)
474+
assert "putting file newCustomFile" in output[0], "strange output"
475+
assert (ROOTPATH / "newCustomFile").exists(), "file doesn't exist"
476+
with (ROOTPATH / "newCustomFile").open("r") as fd:
477+
assert fd.read() == nicedata, "invalid data"
478+
except Exception:
479+
cli.printdebug()
480+
raise
481+
finally:
482+
cli.close()

0 commit comments

Comments
 (0)
Please sign in to comment.