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