1# SPDX-License-Identifier: GPL-2.0-only
2# This file is part of Scapy
3# See https://scapy.net/ for more information
4# Copyright (C) Gabriel Potter
5
6"""
7SMB 2 Server Automaton
8
9This provides a [MS-SMB2] server that can:
10- serve files
11- host a DCE/RPC server
12
13This is a Scapy Automaton that is supposedly easily extendable.
14
15.. note::
16 You will find more complete documentation for this layer over at
17 `SMB <https://scapy.readthedocs.io/en/latest/layers/smb.html#server>`_
18"""
19
20import hashlib
21import pathlib
22import socket
23import struct
24import time
25
26from scapy.arch import get_if_addr
27from scapy.automaton import ATMT, Automaton
28from scapy.config import conf
29from scapy.consts import WINDOWS
30from scapy.error import log_runtime, log_interactive
31from scapy.volatile import RandUUID
32
33from scapy.layers.dcerpc import (
34 DCERPC_Transport,
35 NDRUnion,
36 NDRPointer,
37)
38from scapy.layers.gssapi import (
39 GSS_S_COMPLETE,
40 GSS_S_CONTINUE_NEEDED,
41 GSS_S_CREDENTIALS_EXPIRED,
42)
43from scapy.layers.msrpce.rpcserver import DCERPC_Server
44from scapy.layers.ntlm import (
45 NTLMSSP,
46)
47from scapy.layers.smb import (
48 SMBNegotiate_Request,
49 SMBSession_Null,
50 SMBSession_Setup_AndX_Request,
51 SMBSession_Setup_AndX_Request_Extended_Security,
52 SMB_Header,
53)
54from scapy.layers.windows.security import SECURITY_DESCRIPTOR
55from scapy.layers.smb2 import (
56 DFS_REFERRAL_ENTRY1,
57 DFS_REFERRAL_V3,
58 DirectTCP,
59 FILE_BOTH_DIR_INFORMATION,
60 FILE_FULL_DIR_INFORMATION,
61 FILE_ID_BOTH_DIR_INFORMATION,
62 FILE_NAME_INFORMATION,
63 FileAllInformation,
64 FileAlternateNameInformation,
65 FileBasicInformation,
66 FileEaInformation,
67 FileFsAttributeInformation,
68 FileFsSizeInformation,
69 FileFsVolumeInformation,
70 FileIdBothDirectoryInformation,
71 FileInternalInformation,
72 FileNetworkOpenInformation,
73 FileStandardInformation,
74 FileStreamInformation,
75 NETWORK_INTERFACE_INFO,
76 SMB2_CREATE_DURABLE_HANDLE_RESPONSE_V2,
77 SMB2_CREATE_QUERY_MAXIMAL_ACCESS_RESPONSE,
78 SMB2_CREATE_QUERY_ON_DISK_ID,
79 SMB2_Cancel_Request,
80 SMB2_Change_Notify_Request,
81 SMB2_Change_Notify_Response,
82 SMB2_Close_Request,
83 SMB2_Close_Response,
84 SMB2_Create_Context,
85 SMB2_Create_Request,
86 SMB2_Create_Response,
87 SMB2_ENCRYPTION_CIPHERS,
88 SMB2_Echo_Request,
89 SMB2_Echo_Response,
90 SMB2_Encryption_Capabilities,
91 SMB2_Error_Response,
92 SMB2_FILEID,
93 SMB2_Header,
94 SMB2_IOCTL_Network_Interface_Info,
95 SMB2_IOCTL_RESP_GET_DFS_Referral,
96 SMB2_IOCTL_Request,
97 SMB2_IOCTL_Response,
98 SMB2_IOCTL_Validate_Negotiate_Info_Response,
99 SMB2_Negotiate_Context,
100 SMB2_Negotiate_Protocol_Request,
101 SMB2_Negotiate_Protocol_Response,
102 SMB2_Preauth_Integrity_Capabilities,
103 SMB2_Query_Directory_Request,
104 SMB2_Query_Directory_Response,
105 SMB2_Query_Info_Request,
106 SMB2_Query_Info_Response,
107 SMB2_Read_Request,
108 SMB2_Read_Response,
109 SMB2_SIGNING_ALGORITHMS,
110 SMB2_Session_Logoff_Request,
111 SMB2_Session_Logoff_Response,
112 SMB2_Session_Setup_Request,
113 SMB2_Session_Setup_Response,
114 SMB2_Set_Info_Request,
115 SMB2_Set_Info_Response,
116 SMB2_Signing_Capabilities,
117 SMB2_Tree_Connect_Request,
118 SMB2_Tree_Connect_Response,
119 SMB2_Tree_Disconnect_Request,
120 SMB2_Tree_Disconnect_Response,
121 SMB2_Write_Request,
122 SMB2_Write_Response,
123 SMBStreamSocket,
124 SOCKADDR_STORAGE,
125 SRVSVC_SHARE_TYPES,
126)
127from scapy.layers.spnego import SPNEGOSSP
128
129# Import DCE/RPC
130from scapy.layers.msrpce.raw.ms_srvs import (
131 LPSERVER_INFO_101,
132 LPSHARE_ENUM_STRUCT,
133 LPSHARE_INFO_1,
134 NetrServerGetInfo_Request,
135 NetrServerGetInfo_Response,
136 NetrShareEnum_Request,
137 NetrShareEnum_Response,
138 NetrShareGetInfo_Request,
139 NetrShareGetInfo_Response,
140 SHARE_INFO_1_CONTAINER,
141)
142from scapy.layers.msrpce.raw.ms_wkst import (
143 LPWKSTA_INFO_100,
144 NetrWkstaGetInfo_Request,
145 NetrWkstaGetInfo_Response,
146)
147
148
149class SMBShare:
150 """
151 A class used to define a share, used by SMB_Server
152
153 :param name: the share name
154 :param path: the path the the folder hosted by the share
155 :param type: (optional) share type per [MS-SRVS] sect 2.2.2.4
156 :param remark: (optional) a description of the share
157 :param encryptdata: (optional) whether encryption should be used for this
158 share. This only applies to SMB 3.1.1.
159 """
160
161 def __init__(self, name, path=".", type=None, remark="", encryptdata=False):
162 # Set the default type
163 if type is None:
164 type = 0 # DISKTREE
165 if name.endswith("$"):
166 type &= 0x80000000 # SPECIAL
167 # Lower case the name for resolution
168 self._name = name.lower()
169 # Resolve path
170 self.path = pathlib.Path(path).resolve()
171 # props
172 self.name = name
173 self.type = type
174 self.remark = remark
175 self.encryptdata = encryptdata
176
177 def __repr__(self):
178 type = SRVSVC_SHARE_TYPES[self.type & 0x0FFFFFFF]
179 if self.type & 0x80000000:
180 type = "SPECIAL+" + type
181 if self.type & 0x40000000:
182 type = "TEMPORARY+" + type
183 return "<SMBShare %s [%s]%s = %s>" % (
184 self.name,
185 type,
186 self.remark and (" '%s'" % self.remark) or "",
187 str(self.path),
188 )
189
190
191# The SMB Automaton
192
193
194class SMB_Server(Automaton):
195 """
196 SMB server automaton
197
198 :param shares: the shares to serve. By default, share nothing.
199 Note that IPC$ is appended.
200 :param ssp: the SSP to use
201
202 All other options (in caps) are optional, and SMB specific:
203
204 :param ANONYMOUS_LOGIN: mark the clients as anonymous
205 :param GUEST_LOGIN: mark the clients as guest
206 :param REQUIRE_SIGNATURE: set 'Require Signature'
207 :param REQUIRE_ENCRYPTION: globally require encryption.
208 You could also make it share-specific on 3.1.1.
209 :param MAX_DIALECT: maximum SMB dialect. Defaults to 0x0311 (3.1.1)
210 :param TREE_SHARE_FLAGS: flags to announce on Tree_Connect_Response
211 :param TREE_CAPABILITIES: capabilities to announce on Tree_Connect_Response
212 :param TREE_MAXIMAL_ACCESS: maximal access to announce on Tree_Connect_Response
213 :param FILE_MAXIMAL_ACCESS: maximal access to announce in MxAc Create Context
214 """
215
216 pkt_cls = DirectTCP
217 socketcls = SMBStreamSocket
218
219 def __init__(self, shares=[], ssp=None, verb=True, readonly=True, *args, **kwargs):
220 self.verb = verb
221 if "sock" not in kwargs:
222 raise ValueError(
223 "SMB_Server cannot be started directly ! Use SMB_Server.spawn"
224 )
225 # Various SMB server arguments
226 self.ANONYMOUS_LOGIN = kwargs.pop("ANONYMOUS_LOGIN", False)
227 self.GUEST_LOGIN = kwargs.pop("GUEST_LOGIN", None)
228 self.REQUIRE_SIGNATURE = kwargs.pop("REQUIRE_SIGNATURE", None)
229 self.REQUIRE_ENCRYPTION = kwargs.pop("REQUIRE_ENCRYPTION", False)
230 self.MAX_DIALECT = kwargs.pop("MAX_DIALECT", 0x0311)
231 self.TREE_SHARE_FLAGS = kwargs.pop(
232 "TREE_SHARE_FLAGS", "FORCE_LEVELII_OPLOCK+RESTRICT_EXCLUSIVE_OPENS"
233 )
234 self.TREE_CAPABILITIES = kwargs.pop("TREE_CAPABILITIES", 0)
235 self.TREE_MAXIMAL_ACCESS = kwargs.pop(
236 "TREE_MAXIMAL_ACCESS",
237 "+".join(
238 [
239 "FILE_READ_DATA",
240 "FILE_WRITE_DATA",
241 "FILE_APPEND_DATA",
242 "FILE_READ_EA",
243 "FILE_WRITE_EA",
244 "FILE_EXECUTE",
245 "FILE_DELETE_CHILD",
246 "FILE_READ_ATTRIBUTES",
247 "FILE_WRITE_ATTRIBUTES",
248 "DELETE",
249 "READ_CONTROL",
250 "WRITE_DAC",
251 "WRITE_OWNER",
252 "SYNCHRONIZE",
253 ]
254 ),
255 )
256 self.FILE_MAXIMAL_ACCESS = kwargs.pop(
257 # Read-only
258 "FILE_MAXIMAL_ACCESS",
259 "+".join(
260 [
261 "FILE_READ_DATA",
262 "FILE_READ_EA",
263 "FILE_EXECUTE",
264 "FILE_READ_ATTRIBUTES",
265 "READ_CONTROL",
266 "SYNCHRONIZE",
267 ]
268 ),
269 )
270 self.LOCAL_IPS = kwargs.pop(
271 "LOCAL_IPS", [get_if_addr(kwargs.get("iface", conf.iface) or conf.iface)]
272 )
273 self.DOMAIN_REFERRALS = kwargs.pop("DOMAIN_REFERRALS", [])
274 self.readonly = readonly
275 # We don't want to update the parent shares argument
276 self.shares = shares.copy()
277 # Append the IPC$ share
278 self.shares.append(
279 SMBShare(
280 name="IPC$",
281 type=0x80000003, # SPECIAL+IPC
282 remark="Remote IPC",
283 )
284 )
285 # Initialize the DCE/RPC server for SMB
286 self.rpc_server = SMB_DCERPC_Server(
287 DCERPC_Transport.NCACN_NP,
288 shares=self.shares,
289 verb=self.verb,
290 )
291 # Extend it if another DCE/RPC server is provided
292 if "DCERPC_SERVER_CLS" in kwargs:
293 self.rpc_server.extend(kwargs.pop("DCERPC_SERVER_CLS"))
294 # Internal Session information
295 self.NegotiateCapabilities = None
296 self.GUID = RandUUID()._fix()
297 self.NextForceSign = False
298 self.NextForceEncrypt = False
299
300 # Credits/Sequence handling
301 self.MaxCreditCount = 2048 # [MS-SMB2] note: windows max default
302 self.ClientCreditCount = 1
303 self.SequenceWindow = (0, 0)
304 self.SequenceNumbers = set([0]) # keep track of allowed MIDs
305
306 # Compounds are handled on receiving by the StreamSocket,
307 # and on aggregated in a CompoundQueue to be sent in one go
308 self.NextCompound = False
309 self.CompoundedHandle = None
310 # SSP provider
311 if ssp is None:
312 # No SSP => fallback on NTLM with guest
313 ssp = SPNEGOSSP(
314 [
315 NTLMSSP(
316 USE_MIC=False,
317 DO_NOT_CHECK_LOGIN=True,
318 ),
319 ]
320 )
321 if self.GUEST_LOGIN is None:
322 self.GUEST_LOGIN = True
323 # Initialize
324 Automaton.__init__(self, *args, **kwargs)
325 # Set session options
326 self.session.ssp = ssp
327 self.session.SigningRequired = (
328 self.REQUIRE_SIGNATURE if self.REQUIRE_SIGNATURE is not None else bool(ssp)
329 )
330
331 @property
332 def session(self):
333 # session shorthand
334 return self.sock.session
335
336 def vprint(self, s=""):
337 """
338 Verbose print (if enabled)
339 """
340 if self.verb:
341 if conf.interactive:
342 log_interactive.info("> %s", s)
343 else:
344 print("> %s" % s)
345
346 def send(self, pkt):
347 ForceSign, ForceEncrypt = self.NextForceSign, self.NextForceEncrypt
348 self.NextForceSign = self.NextForceEncrypt = False
349 return super(SMB_Server, self).send(
350 pkt,
351 Compounded=self.NextCompound,
352 ForceSign=ForceSign,
353 ForceEncrypt=ForceEncrypt,
354 )
355
356 @ATMT.state(initial=1)
357 def BEGIN(self):
358 self.authenticated = False
359
360 @ATMT.receive_condition(BEGIN)
361 def received_negotiate_smb1(self, pkt):
362 if SMBNegotiate_Request in pkt:
363 raise self.NEGOTIATED().action_parameters(pkt)
364
365 @ATMT.receive_condition(BEGIN)
366 def received_negotiate_smb2_begin(self, pkt):
367 if SMB2_Negotiate_Protocol_Request in pkt:
368 raise self.NEGOTIATED().action_parameters(pkt)
369
370 @ATMT.action(received_negotiate_smb2_begin)
371 def on_negotiate_smb2_begin(self, pkt):
372 self.on_negotiate(pkt)
373
374 @ATMT.action(received_negotiate_smb1)
375 def on_negotiate(self, pkt):
376 self.session.sspcontext, spnego_token = self.session.ssp.NegTokenInit2()
377
378 # Build SMB2 header
379 self.smb_header = DirectTCP() / SMB2_Header(
380 Flags="SMB2_FLAGS_SERVER_TO_REDIR",
381 CreditRequest=1,
382 CreditCharge=1,
383 )
384
385 if SMBNegotiate_Request in pkt:
386 # SMB1. We don't support anything else than a Multi-Protocol Negotiate.
387 # [MS-SMB2] 3.2.4.2.2.1 Multi-Protocol Negotiate
388
389 # Find a dialect matching SMB2, or error.
390 DialectIndexes = [
391 x.DialectString for x in pkt[SMBNegotiate_Request].Dialects
392 ]
393 for key, rev in [(b"SMB 2.???", 0x02FF), (b"SMB 2.002", 0x0202)]:
394 if key in DialectIndexes:
395 DialectRevision = rev
396 break
397 else:
398 # Error: client only supports SMB1. We must answer in SMB1.
399 resp = DirectTCP() / SMB_Header(
400 Command="SMB_COM_NEGOTIATE",
401 Status="STATUS_NOT_SUPPORTED",
402 Flags="REPLY+CASE_INSENSITIVE+CANONICALIZED_PATHS",
403 Flags2=(
404 "LONG_NAMES+EAS+NT_STATUS+SMB_SECURITY_SIGNATURE+"
405 "UNICODE+IS_LONG_NAME+EXTENDED_SECURITY"
406 ),
407 TID=pkt.TID,
408 MID=pkt.MID,
409 UID=pkt.UID,
410 PIDLow=pkt.PIDLow,
411 )
412 resp /= SMBSession_Null()
413 self.send(resp)
414 return
415
416 # Multi-Protocol Negotiate: allocate credit 1
417 self.SequenceWindow = (1, 1)
418 self.SequenceNumbers = set([1])
419
420 elif SMB2_Negotiate_Protocol_Request in pkt:
421 # SMB2
422 self.update_smbheader(pkt)
423
424 # Find common revisions
425 DialectRevisions = pkt[SMB2_Negotiate_Protocol_Request].Dialects
426 DialectRevisions = [x for x in DialectRevisions if x <= self.MAX_DIALECT]
427 DialectRevisions.sort(reverse=True)
428 if DialectRevisions:
429 DialectRevision = DialectRevisions[0]
430 if DialectRevision & 0xFF != 0xFF:
431 # Version isn't SMB X.???
432 self.session.Dialect = DialectRevision
433
434 # SecurityMode
435 if pkt.SecurityMode.SIGNING_REQUIRED:
436 self.session.SigningRequired = True
437
438 if DialectRevision is None:
439 # No common dialect found.
440 resp = self.smb_header.copy() / SMB2_Error_Response()
441 resp.Command = "SMB2_NEGOTIATE"
442 resp.Status = "STATUS_NOT_SUPPORTED"
443 self.send(resp)
444 return
445
446 # Capabilities: [MS-SMB2] 3.3.5.4
447 self.NegotiateCapabilities = "+".join(
448 [
449 "DFS",
450 "LEASING",
451 "LARGE_MTU",
452 ]
453 )
454 if DialectRevision >= 0x0300:
455 # "if Connection.Dialect belongs to the SMB 3.x dialect family,
456 # the server supports..."
457 self.NegotiateCapabilities += "+" + "+".join(
458 [
459 "MULTI_CHANNEL",
460 "PERSISTENT_HANDLES",
461 "DIRECTORY_LEASING",
462 "ENCRYPTION",
463 ]
464 )
465
466 # Build response
467 resp = self.smb_header.copy() / SMB2_Negotiate_Protocol_Response(
468 DialectRevision=DialectRevision,
469 SecurityMode=(
470 "SIGNING_ENABLED+SIGNING_REQUIRED"
471 if self.session.SigningRequired
472 else "SIGNING_ENABLED"
473 ),
474 ServerTime=(time.time() + 11644473600) * 1e7,
475 ServerStartTime=0,
476 MaxTransactionSize=65536,
477 MaxReadSize=65536,
478 MaxWriteSize=65536,
479 Capabilities=self.NegotiateCapabilities,
480 GUID=self.GUID,
481 )
482 # SMB >= 3.0.0
483 if DialectRevision >= 0x0300:
484 # [MS-SMB2] sect 3.3.5.3.1 note 253
485 resp.MaxTransactionSize = 0x800000
486 resp.MaxReadSize = 0x800000
487 resp.MaxWriteSize = 0x800000
488 # SMB 3.1.1
489 if DialectRevision >= 0x0311 and pkt.NegotiateContextsCount:
490 # Negotiate context-capabilities
491 for ngctx in pkt.NegotiateContexts:
492 if ngctx.ContextType == 0x0002:
493 # SMB2_ENCRYPTION_CAPABILITIES
494 for ciph in ngctx.Ciphers:
495 tciph = SMB2_ENCRYPTION_CIPHERS.get(ciph, None)
496 if tciph in self.session.SupportedCipherIds:
497 # Common !
498 self.session.CipherId = tciph
499 self.session.SupportsEncryption = True
500 break
501 elif ngctx.ContextType == 0x0008:
502 # SMB2_SIGNING_CAPABILITIES
503 for signalg in ngctx.SigningAlgorithms:
504 tsignalg = SMB2_SIGNING_ALGORITHMS.get(signalg, None)
505 if tsignalg in self.session.SupportedSigningAlgorithmIds:
506 # Common !
507 self.session.SigningAlgorithmId = tsignalg
508 break
509 # Send back the negotiated algorithms
510 resp.NegotiateContexts = [
511 # Preauth capabilities
512 SMB2_Negotiate_Context()
513 / SMB2_Preauth_Integrity_Capabilities(
514 # SHA-512 by default
515 HashAlgorithms=[self.session.PreauthIntegrityHashId],
516 Salt=self.session.Salt,
517 ),
518 # Encryption capabilities
519 SMB2_Negotiate_Context()
520 / SMB2_Encryption_Capabilities(
521 # AES-128-CCM by default
522 Ciphers=[self.session.CipherId],
523 ),
524 # Signing capabilities
525 SMB2_Negotiate_Context()
526 / SMB2_Signing_Capabilities(
527 # AES-128-CCM by default
528 SigningAlgorithms=[self.session.SigningAlgorithmId],
529 ),
530 ]
531
532 # Add security blob
533 resp.SecurityBlob = spnego_token
534
535 if SMB2_Negotiate_Protocol_Request in pkt:
536 # If required, compute sessions
537 self.session.computeSMBConnectionPreauth(
538 bytes(pkt[SMB2_Header]), # nego request
539 bytes(resp[SMB2_Header]), # nego response
540 )
541
542 self.send(resp)
543
544 @ATMT.state(error=1)
545 def NEGO_FAILED(self):
546 self.vprint("SMB Negotiate failed: encryption was not negotiated.")
547 self.end()
548
549 @ATMT.state()
550 def NEGOTIATED(self):
551 pass
552
553 @ATMT.state(error=1)
554 def INVALID_SEQUENCE_WINDOW(self, MID, CreditCharge):
555 self.vprint(
556 "SMB: Invalid sequence [%s:%s] in %s. (%s)"
557 % (
558 MID,
559 MID + CreditCharge - 1,
560 repr(self.SequenceWindow),
561 repr(self.SequenceNumbers),
562 )
563 )
564 self.end()
565
566 @ATMT.state(error=1)
567 def INSUFFICIENT_CREDITS(self):
568 self.vprint("SMB Negotiate failed: encryption was not negotiated.")
569 self.end()
570
571 def update_smbheader(self, pkt):
572 """
573 Called when receiving a SMB2 packet to update the current smb_header
574 """
575 # [MS-SMB2] sect 3.3.1.1 - Algorithm for Handling MIDs
576 MID = pkt.MID
577 if self.session.Dialect >= 0x0210:
578 # SMB 2.1.0+ - Credits
579 CreditCharge = pkt.CreditCharge
580 Consumed = range(MID, MID + CreditCharge)
581 if (
582 (
583 CreditCharge <= 0
584 and not isinstance(
585 pkt.payload.payload, SMB2_Negotiate_Protocol_Request
586 )
587 )
588 or CreditCharge >= self.MaxCreditCount
589 or not (self.SequenceWindow[0] <= MID <= self.SequenceWindow[1])
590 or not self.SequenceNumbers.issuperset(Consumed)
591 ):
592 raise self.INVALID_SEQUENCE_WINDOW(MID, CreditCharge)
593
594 # [MS-SMB2] sect 3.3.1.2 - Algorithm for the Granting of Credits
595 CreditGranted = min(
596 pkt.CreditRequest, self.MaxCreditCount - self.ClientCreditCount
597 )
598 if (
599 self.SequenceWindow[1] - self.SequenceWindow[0]
600 >= 2 * self.ClientCreditCount
601 ):
602 # "the server will not allow the difference between the smallest
603 # available sequence number and the largest available sequence number
604 # to exceed 2*[Credit Granted]"
605 CreditGranted = 0
606 self.smb_header.CreditRequest = CreditGranted
607 self.ClientCreditCount += CreditGranted
608 self.ClientCreditCount -= CreditCharge
609 if self.ClientCreditCount <= 0:
610 raise self.INSUFFICIENT_CREDITS()
611
612 else:
613 # SMB 2.0.2 - No credits
614 if not (self.SequenceWindow[0] <= MID <= self.SequenceWindow[1]):
615 raise self.INVALID_SEQUENCE_WINDOW()
616 CreditGranted = 1
617 Consumed = [MID]
618
619 # Update allowed sequence numbers (protect against replay)
620 self.SequenceNumbers -= set(Consumed)
621
622 # Store granted sequence numbers
623 self.SequenceNumbers |= set(
624 range(
625 self.SequenceWindow[1] + 1,
626 self.SequenceWindow[1] + 1 + CreditGranted,
627 )
628 )
629 # And update sequence window
630 self.SequenceWindow = (
631 min(self.SequenceNumbers),
632 self.SequenceWindow[1] + CreditGranted,
633 )
634
635 # [MS-SMB2] sect 3.3.4.1
636 # "the server SHOULD set the CreditCharge field in the SMB2 header
637 # of the response to the CreditCharge value in the SMB2 header of the request."
638 self.smb_header.CreditCharge = pkt.CreditCharge
639 # TODO: check that the announced CreditCharge is correct for the message.
640 # Right now we trust the client.
641
642 # If the packet has a NextCommand, set NextCompound to True
643 self.NextCompound = bool(pkt.NextCommand)
644 # [MS-SMB2] sect 3.3.4.1.1 - "If the request was signed by the client..."
645 # If the packet was signed, note we must answer with a signed packet.
646 if (
647 not self.session.SigningRequired
648 and pkt.SecuritySignature != b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"
649 ):
650 self.NextForceSign = True
651 # [MS-SMB2] sect 3.3.4.1.4 - "If the message being sent is any response to a
652 # client request for which Request.IsEncrypted is TRUE"
653 if pkt[SMB2_Header]._decrypted:
654 self.NextForceEncrypt = True
655
656 # [MS-SMB2] sect 3.3.5.2.7.2
657 # Add SMB2_FLAGS_RELATED_OPERATIONS to the response if present
658 if pkt.Flags.SMB2_FLAGS_RELATED_OPERATIONS:
659 self.smb_header.Flags += "SMB2_FLAGS_RELATED_OPERATIONS"
660 else:
661 self.smb_header.Flags -= "SMB2_FLAGS_RELATED_OPERATIONS"
662
663 # [MS-SMB2] sect 2.2.1.2 - Priority
664 if (self.session.Dialect or 0) >= 0x0311:
665 self.smb_header.Flags &= 0xFF8F
666 self.smb_header.Flags |= int(pkt.Flags) & 0x70
667
668 # Update IDs
669 self.smb_header.SessionId = pkt.SessionId
670 self.smb_header.TID = pkt.TID
671 self.smb_header.MID = pkt.MID
672 self.smb_header.PID = pkt.PID
673
674 @ATMT.receive_condition(NEGOTIATED)
675 def received_negotiate_smb2(self, pkt):
676 if SMB2_Negotiate_Protocol_Request in pkt:
677 raise self.NEGOTIATED().action_parameters(pkt)
678
679 @ATMT.action(received_negotiate_smb2)
680 def on_negotiate_smb2(self, pkt):
681 self.on_negotiate(pkt)
682
683 @ATMT.state(error=1)
684 def INVALID_SMB_VERSION(self):
685 log_runtime.warning("Tree request in SMB1: unimplemented. Quit")
686 self.end()
687
688 @ATMT.receive_condition(NEGOTIATED)
689 def receive_setup_andx_request(self, pkt):
690 if SMB2_Session_Setup_Request in pkt:
691 # SMB2
692 ssp_blob = pkt.SecurityBlob
693 raise self.RECEIVED_SETUP_ANDX_REQUEST().action_parameters(pkt, ssp_blob)
694 if (
695 SMBSession_Setup_AndX_Request_Extended_Security in pkt
696 or SMBSession_Setup_AndX_Request in pkt
697 ):
698 raise self.INVALID_SMB_VERSION()
699
700 @ATMT.state()
701 def RECEIVED_SETUP_ANDX_REQUEST(self):
702 pass
703
704 @ATMT.action(receive_setup_andx_request)
705 def on_setup_andx_request(self, pkt, ssp_blob):
706 self.session.sspcontext, tok, status = self.session.ssp.GSS_Accept_sec_context(
707 self.session.sspcontext,
708 ssp_blob,
709 )
710 self.update_smbheader(pkt)
711 self.smb_header.SessionId = 0x0001000000000015
712 if status not in [GSS_S_CONTINUE_NEEDED, GSS_S_COMPLETE]:
713 # Error
714 resp = self.smb_header.copy() / SMB2_Session_Setup_Response()
715 # Map some GSS return codes to NTStatus
716 if status == GSS_S_CREDENTIALS_EXPIRED:
717 resp.Status = "STATUS_PASSWORD_EXPIRED"
718 else:
719 resp.Status = "STATUS_LOGON_FAILURE"
720 # Reset Session preauth (SMB 3.1.1)
721 self.session.SessionPreauthIntegrityHashValue = None
722 else:
723 # Negotiation
724 resp = self.smb_header.copy() / SMB2_Session_Setup_Response()
725 if self.GUEST_LOGIN:
726 # "If the security subsystem indicates that the session
727 # was established by a guest user, Session.SigningRequired
728 # MUST be set to FALSE and Session.IsGuest MUST be set to TRUE."
729 resp.SessionFlags = "IS_GUEST"
730 self.session.IsGuest = True
731 self.session.SigningRequired = False
732 if self.ANONYMOUS_LOGIN:
733 resp.SessionFlags = "IS_NULL"
734 # [MS-SMB2] sect 3.3.5.5.3
735 if self.session.Dialect >= 0x0300 and self.REQUIRE_ENCRYPTION:
736 resp.SessionFlags += "ENCRYPT_DATA"
737 resp.Status = 0x0 if (status == GSS_S_COMPLETE) else 0xC0000016
738
739 # Set security blob (if any)
740 resp.SecurityBlob = tok
741
742 # We have a response. If required, compute sessions
743 if status == GSS_S_CONTINUE_NEEDED:
744 # the setup session response is used in hash
745 self.session.computeSMBSessionPreauth(
746 bytes(pkt[SMB2_Header]), # session setup request
747 bytes(resp[SMB2_Header]), # session setup response
748 )
749 else:
750 # the setup session response is not used in hash
751 self.session.computeSMBSessionPreauth(
752 bytes(pkt[SMB2_Header]), # session setup request
753 )
754 if status == GSS_S_COMPLETE:
755 # Authentication was successful
756 self.session.computeSMBSessionKeys(IsClient=False)
757 self.authenticated = True
758 # [MS-SMB2] Note: "Windows-based servers always sign the final session setup
759 # response when the user is neither anonymous nor guest."
760 # If not available, it will still be ignored.
761 self.NextForceSign = True
762 self.send(resp)
763 # Check whether we must enable encryption from now on
764 if (
765 self.authenticated
766 and not self.session.IsGuest
767 and self.session.Dialect >= 0x0300
768 and self.REQUIRE_ENCRYPTION
769 ):
770 # [MS-SMB2] sect 3.3.5.5.3: from now on, turn encryption on !
771 self.session.EncryptData = True
772 self.session.EncryptionRequired = True
773 self.session.SigningRequired = False
774
775 @ATMT.condition(RECEIVED_SETUP_ANDX_REQUEST)
776 def wait_for_next_request(self):
777 if self.authenticated:
778 self.vprint(
779 "User authenticated %s!" % (self.GUEST_LOGIN and " as guest" or "")
780 )
781 raise self.AUTHENTICATED()
782 else:
783 raise self.NEGOTIATED()
784
785 @ATMT.state()
786 def AUTHENTICATED(self):
787 """Dev: overload this"""
788 pass
789
790 # DEV: add a condition on AUTHENTICATED with prio=0
791
792 @ATMT.condition(AUTHENTICATED, prio=1)
793 def should_serve(self):
794 # Serve files
795 self.current_trees = {}
796 self.current_handles = {}
797 self.enumerate_index = {} # used for query directory enumeration
798 self.tree_id = 0
799 self.base_time_t = self.current_smb_time()
800 raise self.SERVING()
801
802 def _ioctl_error(self, Status="STATUS_NOT_SUPPORTED"):
803 pkt = self.smb_header.copy() / SMB2_Error_Response(ErrorData=b"\xff")
804 pkt.Status = Status
805 pkt.Command = "SMB2_IOCTL"
806 self.send(pkt)
807
808 @ATMT.state(final=1)
809 def END(self):
810 self.end()
811
812 # SERVE FILES
813
814 def current_tree(self):
815 """
816 Return the current tree name
817 """
818 return self.current_trees[self.smb_header.TID]
819
820 def root_path(self):
821 """
822 Return the root path of the current tree
823 """
824 curtree = self.current_tree()
825 try:
826 share_path = next(x.path for x in self.shares if x._name == curtree.lower())
827 except StopIteration:
828 return None
829 return pathlib.Path(share_path).resolve()
830
831 @ATMT.state()
832 def SERVING(self):
833 """
834 Main state when serving files
835 """
836 pass
837
838 @ATMT.receive_condition(SERVING)
839 def receive_logoff_request(self, pkt):
840 if SMB2_Session_Logoff_Request in pkt:
841 raise self.NEGOTIATED().action_parameters(pkt)
842
843 @ATMT.action(receive_logoff_request)
844 def send_logoff_response(self, pkt):
845 self.update_smbheader(pkt)
846 self.send(self.smb_header.copy() / SMB2_Session_Logoff_Response())
847
848 @ATMT.receive_condition(SERVING)
849 def receive_setup_andx_request_in_serving(self, pkt):
850 self.receive_setup_andx_request(pkt)
851
852 @ATMT.receive_condition(SERVING)
853 def receive_tree_connect(self, pkt):
854 if SMB2_Tree_Connect_Request in pkt:
855 tree_name = pkt[SMB2_Tree_Connect_Request].Path.split("\\")[-1]
856 raise self.SERVING().action_parameters(pkt, tree_name)
857
858 @ATMT.action(receive_tree_connect)
859 def send_tree_connect_response(self, pkt, tree_name):
860 self.update_smbheader(pkt)
861 # Check the tree name against the shares we're serving
862 try:
863 share = next(x for x in self.shares if x._name == tree_name.lower())
864 tree_name = share.name # normalize
865 except StopIteration:
866 # Unknown tree
867 resp = self.smb_header.copy() / SMB2_Error_Response()
868 resp.Command = "SMB2_TREE_CONNECT"
869 resp.Status = "STATUS_BAD_NETWORK_NAME"
870 self.send(resp)
871 return
872 # Add tree to current trees
873 if tree_name not in self.current_trees:
874 self.tree_id += 1
875 self.smb_header.TID = self.tree_id
876 self.current_trees[self.smb_header.TID] = tree_name
877
878 # Construct ShareFlags
879 ShareFlags = (
880 "AUTO_CACHING+NO_CACHING"
881 if self.current_tree() == "IPC$"
882 else self.TREE_SHARE_FLAGS
883 )
884 # [MS-SMB2] sect 3.3.5.7
885 if (
886 self.session.Dialect >= 0x0311
887 and not self.session.EncryptData
888 and share.encryptdata
889 ):
890 if not self.session.SupportsEncryption:
891 raise Exception("Peer asked for encryption but doesn't support it !")
892 ShareFlags += "+ENCRYPT_DATA"
893
894 self.vprint("Tree Connect on: %s" % tree_name)
895 self.send(
896 self.smb_header.copy()
897 / SMB2_Tree_Connect_Response(
898 ShareType="PIPE" if self.current_tree() == "IPC$" else "DISK",
899 ShareFlags=ShareFlags,
900 Capabilities=(
901 0 if self.current_tree() == "IPC$" else self.TREE_CAPABILITIES
902 ),
903 MaximalAccess=self.TREE_MAXIMAL_ACCESS,
904 )
905 )
906
907 @ATMT.receive_condition(SERVING)
908 def receive_ioctl(self, pkt):
909 if SMB2_IOCTL_Request in pkt:
910 raise self.SERVING().action_parameters(pkt)
911
912 @ATMT.action(receive_ioctl)
913 def send_ioctl_response(self, pkt):
914 self.update_smbheader(pkt)
915 if pkt.CtlCode == 0x11C017:
916 # FSCTL_PIPE_TRANSCEIVE
917 self.rpc_server.recv(pkt.Input.load)
918 self.send(
919 self.smb_header.copy()
920 / SMB2_IOCTL_Response(
921 CtlCode=0x11C017,
922 FileId=pkt[SMB2_IOCTL_Request].FileId,
923 Buffer=[("Output", self.rpc_server.get_response())],
924 )
925 )
926 elif pkt.CtlCode == 0x00140204 and self.session.sspcontext.SessionKey:
927 # FSCTL_VALIDATE_NEGOTIATE_INFO
928 # This is a security measure asking the server to validate
929 # what flags were negotiated during the SMBNegotiate exchange.
930 # This packet is ALWAYS signed, and expects a signed response.
931
932 # https://docs.microsoft.com/en-us/archive/blogs/openspecification/smb3-secure-dialect-negotiation
933 # > "Down-level servers (pre-Windows 2012) will return
934 # > STATUS_NOT_SUPPORTED or STATUS_INVALID_DEVICE_REQUEST
935 # > since they do not allow or implement
936 # > FSCTL_VALIDATE_NEGOTIATE_INFO.
937 # > The client should accept the
938 # > response provided it's properly signed".
939
940 if (self.session.Dialect or 0) < 0x0300:
941 # SMB < 3 isn't supposed to support FSCTL_VALIDATE_NEGOTIATE_INFO
942 self._ioctl_error(Status="STATUS_FILE_CLOSED")
943 return
944
945 # SMB3
946 self.send(
947 self.smb_header.copy()
948 / SMB2_IOCTL_Response(
949 CtlCode=0x00140204,
950 FileId=pkt[SMB2_IOCTL_Request].FileId,
951 Buffer=[
952 (
953 "Output",
954 SMB2_IOCTL_Validate_Negotiate_Info_Response(
955 GUID=self.GUID,
956 DialectRevision=self.session.Dialect,
957 SecurityMode=(
958 "SIGNING_ENABLED+SIGNING_REQUIRED"
959 if self.session.SigningRequired
960 else "SIGNING_ENABLED"
961 ),
962 Capabilities=self.NegotiateCapabilities,
963 ),
964 )
965 ],
966 )
967 )
968 elif pkt.CtlCode == 0x001401FC:
969 # FSCTL_QUERY_NETWORK_INTERFACE_INFO
970 self.send(
971 self.smb_header.copy()
972 / SMB2_IOCTL_Response(
973 CtlCode=0x001401FC,
974 FileId=pkt[SMB2_IOCTL_Request].FileId,
975 Output=SMB2_IOCTL_Network_Interface_Info(
976 interfaces=[
977 NETWORK_INTERFACE_INFO(
978 SockAddr_Storage=SOCKADDR_STORAGE(
979 Family=0x0002,
980 IPv4Adddress=x,
981 )
982 )
983 for x in self.LOCAL_IPS
984 ]
985 ),
986 )
987 )
988 elif pkt.CtlCode == 0x00060194:
989 # FSCTL_DFS_GET_REFERRALS
990 if (
991 self.DOMAIN_REFERRALS
992 and not pkt[SMB2_IOCTL_Request].Input.RequestFileName
993 ):
994 # Requesting domain referrals
995 self.send(
996 self.smb_header.copy()
997 / SMB2_IOCTL_Response(
998 CtlCode=0x00060194,
999 FileId=pkt[SMB2_IOCTL_Request].FileId,
1000 Output=SMB2_IOCTL_RESP_GET_DFS_Referral(
1001 ReferralEntries=[
1002 DFS_REFERRAL_V3(
1003 ReferralEntryFlags="NameListReferral",
1004 TimeToLive=600,
1005 )
1006 for _ in self.DOMAIN_REFERRALS
1007 ],
1008 ReferralBuffer=[
1009 DFS_REFERRAL_ENTRY1(SpecialName=name)
1010 for name in self.DOMAIN_REFERRALS
1011 ],
1012 ),
1013 )
1014 )
1015 return
1016 resp = self.smb_header.copy() / SMB2_Error_Response()
1017 resp.Command = "SMB2_IOCTL"
1018 resp.Status = "STATUS_FS_DRIVER_REQUIRED"
1019 self.send(resp)
1020 else:
1021 # Among other things, FSCTL_VALIDATE_NEGOTIATE_INFO
1022 self._ioctl_error(Status="STATUS_NOT_SUPPORTED")
1023
1024 @ATMT.receive_condition(SERVING)
1025 def receive_create_file(self, pkt):
1026 if SMB2_Create_Request in pkt:
1027 raise self.SERVING().action_parameters(pkt)
1028
1029 PIPES_TABLE = {
1030 "srvsvc": SMB2_FILEID(Persistent=0x4000000012, Volatile=0x4000000001),
1031 "wkssvc": SMB2_FILEID(Persistent=0x4000000013, Volatile=0x4000000002),
1032 "NETLOGON": SMB2_FILEID(Persistent=0x4000000014, Volatile=0x4000000003),
1033 }
1034
1035 # special handle in case of compounded requests ([MS-SMB2] 3.2.4.1.4)
1036 # that points to the chained opened file handle
1037 LAST_HANDLE = SMB2_FILEID(
1038 Persistent=0xFFFFFFFFFFFFFFFF, Volatile=0xFFFFFFFFFFFFFFFF
1039 )
1040
1041 def current_smb_time(self):
1042 return (
1043 FileNetworkOpenInformation().get_field("CreationTime").i2m(None, None)
1044 - 864000000000 # one day ago
1045 )
1046
1047 def make_file_id(self, fname):
1048 """
1049 Generate deterministic FileId based on the fname
1050 """
1051 hash = hashlib.md5((fname or "").encode()).digest()
1052 return 0x4000000000 | struct.unpack("<I", hash[:4])[0]
1053
1054 def lookup_file(self, fname, durable_handle=None, create=False, createOptions=None):
1055 """
1056 Lookup the file and build it's SMB2_FILEID
1057 """
1058 root = self.root_path()
1059 if isinstance(fname, pathlib.Path):
1060 path = fname
1061 fname = path.name
1062 else:
1063 path = root / (fname or "").replace("\\", "/")
1064 path = path.resolve()
1065 # Word of caution: this check ONLY works because root and path have been
1066 # resolve(). Be careful
1067 # Note: symbolic links are currently unsupported.
1068 if root not in path.parents and path != root:
1069 raise FileNotFoundError
1070 if WINDOWS and path.is_reserved():
1071 raise FileNotFoundError
1072 if not path.exists():
1073 if create and createOptions:
1074 if createOptions.FILE_DIRECTORY_FILE:
1075 # Folder creation
1076 path.mkdir()
1077 self.vprint("Created folder:" + fname)
1078 else:
1079 # File creation
1080 path.touch()
1081 self.vprint("Created file:" + fname)
1082 else:
1083 raise FileNotFoundError
1084 if durable_handle is None:
1085 handle = SMB2_FILEID(
1086 Persistent=self.make_file_id(fname) + self.smb_header.MID,
1087 )
1088 else:
1089 # We were given a durable handle. Use it
1090 handle = durable_handle
1091 attrs = {
1092 "CreationTime": self.base_time_t,
1093 "LastAccessTime": self.base_time_t,
1094 "LastWriteTime": self.base_time_t,
1095 "ChangeTime": self.base_time_t,
1096 "EndOfFile": 0,
1097 "AllocationSize": 0,
1098 }
1099 path_stat = path.stat()
1100 attrs["EndOfFile"] = attrs["AllocationSize"] = path_stat.st_size
1101 if fname is None:
1102 # special case
1103 attrs["FileAttributes"] = "+".join(
1104 [
1105 "FILE_ATTRIBUTE_HIDDEN",
1106 "FILE_ATTRIBUTE_SYSTEM",
1107 "FILE_ATTRIBUTE_DIRECTORY",
1108 ]
1109 )
1110 elif path.is_dir():
1111 attrs["FileAttributes"] = "FILE_ATTRIBUTE_DIRECTORY"
1112 else:
1113 attrs["FileAttributes"] = "FILE_ATTRIBUTE_ARCHIVE"
1114 self.current_handles[handle] = (
1115 path, # file path
1116 attrs, # file attributes
1117 )
1118 self.enumerate_index[handle] = 0
1119 return handle
1120
1121 def set_compounded_handle(self, handle):
1122 """
1123 Mark a handle as the current one being compounded.
1124 """
1125 self.CompoundedHandle = handle
1126
1127 def get_file_id(self, pkt):
1128 """
1129 Return the FileId attribute of pkt, accounting for compounded requests.
1130 """
1131 fid = pkt.FileId
1132 if fid == self.LAST_HANDLE:
1133 return self.CompoundedHandle
1134 return fid
1135
1136 def lookup_folder(self, handle, filter, offset, cls):
1137 """
1138 Lookup a folder handle
1139 """
1140 path = self.current_handles[handle][0]
1141 self.vprint("Query directory: " + str(path))
1142 self.current_handles[handle][1]["LastAccessTime"] = self.current_smb_time()
1143 if not path.is_dir():
1144 raise NotADirectoryError
1145 return sorted(
1146 [
1147 cls(FileName=x.name, **self.current_handles[self.lookup_file(x)][1])
1148 for x in path.glob(filter)
1149 # Note: symbolic links are unsupported because it's hard to check
1150 # for path traversal on them.
1151 if not x.is_symlink()
1152 ]
1153 + [
1154 cls(
1155 FileAttributes=("FILE_ATTRIBUTE_DIRECTORY"),
1156 FileName=".",
1157 )
1158 ]
1159 + (
1160 [
1161 cls(
1162 FileAttributes=("FILE_ATTRIBUTE_DIRECTORY"),
1163 FileName="..",
1164 )
1165 ]
1166 if path.resolve() != self.root_path()
1167 else []
1168 ),
1169 key=lambda x: x.FileName,
1170 )[offset:]
1171
1172 @ATMT.action(receive_create_file)
1173 def send_create_file_response(self, pkt):
1174 """
1175 Handle CreateFile request
1176
1177 See [MS-SMB2] 3.3.5.9 ()
1178 """
1179 self.update_smbheader(pkt)
1180 if pkt[SMB2_Create_Request].NameLen:
1181 fname = pkt[SMB2_Create_Request].Name
1182 else:
1183 fname = None
1184 if fname:
1185 self.vprint("Opened: " + fname)
1186 if self.current_tree() == "IPC$":
1187 # Special IPC$ case: opening a pipe
1188 FILE_ID = self.PIPES_TABLE.get(fname, None)
1189 if FILE_ID:
1190 attrs = {
1191 "CreationTime": 0,
1192 "LastAccessTime": 0,
1193 "LastWriteTime": 0,
1194 "ChangeTime": 0,
1195 "EndOfFile": 0,
1196 "AllocationSize": 4096,
1197 }
1198 self.current_handles[FILE_ID] = (
1199 fname,
1200 attrs,
1201 )
1202 self.send(
1203 self.smb_header.copy()
1204 / SMB2_Create_Response(
1205 OplockLevel=pkt.RequestedOplockLevel,
1206 FileId=FILE_ID,
1207 **attrs,
1208 )
1209 )
1210 else:
1211 # NOT_FOUND
1212 resp = self.smb_header.copy() / SMB2_Error_Response()
1213 resp.Command = "SMB2_CREATE"
1214 resp.Status = "STATUS_OBJECT_NAME_NOT_FOUND"
1215 self.send(resp)
1216 return
1217 else:
1218 # Check if there is a Durable Handle Reconnect Request
1219 durable_handle = None
1220 if pkt[SMB2_Create_Request].CreateContextsLen:
1221 try:
1222 durable_handle = next(
1223 x.Data.FileId
1224 for x in pkt[SMB2_Create_Request].CreateContexts
1225 if x.Name == b"DH2C"
1226 )
1227 except StopIteration:
1228 pass
1229 # Lookup file handle
1230 try:
1231 handle = self.lookup_file(fname, durable_handle=durable_handle)
1232 except FileNotFoundError:
1233 # NOT_FOUND
1234 if pkt[SMB2_Create_Request].CreateDisposition in [
1235 0x00000002, # FILE_CREATE
1236 0x00000005, # FILE_OVERWRITE_IF
1237 ]:
1238 if self.readonly:
1239 resp = self.smb_header.copy() / SMB2_Error_Response()
1240 resp.Command = "SMB2_CREATE"
1241 resp.Status = "STATUS_ACCESS_DENIED"
1242 self.send(resp)
1243 return
1244 else:
1245 # Create file
1246 handle = self.lookup_file(
1247 fname,
1248 durable_handle=durable_handle,
1249 create=True,
1250 createOptions=pkt[SMB2_Create_Request].CreateOptions,
1251 )
1252 else:
1253 resp = self.smb_header.copy() / SMB2_Error_Response()
1254 resp.Command = "SMB2_CREATE"
1255 resp.Status = "STATUS_OBJECT_NAME_NOT_FOUND"
1256 self.send(resp)
1257 return
1258 # Store compounded handle
1259 self.set_compounded_handle(handle)
1260 # Build response
1261 attrs = self.current_handles[handle][1]
1262 resp = self.smb_header.copy() / SMB2_Create_Response(
1263 OplockLevel=pkt.RequestedOplockLevel,
1264 FileId=handle,
1265 **attrs,
1266 )
1267 # Handle the various chain elements
1268 if pkt[SMB2_Create_Request].CreateContextsLen:
1269 CreateContexts = []
1270 # Note: failing to provide context elements when the client asks for
1271 # them will make the windows implementation fall into a weird
1272 # "the-server-is-dumb" mode. So provide them 'quoi qu'il en coûte'.
1273 for elt in pkt[SMB2_Create_Request].CreateContexts:
1274 if elt.Name == b"QFid":
1275 # [MS-SMB2] sect 3.3.5.9.9
1276 CreateContexts.append(
1277 SMB2_Create_Context(
1278 Name=b"QFid",
1279 Data=SMB2_CREATE_QUERY_ON_DISK_ID(
1280 DiskFileId=self.make_file_id(fname),
1281 VolumeId=0xBA39CD11,
1282 ),
1283 )
1284 )
1285 elif elt.Name == b"MxAc":
1286 # [MS-SMB2] sect 3.3.5.9.5
1287 CreateContexts.append(
1288 SMB2_Create_Context(
1289 Name=b"MxAc",
1290 Data=SMB2_CREATE_QUERY_MAXIMAL_ACCESS_RESPONSE(
1291 QueryStatus=0,
1292 MaximalAccess=self.FILE_MAXIMAL_ACCESS,
1293 ),
1294 )
1295 )
1296 elif elt.Name == b"DH2Q":
1297 # [MS-SMB2] sect 3.3.5.9.10
1298 if "FILE_ATTRIBUTE_DIRECTORY" in attrs["FileAttributes"]:
1299 continue
1300 CreateContexts.append(
1301 SMB2_Create_Context(
1302 Name=b"DH2Q",
1303 Data=SMB2_CREATE_DURABLE_HANDLE_RESPONSE_V2(
1304 Timeout=180000
1305 ),
1306 )
1307 )
1308 elif elt.Name == b"RqLs":
1309 # [MS-SMB2] sect 3.3.5.9.11
1310 # TODO: hmm, we are probably supposed to do something here
1311 CreateContexts.append(
1312 SMB2_Create_Context(
1313 Name=b"RqLs",
1314 Data=elt.Data,
1315 )
1316 )
1317 resp.CreateContexts = CreateContexts
1318 self.send(resp)
1319
1320 @ATMT.receive_condition(SERVING)
1321 def receive_change_notify_info(self, pkt):
1322 if SMB2_Change_Notify_Request in pkt:
1323 raise self.SERVING().action_parameters(pkt)
1324
1325 @ATMT.action(receive_change_notify_info)
1326 def send_change_notify_info_response(self, pkt):
1327 # [MS-SMB2] sect 3.3.5.19
1328 # "If the underlying object store does not support change notifications, the
1329 # server MUST fail this request with STATUS_NOT_SUPPORTED."
1330 self.update_smbheader(pkt)
1331 resp = self.smb_header.copy() / SMB2_Error_Response()
1332 resp.Command = "SMB2_CHANGE_NOTIFY"
1333 # ScapyFS doesn't support notifications
1334 resp.Status = "STATUS_NOT_SUPPORTED"
1335 self.send(resp)
1336
1337 @ATMT.receive_condition(SERVING)
1338 def receive_query_directory_info(self, pkt):
1339 if SMB2_Query_Directory_Request in pkt:
1340 raise self.SERVING().action_parameters(pkt)
1341
1342 @ATMT.action(receive_query_directory_info)
1343 def send_query_directory_response(self, pkt):
1344 self.update_smbheader(pkt)
1345 if not pkt.FileNameLen:
1346 # this is broken.
1347 return
1348 query = pkt.FileName
1349 fid = self.get_file_id(pkt)
1350 # Check for handled FileInformationClass
1351 # 0x02: FileFullDirectoryInformation
1352 # 0x03: FileBothDirectoryInformation
1353 # 0x25: FileIdBothDirectoryInformation
1354 if pkt.FileInformationClass not in [0x02, 0x03, 0x25]:
1355 # Unknown FileInformationClass
1356 resp = self.smb_header.copy() / SMB2_Error_Response()
1357 resp.Command = "SMB2_QUERY_DIRECTORY"
1358 resp.Status = "STATUS_INVALID_INFO_CLASS"
1359 self.send(resp)
1360 return
1361 # Handle SMB2_RESTART_SCANS
1362 if pkt[SMB2_Query_Directory_Request].Flags.SMB2_RESTART_SCANS:
1363 self.enumerate_index[fid] = 0
1364 # Lookup the files
1365 try:
1366 files = self.lookup_folder(
1367 fid,
1368 query,
1369 self.enumerate_index[fid],
1370 {
1371 0x02: FILE_FULL_DIR_INFORMATION,
1372 0x03: FILE_BOTH_DIR_INFORMATION,
1373 0x25: FILE_ID_BOTH_DIR_INFORMATION,
1374 }[pkt.FileInformationClass],
1375 )
1376 except NotADirectoryError:
1377 resp = self.smb_header.copy() / SMB2_Error_Response()
1378 resp.Command = "SMB2_QUERY_DIRECTORY"
1379 resp.Status = "STATUS_INVALID_PARAMETER"
1380 self.send(resp)
1381 return
1382 if not files:
1383 # No more files !
1384 self.enumerate_index[fid] = 0
1385 resp = self.smb_header.copy() / SMB2_Error_Response()
1386 resp.Command = "SMB2_QUERY_DIRECTORY"
1387 resp.Status = "STATUS_NO_MORE_FILES"
1388 self.send(resp)
1389 return
1390 # Handle SMB2_RETURN_SINGLE_ENTRY
1391 if pkt[SMB2_Query_Directory_Request].Flags.SMB2_RETURN_SINGLE_ENTRY:
1392 files = files[:1]
1393 # Increment index
1394 self.enumerate_index[fid] += len(files)
1395 # Build response based on the FileInformationClass
1396 fileinfo = FileIdBothDirectoryInformation(
1397 files=files,
1398 )
1399 self.send(
1400 self.smb_header.copy()
1401 / SMB2_Query_Directory_Response(Buffer=[("Output", fileinfo)])
1402 )
1403
1404 @ATMT.receive_condition(SERVING)
1405 def receive_query_info(self, pkt):
1406 if SMB2_Query_Info_Request in pkt:
1407 raise self.SERVING().action_parameters(pkt)
1408
1409 @ATMT.action(receive_query_info)
1410 def send_query_info_response(self, pkt):
1411 self.update_smbheader(pkt)
1412 # [MS-FSCC] + [MS-SMB2] sect 2.2.37 / 3.3.5.20.1
1413 fid = self.get_file_id(pkt)
1414 if pkt.InfoType == 0x01: # SMB2_0_INFO_FILE
1415 if pkt.FileInfoClass == 0x05: # FileStandardInformation
1416 attrs = self.current_handles[fid][1]
1417 fileinfo = FileStandardInformation(
1418 EndOfFile=attrs["EndOfFile"],
1419 AllocationSize=attrs["AllocationSize"],
1420 )
1421 elif pkt.FileInfoClass == 0x06: # FileInternalInformation
1422 pth = self.current_handles[fid][0]
1423 fileinfo = FileInternalInformation(
1424 IndexNumber=hash(pth) & 0xFFFFFFFFFFFFFFFF,
1425 )
1426 elif pkt.FileInfoClass == 0x07: # FileEaInformation
1427 fileinfo = FileEaInformation()
1428 elif pkt.FileInfoClass == 0x12: # FileAllInformation
1429 attrs = self.current_handles[fid][1]
1430 fileinfo = FileAllInformation(
1431 BasicInformation=FileBasicInformation(
1432 CreationTime=attrs["CreationTime"],
1433 LastAccessTime=attrs["LastAccessTime"],
1434 LastWriteTime=attrs["LastWriteTime"],
1435 ChangeTime=attrs["ChangeTime"],
1436 FileAttributes=attrs["FileAttributes"],
1437 ),
1438 StandardInformation=FileStandardInformation(
1439 EndOfFile=attrs["EndOfFile"],
1440 AllocationSize=attrs["AllocationSize"],
1441 ),
1442 )
1443 elif pkt.FileInfoClass == 0x15: # FileAlternateNameInformation
1444 pth = self.current_handles[fid][0]
1445 fileinfo = FileAlternateNameInformation(
1446 FileName=pth.name,
1447 )
1448 elif pkt.FileInfoClass == 0x16: # FileStreamInformation
1449 attrs = self.current_handles[fid][1]
1450 fileinfo = FileStreamInformation(
1451 StreamSize=attrs["EndOfFile"],
1452 StreamAllocationSize=attrs["AllocationSize"],
1453 )
1454 elif pkt.FileInfoClass == 0x22: # FileNetworkOpenInformation
1455 attrs = self.current_handles[fid][1]
1456 fileinfo = FileNetworkOpenInformation(
1457 **attrs,
1458 )
1459 elif pkt.FileInfoClass == 0x30: # FileNormalizedNameInformation
1460 pth = self.current_handles[fid][0]
1461 fileinfo = FILE_NAME_INFORMATION(
1462 FileName=pth.name,
1463 )
1464 else:
1465 log_runtime.warning(
1466 "Unimplemented: %s"
1467 % pkt[SMB2_Query_Info_Request].sprintf("%InfoType% %FileInfoClass%")
1468 )
1469 return
1470 elif pkt.InfoType == 0x02: # SMB2_0_INFO_FILESYSTEM
1471 # [MS-FSCC] sect 2.5
1472 if pkt.FileInfoClass == 0x01: # FileFsVolumeInformation
1473 fileinfo = FileFsVolumeInformation()
1474 elif pkt.FileInfoClass == 0x03: # FileFsSizeInformation
1475 fileinfo = FileFsSizeInformation()
1476 elif pkt.FileInfoClass == 0x05: # FileFsAttributeInformation
1477 fileinfo = FileFsAttributeInformation(
1478 FileSystemAttributes=0x88000F,
1479 )
1480 elif pkt.FileInfoClass == 0x07: # FileEaInformation
1481 fileinfo = FileEaInformation()
1482 else:
1483 log_runtime.warning(
1484 "Unimplemented: %s"
1485 % pkt[SMB2_Query_Info_Request].sprintf("%InfoType% %FileInfoClass%")
1486 )
1487 return
1488 elif pkt.InfoType == 0x03: # SMB2_0_INFO_SECURITY
1489 # [MS-FSCC] 2.4.6
1490 fileinfo = SECURITY_DESCRIPTOR()
1491 # TODO: fill it
1492 if pkt.AdditionalInformation.OWNER_SECURITY_INFORMATION:
1493 pass
1494 if pkt.AdditionalInformation.GROUP_SECURITY_INFORMATION:
1495 pass
1496 if pkt.AdditionalInformation.DACL_SECURITY_INFORMATION:
1497 pass
1498 if pkt.AdditionalInformation.SACL_SECURITY_INFORMATION:
1499 pass
1500 # Observed:
1501 if (
1502 pkt.AdditionalInformation.OWNER_SECURITY_INFORMATION
1503 or pkt.AdditionalInformation.SACL_SECURITY_INFORMATION
1504 or pkt.AdditionalInformation.GROUP_SECURITY_INFORMATION
1505 or pkt.AdditionalInformation.DACL_SECURITY_INFORMATION
1506 ):
1507 pkt = self.smb_header.copy() / SMB2_Error_Response(ErrorData=b"\xff")
1508 pkt.Status = "STATUS_ACCESS_DENIED"
1509 pkt.Command = "SMB2_QUERY_INFO"
1510 self.send(pkt)
1511 return
1512 if pkt.AdditionalInformation.ATTRIBUTE_SECURITY_INFORMATION:
1513 fileinfo.Control = 0x8800
1514 self.send(
1515 self.smb_header.copy()
1516 / SMB2_Query_Info_Response(Buffer=[("Output", fileinfo)])
1517 )
1518
1519 @ATMT.receive_condition(SERVING)
1520 def receive_set_info_request(self, pkt):
1521 if SMB2_Set_Info_Request in pkt:
1522 raise self.SERVING().action_parameters(pkt)
1523
1524 @ATMT.action(receive_set_info_request)
1525 def send_set_info_response(self, pkt):
1526 self.update_smbheader(pkt)
1527 self.send(self.smb_header.copy() / SMB2_Set_Info_Response())
1528
1529 @ATMT.receive_condition(SERVING)
1530 def receive_write_request(self, pkt):
1531 if SMB2_Write_Request in pkt:
1532 raise self.SERVING().action_parameters(pkt)
1533
1534 @ATMT.action(receive_write_request)
1535 def send_write_response(self, pkt):
1536 self.update_smbheader(pkt)
1537 resp = SMB2_Write_Response(Count=len(pkt.Data))
1538 fid = self.get_file_id(pkt)
1539 if self.current_tree() == "IPC$":
1540 if fid in self.PIPES_TABLE.values():
1541 # A pipe
1542 self.rpc_server.recv(pkt.Data)
1543 else:
1544 if self.readonly:
1545 # Read only !
1546 resp = SMB2_Error_Response()
1547 resp.Command = "SMB2_WRITE"
1548 resp.Status = "ERROR_FILE_READ_ONLY"
1549 else:
1550 # Write file
1551 pth, _ = self.current_handles[fid]
1552 length = pkt[SMB2_Write_Request].DataLen
1553 off = pkt[SMB2_Write_Request].Offset
1554 self.vprint("Writing %s bytes at %s" % (length, off))
1555 with open(pth, "r+b") as fd:
1556 fd.seek(off)
1557 resp.Count = fd.write(pkt[SMB2_Write_Request].Data)
1558 self.send(self.smb_header.copy() / resp)
1559
1560 @ATMT.receive_condition(SERVING)
1561 def receive_read_request(self, pkt):
1562 if SMB2_Read_Request in pkt:
1563 raise self.SERVING().action_parameters(pkt)
1564
1565 @ATMT.action(receive_read_request)
1566 def send_read_response(self, pkt):
1567 self.update_smbheader(pkt)
1568 resp = SMB2_Read_Response()
1569 fid = self.get_file_id(pkt)
1570 if self.current_tree() == "IPC$":
1571 # Read output from DCE/RPC server
1572 r = self.rpc_server.get_response()
1573 resp.Data = bytes(r)
1574 else:
1575 # Read file and send content
1576 pth, _ = self.current_handles[fid]
1577 length = pkt[SMB2_Read_Request].Length
1578 off = pkt[SMB2_Read_Request].Offset
1579 self.vprint("Reading %s bytes at %s" % (length, off))
1580 with open(pth, "rb") as fd:
1581 fd.seek(off)
1582 resp.Data = fd.read(length)
1583 self.send(self.smb_header.copy() / resp)
1584
1585 @ATMT.receive_condition(SERVING)
1586 def receive_close_request(self, pkt):
1587 if SMB2_Close_Request in pkt:
1588 raise self.SERVING().action_parameters(pkt)
1589
1590 @ATMT.action(receive_close_request)
1591 def send_close_response(self, pkt):
1592 self.update_smbheader(pkt)
1593 if self.current_tree() != "IPC$":
1594 fid = self.get_file_id(pkt)
1595 pth, attrs = self.current_handles[fid]
1596 if pth:
1597 self.vprint("Closed: " + str(pth))
1598 del self.current_handles[fid]
1599 del self.enumerate_index[fid]
1600 self.send(
1601 self.smb_header.copy()
1602 / SMB2_Close_Response(
1603 Flags=pkt[SMB2_Close_Request].Flags,
1604 **attrs,
1605 )
1606 )
1607 else:
1608 self.send(self.smb_header.copy() / SMB2_Close_Response())
1609
1610 @ATMT.receive_condition(SERVING)
1611 def receive_tree_disconnect_request(self, pkt):
1612 if SMB2_Tree_Disconnect_Request in pkt:
1613 raise self.SERVING().action_parameters(pkt)
1614
1615 @ATMT.action(receive_tree_disconnect_request)
1616 def send_tree_disconnect_response(self, pkt):
1617 self.update_smbheader(pkt)
1618 try:
1619 del self.current_trees[self.smb_header.TID] # clear tree
1620 resp = self.smb_header.copy() / SMB2_Tree_Disconnect_Response()
1621 except KeyError:
1622 resp = self.smb_header.copy() / SMB2_Error_Response()
1623 resp.Command = "SMB2_TREE_DISCONNECT"
1624 resp.Status = "STATUS_NETWORK_NAME_DELETED"
1625 self.send(resp)
1626
1627 @ATMT.receive_condition(SERVING)
1628 def receive_cancel_request(self, pkt):
1629 if SMB2_Cancel_Request in pkt:
1630 raise self.SERVING().action_parameters(pkt)
1631
1632 @ATMT.action(receive_cancel_request)
1633 def send_notify_cancel_response(self, pkt):
1634 self.update_smbheader(pkt)
1635 resp = self.smb_header.copy() / SMB2_Change_Notify_Response()
1636 resp.Status = "STATUS_CANCELLED"
1637 self.send(resp)
1638
1639 @ATMT.receive_condition(SERVING)
1640 def receive_echo_request(self, pkt):
1641 if SMB2_Echo_Request in pkt:
1642 raise self.SERVING().action_parameters(pkt)
1643
1644 @ATMT.action(receive_echo_request)
1645 def send_echo_reply(self, pkt):
1646 self.update_smbheader(pkt)
1647 self.send(self.smb_header.copy() / SMB2_Echo_Response())
1648
1649
1650# DCE/RPC server for SMB
1651
1652
1653class SMB_DCERPC_Server(DCERPC_Server):
1654 """
1655 DCE/RPC server than handles the minimum RPCs for SMB to work:
1656 """
1657
1658 def __init__(self, *args, **kwargs):
1659 self.shares = kwargs.pop("shares")
1660 super(SMB_DCERPC_Server, self).__init__(*args, **kwargs)
1661
1662 @DCERPC_Server.answer(NetrShareEnum_Request)
1663 def netr_share_enum(self, req):
1664 """
1665 NetrShareEnum [MS-SRVS]
1666 "retrieves information about each shared resource on a server."
1667 """
1668 level = req.InfoStruct.Level
1669
1670 # Create response
1671 resp = NetrShareEnum_Response(
1672 InfoStruct=LPSHARE_ENUM_STRUCT(
1673 Level=level,
1674 ShareInfo=NDRUnion(
1675 tag=level,
1676 value=None,
1677 ),
1678 ),
1679 ndr64=self.ndr64,
1680 )
1681
1682 if level == 1:
1683 nbEntries = len(self.shares)
1684 resp.InfoStruct.ShareInfo.value = NDRPointer(
1685 referent_id=0x20000,
1686 value=SHARE_INFO_1_CONTAINER(
1687 Buffer=[
1688 # Add shares
1689 LPSHARE_INFO_1(
1690 shi1_netname=x.name,
1691 shi1_type=x.type,
1692 shi1_remark=x.remark,
1693 )
1694 for x in self.shares
1695 ],
1696 EntriesRead=nbEntries,
1697 ),
1698 )
1699 resp.TotalEntries = nbEntries
1700 else:
1701 # We only support level 1 :(
1702 resp.status = 0x0000007C # ERROR_INVALID_LEVEL
1703
1704 return resp
1705
1706 @DCERPC_Server.answer(NetrWkstaGetInfo_Request)
1707 def netr_wksta_getinfo(self, req):
1708 """
1709 NetrWkstaGetInfo [MS-SRVS]
1710 "returns information about the configuration of a workstation."
1711 """
1712 level = req.Level
1713
1714 # Create response
1715 resp = NetrWkstaGetInfo_Response(
1716 WkstaInfo=NDRUnion(
1717 tag=level,
1718 value=None,
1719 ),
1720 ndr64=self.ndr64,
1721 )
1722
1723 if level == 100:
1724 resp.WkstaInfo.value = NDRPointer(
1725 referent_id=0x20000,
1726 value=LPWKSTA_INFO_100(
1727 wki100_platform_id=500, # NT
1728 wki100_ver_major=5,
1729 ),
1730 )
1731 else:
1732 # We only support level 101 :(
1733 resp.status = 0x0000007C # ERROR_INVALID_LEVEL
1734
1735 return resp
1736
1737 @DCERPC_Server.answer(NetrServerGetInfo_Request)
1738 def netr_server_getinfo(self, req):
1739 """
1740 NetrServerGetInfo [MS-WKST]
1741 "retrieves current configuration information for CIFS and
1742 SMB Version 1.0 servers."
1743 """
1744 level = req.Level
1745
1746 # Create response
1747 resp = NetrServerGetInfo_Response(
1748 InfoStruct=NDRUnion(
1749 tag=level,
1750 value=None,
1751 ),
1752 ndr64=self.ndr64,
1753 )
1754
1755 if level == 101:
1756 resp.InfoStruct.value = NDRPointer(
1757 referent_id=0x20000,
1758 value=LPSERVER_INFO_101(
1759 sv101_platform_id=500, # NT
1760 sv101_name="WORKSTATION",
1761 sv101_version_major=10,
1762 sv101_version_minor=0,
1763 sv101_version_type=1, # Workstation
1764 ),
1765 )
1766 else:
1767 # We only support level 101 :(
1768 resp.status = 0x0000007C # ERROR_INVALID_LEVEL
1769
1770 return resp
1771
1772 @DCERPC_Server.answer(NetrShareGetInfo_Request)
1773 def netr_share_getinfo(self, req):
1774 """
1775 NetrShareGetInfo [MS-SRVS]
1776 "retrieves information about a particular shared resource on a server."
1777 """
1778 level = req.Level
1779 share_netname = req.payload.payload.valueof("NetName").decode()
1780
1781 # Create response
1782 resp = NetrShareGetInfo_Response(
1783 InfoStruct=NDRUnion(
1784 tag=level,
1785 value=None,
1786 ),
1787 ndr64=self.ndr64,
1788 )
1789
1790 # Find the share the client is asking details for
1791 try:
1792 share = next(x for x in self.shares if x.name == share_netname)
1793 except StopIteration:
1794 # Share doesn't exist !
1795 resp.status = 0x00000906 # NERR_NetNameNotFound
1796 return resp
1797
1798 if level == 1:
1799 resp.InfoStruct.value = NDRPointer(
1800 referent_id=0x20000,
1801 value=LPSHARE_INFO_1(
1802 shi1_netname=share.name,
1803 shi1_type=share.type,
1804 shi1_remark=share.remark,
1805 ),
1806 )
1807 else:
1808 # We only support level 1 :(
1809 resp.status = 0x0000007C # ERROR_INVALID_LEVEL
1810
1811 return resp
1812
1813
1814# Util
1815
1816
1817class smbserver:
1818 r"""
1819 Spawns a simple smbserver
1820
1821 smbserver parameters:
1822
1823 :param shares: the list of shares to announce. Note that IPC$ is appended.
1824 By default, a 'Scapy' share on './'
1825 :param port: (optional) the port to bind on, default 445
1826 :param iface: (optional) the interface to bind on, default conf.iface
1827 :param readonly: (optional) whether the server is read-only or not. default True
1828 :param ssp: (optional) the SSP to use. See the examples below.
1829 Default NTLM with guest
1830
1831 Many more SMB-specific parameters are available in help(SMB_Server)
1832 """
1833
1834 def __init__(
1835 self,
1836 shares=None,
1837 iface: str = None,
1838 port: int = 445,
1839 verb: int = 2,
1840 readonly: bool = True,
1841 # SMB arguments
1842 ssp=None,
1843 **kwargs,
1844 ):
1845 # Default Share
1846 if shares is None:
1847 shares = [
1848 SMBShare(
1849 name="Scapy", path=".", remark="Scapy's SMB server default share"
1850 )
1851 ]
1852 # Verb
1853 if verb >= 2:
1854 log_runtime.info("-- Scapy %s SMB Server --" % conf.version)
1855 log_runtime.info(
1856 "SSP: %s. Read-Only: %s. Serving %s shares:"
1857 % (
1858 conf.color_theme.yellow(ssp or "NTLM (guest)"),
1859 (
1860 conf.color_theme.yellow("YES")
1861 if readonly
1862 else conf.color_theme.format("NO", "bg_red+white")
1863 ),
1864 conf.color_theme.red(len(shares)),
1865 )
1866 )
1867 for share in shares:
1868 log_runtime.info(" * %s" % share)
1869 # Start SMB Server
1870 self.srv = SMB_Server.spawn(
1871 # TCP server
1872 port=port,
1873 iface=iface or conf.loopback_name,
1874 verb=verb,
1875 # SMB server
1876 ssp=ssp,
1877 shares=shares,
1878 readonly=readonly,
1879 # SMB arguments
1880 **kwargs,
1881 )
1882
1883 def close(self):
1884 """
1885 Close the smbserver if started in background mode (bg=True)
1886 """
1887 if self.srv:
1888 try:
1889 self.srv.shutdown(socket.SHUT_RDWR)
1890 except OSError:
1891 pass
1892 self.srv.close()
1893
1894
1895if __name__ == "__main__":
1896 from scapy.utils import AutoArgparse
1897
1898 AutoArgparse(smbserver)