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, ssp_blob
666 )
667 self.update_smbheader(pkt)
668 if SMB2_Session_Setup_Request in pkt:
669 # SMB2
670 self.smb_header.SessionId = 0x0001000000000015
671 if status not in [GSS_S_CONTINUE_NEEDED, GSS_S_COMPLETE]:
672 # Error
673 if SMB2_Session_Setup_Request in pkt:
674 # SMB2
675 resp = self.smb_header.copy() / SMB2_Session_Setup_Response()
676 # Set security blob (if any)
677 resp.SecurityBlob = tok
678 else:
679 # SMB1
680 resp = self.smb_header.copy() / SMBSession_Null()
681 # Map some GSS return codes to NTStatus
682 if status == GSS_S_CREDENTIALS_EXPIRED:
683 resp.Status = "STATUS_PASSWORD_EXPIRED"
684 else:
685 resp.Status = "STATUS_LOGON_FAILURE"
686 # Reset Session preauth (SMB 3.1.1)
687 self.session.SessionPreauthIntegrityHashValue = None
688 else:
689 # Negotiation
690 if (
691 SMBSession_Setup_AndX_Request_Extended_Security in pkt
692 or SMB2_Session_Setup_Request in pkt
693 ):
694 # SMB1 extended / SMB2
695 if SMB2_Session_Setup_Request in pkt:
696 resp = self.smb_header.copy() / SMB2_Session_Setup_Response()
697 if self.GUEST_LOGIN:
698 # "If the security subsystem indicates that the session
699 # was established by a guest user, Session.SigningRequired
700 # MUST be set to FALSE and Session.IsGuest MUST be set to TRUE."
701 resp.SessionFlags = "IS_GUEST"
702 self.session.IsGuest = True
703 self.session.SigningRequired = False
704 if self.ANONYMOUS_LOGIN:
705 resp.SessionFlags = "IS_NULL"
706 # [MS-SMB2] sect 3.3.5.5.3
707 if self.session.Dialect >= 0x0300 and self.REQUIRE_ENCRYPTION:
708 resp.SessionFlags += "ENCRYPT_DATA"
709 else:
710 # SMB1 extended
711 resp = (
712 self.smb_header.copy()
713 / SMBSession_Setup_AndX_Response_Extended_Security(
714 NativeOS="Windows 4.0",
715 NativeLanMan="Windows 4.0",
716 )
717 )
718 if self.GUEST_LOGIN:
719 resp.Action = "SMB_SETUP_GUEST"
720 # Set security blob
721 resp.SecurityBlob = tok
722 elif SMBSession_Setup_AndX_Request in pkt:
723 # Non-extended
724 resp = self.smb_header.copy() / SMBSession_Setup_AndX_Response(
725 NativeOS="Windows 4.0",
726 NativeLanMan="Windows 4.0",
727 )
728 resp.Status = 0x0 if (status == GSS_S_COMPLETE) else 0xC0000016
729 # We have a response. If required, compute sessions
730 if status == GSS_S_CONTINUE_NEEDED:
731 # the setup session response is used in hash
732 self.session.computeSMBSessionPreauth(
733 bytes(pkt[SMB2_Header]), # session setup request
734 bytes(resp[SMB2_Header]), # session setup response
735 )
736 else:
737 # the setup session response is not used in hash
738 self.session.computeSMBSessionPreauth(
739 bytes(pkt[SMB2_Header]), # session setup request
740 )
741 if status == GSS_S_COMPLETE:
742 # Authentication was successful
743 self.session.computeSMBSessionKeys(IsClient=False)
744 self.authenticated = True
745 # [MS-SMB2] Note: "Windows-based servers always sign the final session setup
746 # response when the user is neither anonymous nor guest."
747 # If not available, it will still be ignored.
748 self.NextForceSign = True
749 self.send(resp)
750 # Check whether we must enable encryption from now on
751 if (
752 self.authenticated
753 and not self.session.IsGuest
754 and self.session.Dialect >= 0x0300
755 and self.REQUIRE_ENCRYPTION
756 ):
757 # [MS-SMB2] sect 3.3.5.5.3: from now on, turn encryption on !
758 self.session.EncryptData = True
759 self.session.SigningRequired = False
760
761 @ATMT.condition(RECEIVED_SETUP_ANDX_REQUEST)
762 def wait_for_next_request(self):
763 if self.authenticated:
764 self.vprint(
765 "User authenticated %s!" % (self.GUEST_LOGIN and " as guest" or "")
766 )
767 raise self.AUTHENTICATED()
768 else:
769 raise self.NEGOTIATED()
770
771 @ATMT.state()
772 def AUTHENTICATED(self):
773 """Dev: overload this"""
774 pass
775
776 # DEV: add a condition on AUTHENTICATED with prio=0
777
778 @ATMT.condition(AUTHENTICATED, prio=1)
779 def should_serve(self):
780 # Serve files
781 self.current_trees = {}
782 self.current_handles = {}
783 self.enumerate_index = {} # used for query directory enumeration
784 self.tree_id = 0
785 self.base_time_t = self.current_smb_time()
786 raise self.SERVING()
787
788 def _ioctl_error(self, Status="STATUS_NOT_SUPPORTED"):
789 pkt = self.smb_header.copy() / SMB2_Error_Response(ErrorData=b"\xff")
790 pkt.Status = Status
791 pkt.Command = "SMB2_IOCTL"
792 self.send(pkt)
793
794 @ATMT.state(final=1)
795 def END(self):
796 self.end()
797
798 # SERVE FILES
799
800 def current_tree(self):
801 """
802 Return the current tree name
803 """
804 return self.current_trees[self.smb_header.TID]
805
806 def root_path(self):
807 """
808 Return the root path of the current tree
809 """
810 curtree = self.current_tree()
811 try:
812 share_path = next(x.path for x in self.shares if x._name == curtree.lower())
813 except StopIteration:
814 return None
815 return pathlib.Path(share_path).resolve()
816
817 @ATMT.state()
818 def SERVING(self):
819 """
820 Main state when serving files
821 """
822 pass
823
824 @ATMT.receive_condition(SERVING)
825 def receive_logoff_request(self, pkt):
826 if SMB2_Session_Logoff_Request in pkt:
827 raise self.NEGOTIATED().action_parameters(pkt)
828
829 @ATMT.action(receive_logoff_request)
830 def send_logoff_response(self, pkt):
831 self.update_smbheader(pkt)
832 self.send(self.smb_header.copy() / SMB2_Session_Logoff_Response())
833
834 @ATMT.receive_condition(SERVING)
835 def receive_setup_andx_request_in_serving(self, pkt):
836 self.receive_setup_andx_request(pkt)
837
838 @ATMT.receive_condition(SERVING)
839 def is_smb1_tree(self, pkt):
840 if SMBTree_Connect_AndX in pkt:
841 # Unsupported
842 log_runtime.warning("Tree request in SMB1: unimplemented. Quit")
843 raise self.END()
844
845 @ATMT.receive_condition(SERVING)
846 def receive_tree_connect(self, pkt):
847 if SMB2_Tree_Connect_Request in pkt:
848 tree_name = pkt[SMB2_Tree_Connect_Request].Path.split("\\")[-1]
849 raise self.SERVING().action_parameters(pkt, tree_name)
850
851 @ATMT.action(receive_tree_connect)
852 def send_tree_connect_response(self, pkt, tree_name):
853 self.update_smbheader(pkt)
854 # Check the tree name against the shares we're serving
855 try:
856 share = next(x for x in self.shares if x._name == tree_name.lower())
857 except StopIteration:
858 # Unknown tree
859 resp = self.smb_header.copy() / SMB2_Error_Response()
860 resp.Command = "SMB2_TREE_CONNECT"
861 resp.Status = "STATUS_BAD_NETWORK_NAME"
862 self.send(resp)
863 return
864 # Add tree to current trees
865 if tree_name not in self.current_trees:
866 self.tree_id += 1
867 self.smb_header.TID = self.tree_id
868 self.current_trees[self.smb_header.TID] = tree_name
869
870 # Construct ShareFlags
871 ShareFlags = (
872 "AUTO_CACHING+NO_CACHING"
873 if self.current_tree() == "IPC$"
874 else self.TREE_SHARE_FLAGS
875 )
876 # [MS-SMB2] sect 3.3.5.7
877 if (
878 self.session.Dialect >= 0x0311
879 and not self.session.EncryptData
880 and share.encryptdata
881 ):
882 if not self.session.SupportsEncryption:
883 raise Exception("Peer asked for encryption but doesn't support it !")
884 ShareFlags += "+ENCRYPT_DATA"
885
886 self.vprint("Tree Connect on: %s" % tree_name)
887 self.send(
888 self.smb_header.copy()
889 / SMB2_Tree_Connect_Response(
890 ShareType="PIPE" if self.current_tree() == "IPC$" else "DISK",
891 ShareFlags=ShareFlags,
892 Capabilities=(
893 0 if self.current_tree() == "IPC$" else self.TREE_CAPABILITIES
894 ),
895 MaximalAccess=self.TREE_MAXIMAL_ACCESS,
896 )
897 )
898
899 @ATMT.receive_condition(SERVING)
900 def receive_ioctl(self, pkt):
901 if SMB2_IOCTL_Request in pkt:
902 raise self.SERVING().action_parameters(pkt)
903
904 @ATMT.action(receive_ioctl)
905 def send_ioctl_response(self, pkt):
906 self.update_smbheader(pkt)
907 if pkt.CtlCode == 0x11C017:
908 # FSCTL_PIPE_TRANSCEIVE
909 self.rpc_server.recv(pkt.Input.load)
910 self.send(
911 self.smb_header.copy()
912 / SMB2_IOCTL_Response(
913 CtlCode=0x11C017,
914 FileId=pkt[SMB2_IOCTL_Request].FileId,
915 Buffer=[("Output", self.rpc_server.get_response())],
916 )
917 )
918 elif pkt.CtlCode == 0x00140204 and self.session.sspcontext.SessionKey:
919 # FSCTL_VALIDATE_NEGOTIATE_INFO
920 # This is a security measure asking the server to validate
921 # what flags were negotiated during the SMBNegotiate exchange.
922 # This packet is ALWAYS signed, and expects a signed response.
923
924 # https://docs.microsoft.com/en-us/archive/blogs/openspecification/smb3-secure-dialect-negotiation
925 # > "Down-level servers (pre-Windows 2012) will return
926 # > STATUS_NOT_SUPPORTED or STATUS_INVALID_DEVICE_REQUEST
927 # > since they do not allow or implement
928 # > FSCTL_VALIDATE_NEGOTIATE_INFO.
929 # > The client should accept the
930 # > response provided it's properly signed".
931
932 if (self.session.Dialect or 0) < 0x0300:
933 # SMB < 3 isn't supposed to support FSCTL_VALIDATE_NEGOTIATE_INFO
934 self._ioctl_error(Status="STATUS_FILE_CLOSED")
935 return
936
937 # SMB3
938 self.send(
939 self.smb_header.copy()
940 / SMB2_IOCTL_Response(
941 CtlCode=0x00140204,
942 FileId=pkt[SMB2_IOCTL_Request].FileId,
943 Buffer=[
944 (
945 "Output",
946 SMB2_IOCTL_Validate_Negotiate_Info_Response(
947 GUID=self.GUID,
948 DialectRevision=self.session.Dialect,
949 SecurityMode=(
950 "SIGNING_ENABLED+SIGNING_REQUIRED"
951 if self.session.SigningRequired
952 else "SIGNING_ENABLED"
953 ),
954 Capabilities=self.NegotiateCapabilities,
955 ),
956 )
957 ],
958 )
959 )
960 elif pkt.CtlCode == 0x001401FC:
961 # FSCTL_QUERY_NETWORK_INTERFACE_INFO
962 self.send(
963 self.smb_header.copy()
964 / SMB2_IOCTL_Response(
965 CtlCode=0x001401FC,
966 FileId=pkt[SMB2_IOCTL_Request].FileId,
967 Output=SMB2_IOCTL_Network_Interface_Info(
968 interfaces=[
969 NETWORK_INTERFACE_INFO(
970 SockAddr_Storage=SOCKADDR_STORAGE(
971 Family=0x0002,
972 IPv4Adddress=x,
973 )
974 )
975 for x in self.LOCAL_IPS
976 ]
977 ),
978 )
979 )
980 elif pkt.CtlCode == 0x00060194:
981 # FSCTL_DFS_GET_REFERRALS
982 if (
983 self.DOMAIN_REFERRALS
984 and not pkt[SMB2_IOCTL_Request].Input.RequestFileName
985 ):
986 # Requesting domain referrals
987 self.send(
988 self.smb_header.copy()
989 / SMB2_IOCTL_Response(
990 CtlCode=0x00060194,
991 FileId=pkt[SMB2_IOCTL_Request].FileId,
992 Output=SMB2_IOCTL_RESP_GET_DFS_Referral(
993 ReferralEntries=[
994 DFS_REFERRAL_V3(
995 ReferralEntryFlags="NameListReferral",
996 TimeToLive=600,
997 )
998 for _ in self.DOMAIN_REFERRALS
999 ],
1000 ReferralBuffer=[
1001 DFS_REFERRAL_ENTRY1(SpecialName=name)
1002 for name in self.DOMAIN_REFERRALS
1003 ],
1004 ),
1005 )
1006 )
1007 return
1008 resp = self.smb_header.copy() / SMB2_Error_Response()
1009 resp.Command = "SMB2_IOCTL"
1010 resp.Status = "STATUS_FS_DRIVER_REQUIRED"
1011 self.send(resp)
1012 else:
1013 # Among other things, FSCTL_VALIDATE_NEGOTIATE_INFO
1014 self._ioctl_error(Status="STATUS_NOT_SUPPORTED")
1015
1016 @ATMT.receive_condition(SERVING)
1017 def receive_create_file(self, pkt):
1018 if SMB2_Create_Request in pkt:
1019 raise self.SERVING().action_parameters(pkt)
1020
1021 PIPES_TABLE = {
1022 "srvsvc": SMB2_FILEID(Persistent=0x4000000012, Volatile=0x4000000001),
1023 "wkssvc": SMB2_FILEID(Persistent=0x4000000013, Volatile=0x4000000002),
1024 "NETLOGON": SMB2_FILEID(Persistent=0x4000000014, Volatile=0x4000000003),
1025 }
1026
1027 # special handle in case of compounded requests ([MS-SMB2] 3.2.4.1.4)
1028 # that points to the chained opened file handle
1029 LAST_HANDLE = SMB2_FILEID(
1030 Persistent=0xFFFFFFFFFFFFFFFF, Volatile=0xFFFFFFFFFFFFFFFF
1031 )
1032
1033 def current_smb_time(self):
1034 return (
1035 FileNetworkOpenInformation().get_field("CreationTime").i2m(None, None)
1036 - 864000000000 # one day ago
1037 )
1038
1039 def make_file_id(self, fname):
1040 """
1041 Generate deterministic FileId based on the fname
1042 """
1043 hash = hashlib.md5((fname or "").encode()).digest()
1044 return 0x4000000000 | struct.unpack("<I", hash[:4])[0]
1045
1046 def lookup_file(self, fname, durable_handle=None, create=False, createOptions=None):
1047 """
1048 Lookup the file and build it's SMB2_FILEID
1049 """
1050 root = self.root_path()
1051 if isinstance(fname, pathlib.Path):
1052 path = fname
1053 fname = path.name
1054 else:
1055 path = root / (fname or "").replace("\\", "/")
1056 path = path.resolve()
1057 # Word of caution: this check ONLY works because root and path have been
1058 # resolve(). Be careful
1059 # Note: symbolic links are currently unsupported.
1060 if root not in path.parents and path != root:
1061 raise FileNotFoundError
1062 if WINDOWS and path.is_reserved():
1063 raise FileNotFoundError
1064 if not path.exists():
1065 if create and createOptions:
1066 if createOptions.FILE_DIRECTORY_FILE:
1067 # Folder creation
1068 path.mkdir()
1069 self.vprint("Created folder:" + fname)
1070 else:
1071 # File creation
1072 path.touch()
1073 self.vprint("Created file:" + fname)
1074 else:
1075 raise FileNotFoundError
1076 if durable_handle is None:
1077 handle = SMB2_FILEID(
1078 Persistent=self.make_file_id(fname) + self.smb_header.MID,
1079 )
1080 else:
1081 # We were given a durable handle. Use it
1082 handle = durable_handle
1083 attrs = {
1084 "CreationTime": self.base_time_t,
1085 "LastAccessTime": self.base_time_t,
1086 "LastWriteTime": self.base_time_t,
1087 "ChangeTime": self.base_time_t,
1088 "EndOfFile": 0,
1089 "AllocationSize": 0,
1090 }
1091 path_stat = path.stat()
1092 attrs["EndOfFile"] = attrs["AllocationSize"] = path_stat.st_size
1093 if fname is None:
1094 # special case
1095 attrs["FileAttributes"] = "+".join(
1096 [
1097 "FILE_ATTRIBUTE_HIDDEN",
1098 "FILE_ATTRIBUTE_SYSTEM",
1099 "FILE_ATTRIBUTE_DIRECTORY",
1100 ]
1101 )
1102 elif path.is_dir():
1103 attrs["FileAttributes"] = "FILE_ATTRIBUTE_DIRECTORY"
1104 else:
1105 attrs["FileAttributes"] = "FILE_ATTRIBUTE_ARCHIVE"
1106 self.current_handles[handle] = (
1107 path, # file path
1108 attrs, # file attributes
1109 )
1110 self.enumerate_index[handle] = 0
1111 return handle
1112
1113 def set_compounded_handle(self, handle):
1114 """
1115 Mark a handle as the current one being compounded.
1116 """
1117 self.CompoundedHandle = handle
1118
1119 def get_file_id(self, pkt):
1120 """
1121 Return the FileId attribute of pkt, accounting for compounded requests.
1122 """
1123 fid = pkt.FileId
1124 if fid == self.LAST_HANDLE:
1125 return self.CompoundedHandle
1126 return fid
1127
1128 def lookup_folder(self, handle, filter, offset, cls):
1129 """
1130 Lookup a folder handle
1131 """
1132 path = self.current_handles[handle][0]
1133 self.vprint("Query directory: " + str(path))
1134 self.current_handles[handle][1]["LastAccessTime"] = self.current_smb_time()
1135 if not path.is_dir():
1136 raise NotADirectoryError
1137 return sorted(
1138 [
1139 cls(FileName=x.name, **self.current_handles[self.lookup_file(x)][1])
1140 for x in path.glob(filter)
1141 # Note: symbolic links are unsupported because it's hard to check
1142 # for path traversal on them.
1143 if not x.is_symlink()
1144 ]
1145 + [
1146 cls(
1147 FileAttributes=("FILE_ATTRIBUTE_DIRECTORY"),
1148 FileName=".",
1149 )
1150 ]
1151 + (
1152 [
1153 cls(
1154 FileAttributes=("FILE_ATTRIBUTE_DIRECTORY"),
1155 FileName="..",
1156 )
1157 ]
1158 if path.resolve() != self.root_path()
1159 else []
1160 ),
1161 key=lambda x: x.FileName,
1162 )[offset:]
1163
1164 @ATMT.action(receive_create_file)
1165 def send_create_file_response(self, pkt):
1166 """
1167 Handle CreateFile request
1168
1169 See [MS-SMB2] 3.3.5.9 ()
1170 """
1171 self.update_smbheader(pkt)
1172 if pkt[SMB2_Create_Request].NameLen:
1173 fname = pkt[SMB2_Create_Request].Name
1174 else:
1175 fname = None
1176 if fname:
1177 self.vprint("Opened: " + fname)
1178 if self.current_tree() == "IPC$":
1179 # Special IPC$ case: opening a pipe
1180 FILE_ID = self.PIPES_TABLE.get(fname, None)
1181 if FILE_ID:
1182 attrs = {
1183 "CreationTime": 0,
1184 "LastAccessTime": 0,
1185 "LastWriteTime": 0,
1186 "ChangeTime": 0,
1187 "EndOfFile": 0,
1188 "AllocationSize": 4096,
1189 }
1190 self.current_handles[FILE_ID] = (
1191 fname,
1192 attrs,
1193 )
1194 self.send(
1195 self.smb_header.copy()
1196 / SMB2_Create_Response(
1197 OplockLevel=pkt.RequestedOplockLevel,
1198 FileId=FILE_ID,
1199 **attrs,
1200 )
1201 )
1202 else:
1203 # NOT_FOUND
1204 resp = self.smb_header.copy() / SMB2_Error_Response()
1205 resp.Command = "SMB2_CREATE"
1206 resp.Status = "STATUS_OBJECT_NAME_NOT_FOUND"
1207 self.send(resp)
1208 return
1209 else:
1210 # Check if there is a Durable Handle Reconnect Request
1211 durable_handle = None
1212 if pkt[SMB2_Create_Request].CreateContextsLen:
1213 try:
1214 durable_handle = next(
1215 x.Data.FileId
1216 for x in pkt[SMB2_Create_Request].CreateContexts
1217 if x.Name == b"DH2C"
1218 )
1219 except StopIteration:
1220 pass
1221 # Lookup file handle
1222 try:
1223 handle = self.lookup_file(fname, durable_handle=durable_handle)
1224 except FileNotFoundError:
1225 # NOT_FOUND
1226 if pkt[SMB2_Create_Request].CreateDisposition in [
1227 0x00000002, # FILE_CREATE
1228 0x00000005, # FILE_OVERWRITE_IF
1229 ]:
1230 if self.readonly:
1231 resp = self.smb_header.copy() / SMB2_Error_Response()
1232 resp.Command = "SMB2_CREATE"
1233 resp.Status = "STATUS_ACCESS_DENIED"
1234 self.send(resp)
1235 return
1236 else:
1237 # Create file
1238 handle = self.lookup_file(
1239 fname,
1240 durable_handle=durable_handle,
1241 create=True,
1242 createOptions=pkt[SMB2_Create_Request].CreateOptions,
1243 )
1244 else:
1245 resp = self.smb_header.copy() / SMB2_Error_Response()
1246 resp.Command = "SMB2_CREATE"
1247 resp.Status = "STATUS_OBJECT_NAME_NOT_FOUND"
1248 self.send(resp)
1249 return
1250 # Store compounded handle
1251 self.set_compounded_handle(handle)
1252 # Build response
1253 attrs = self.current_handles[handle][1]
1254 resp = self.smb_header.copy() / SMB2_Create_Response(
1255 OplockLevel=pkt.RequestedOplockLevel,
1256 FileId=handle,
1257 **attrs,
1258 )
1259 # Handle the various chain elements
1260 if pkt[SMB2_Create_Request].CreateContextsLen:
1261 CreateContexts = []
1262 # Note: failing to provide context elements when the client asks for
1263 # them will make the windows implementation fall into a weird
1264 # "the-server-is-dumb" mode. So provide them 'quoi qu'il en coûte'.
1265 for elt in pkt[SMB2_Create_Request].CreateContexts:
1266 if elt.Name == b"QFid":
1267 # [MS-SMB2] sect 3.3.5.9.9
1268 CreateContexts.append(
1269 SMB2_Create_Context(
1270 Name=b"QFid",
1271 Data=SMB2_CREATE_QUERY_ON_DISK_ID(
1272 DiskFileId=self.make_file_id(fname),
1273 VolumeId=0xBA39CD11,
1274 ),
1275 )
1276 )
1277 elif elt.Name == b"MxAc":
1278 # [MS-SMB2] sect 3.3.5.9.5
1279 CreateContexts.append(
1280 SMB2_Create_Context(
1281 Name=b"MxAc",
1282 Data=SMB2_CREATE_QUERY_MAXIMAL_ACCESS_RESPONSE(
1283 QueryStatus=0,
1284 MaximalAccess=self.FILE_MAXIMAL_ACCESS,
1285 ),
1286 )
1287 )
1288 elif elt.Name == b"DH2Q":
1289 # [MS-SMB2] sect 3.3.5.9.10
1290 if "FILE_ATTRIBUTE_DIRECTORY" in attrs["FileAttributes"]:
1291 continue
1292 CreateContexts.append(
1293 SMB2_Create_Context(
1294 Name=b"DH2Q",
1295 Data=SMB2_CREATE_DURABLE_HANDLE_RESPONSE_V2(
1296 Timeout=180000
1297 ),
1298 )
1299 )
1300 elif elt.Name == b"RqLs":
1301 # [MS-SMB2] sect 3.3.5.9.11
1302 # TODO: hmm, we are probably supposed to do something here
1303 CreateContexts.append(
1304 SMB2_Create_Context(
1305 Name=b"RqLs",
1306 Data=elt.Data,
1307 )
1308 )
1309 resp.CreateContexts = CreateContexts
1310 self.send(resp)
1311
1312 @ATMT.receive_condition(SERVING)
1313 def receive_change_notify_info(self, pkt):
1314 if SMB2_Change_Notify_Request in pkt:
1315 raise self.SERVING().action_parameters(pkt)
1316
1317 @ATMT.action(receive_change_notify_info)
1318 def send_change_notify_info_response(self, pkt):
1319 # [MS-SMB2] sect 3.3.5.19
1320 # "If the underlying object store does not support change notifications, the
1321 # server MUST fail this request with STATUS_NOT_SUPPORTED."
1322 self.update_smbheader(pkt)
1323 resp = self.smb_header.copy() / SMB2_Error_Response()
1324 resp.Command = "SMB2_CHANGE_NOTIFY"
1325 # ScapyFS doesn't support notifications
1326 resp.Status = "STATUS_NOT_SUPPORTED"
1327 self.send(resp)
1328
1329 @ATMT.receive_condition(SERVING)
1330 def receive_query_directory_info(self, pkt):
1331 if SMB2_Query_Directory_Request in pkt:
1332 raise self.SERVING().action_parameters(pkt)
1333
1334 @ATMT.action(receive_query_directory_info)
1335 def send_query_directory_response(self, pkt):
1336 self.update_smbheader(pkt)
1337 if not pkt.FileNameLen:
1338 # this is broken.
1339 return
1340 query = pkt.FileName
1341 fid = self.get_file_id(pkt)
1342 # Check for handled FileInformationClass
1343 # 0x02: FileFullDirectoryInformation
1344 # 0x03: FileBothDirectoryInformation
1345 # 0x25: FileIdBothDirectoryInformation
1346 if pkt.FileInformationClass not in [0x02, 0x03, 0x25]:
1347 # Unknown FileInformationClass
1348 resp = self.smb_header.copy() / SMB2_Error_Response()
1349 resp.Command = "SMB2_QUERY_DIRECTORY"
1350 resp.Status = "STATUS_INVALID_INFO_CLASS"
1351 self.send(resp)
1352 return
1353 # Handle SMB2_RESTART_SCANS
1354 if pkt[SMB2_Query_Directory_Request].Flags.SMB2_RESTART_SCANS:
1355 self.enumerate_index[fid] = 0
1356 # Lookup the files
1357 try:
1358 files = self.lookup_folder(
1359 fid,
1360 query,
1361 self.enumerate_index[fid],
1362 {
1363 0x02: FILE_FULL_DIR_INFORMATION,
1364 0x03: FILE_BOTH_DIR_INFORMATION,
1365 0x25: FILE_ID_BOTH_DIR_INFORMATION,
1366 }[pkt.FileInformationClass],
1367 )
1368 except NotADirectoryError:
1369 resp = self.smb_header.copy() / SMB2_Error_Response()
1370 resp.Command = "SMB2_QUERY_DIRECTORY"
1371 resp.Status = "STATUS_INVALID_PARAMETER"
1372 self.send(resp)
1373 return
1374 if not files:
1375 # No more files !
1376 self.enumerate_index[fid] = 0
1377 resp = self.smb_header.copy() / SMB2_Error_Response()
1378 resp.Command = "SMB2_QUERY_DIRECTORY"
1379 resp.Status = "STATUS_NO_MORE_FILES"
1380 self.send(resp)
1381 return
1382 # Handle SMB2_RETURN_SINGLE_ENTRY
1383 if pkt[SMB2_Query_Directory_Request].Flags.SMB2_RETURN_SINGLE_ENTRY:
1384 files = files[:1]
1385 # Increment index
1386 self.enumerate_index[fid] += len(files)
1387 # Build response based on the FileInformationClass
1388 fileinfo = FileIdBothDirectoryInformation(
1389 files=files,
1390 )
1391 self.send(
1392 self.smb_header.copy()
1393 / SMB2_Query_Directory_Response(Buffer=[("Output", fileinfo)])
1394 )
1395
1396 @ATMT.receive_condition(SERVING)
1397 def receive_query_info(self, pkt):
1398 if SMB2_Query_Info_Request in pkt:
1399 raise self.SERVING().action_parameters(pkt)
1400
1401 @ATMT.action(receive_query_info)
1402 def send_query_info_response(self, pkt):
1403 self.update_smbheader(pkt)
1404 # [MS-FSCC] + [MS-SMB2] sect 2.2.37 / 3.3.5.20.1
1405 fid = self.get_file_id(pkt)
1406 if pkt.InfoType == 0x01: # SMB2_0_INFO_FILE
1407 if pkt.FileInfoClass == 0x05: # FileStandardInformation
1408 attrs = self.current_handles[fid][1]
1409 fileinfo = FileStandardInformation(
1410 EndOfFile=attrs["EndOfFile"],
1411 AllocationSize=attrs["AllocationSize"],
1412 )
1413 elif pkt.FileInfoClass == 0x06: # FileInternalInformation
1414 pth = self.current_handles[fid][0]
1415 fileinfo = FileInternalInformation(
1416 IndexNumber=hash(pth) & 0xFFFFFFFFFFFFFFFF,
1417 )
1418 elif pkt.FileInfoClass == 0x07: # FileEaInformation
1419 fileinfo = FileEaInformation()
1420 elif pkt.FileInfoClass == 0x12: # FileAllInformation
1421 attrs = self.current_handles[fid][1]
1422 fileinfo = FileAllInformation(
1423 BasicInformation=FileBasicInformation(
1424 CreationTime=attrs["CreationTime"],
1425 LastAccessTime=attrs["LastAccessTime"],
1426 LastWriteTime=attrs["LastWriteTime"],
1427 ChangeTime=attrs["ChangeTime"],
1428 FileAttributes=attrs["FileAttributes"],
1429 ),
1430 StandardInformation=FileStandardInformation(
1431 EndOfFile=attrs["EndOfFile"],
1432 AllocationSize=attrs["AllocationSize"],
1433 ),
1434 )
1435 elif pkt.FileInfoClass == 0x15: # FileAlternateNameInformation
1436 pth = self.current_handles[fid][0]
1437 fileinfo = FileAlternateNameInformation(
1438 FileName=pth.name,
1439 )
1440 elif pkt.FileInfoClass == 0x16: # FileStreamInformation
1441 attrs = self.current_handles[fid][1]
1442 fileinfo = FileStreamInformation(
1443 StreamSize=attrs["EndOfFile"],
1444 StreamAllocationSize=attrs["AllocationSize"],
1445 )
1446 elif pkt.FileInfoClass == 0x22: # FileNetworkOpenInformation
1447 attrs = self.current_handles[fid][1]
1448 fileinfo = FileNetworkOpenInformation(
1449 **attrs,
1450 )
1451 elif pkt.FileInfoClass == 0x30: # FileNormalizedNameInformation
1452 pth = self.current_handles[fid][0]
1453 fileinfo = FILE_NAME_INFORMATION(
1454 FileName=pth.name,
1455 )
1456 else:
1457 log_runtime.warning(
1458 "Unimplemented: %s"
1459 % pkt[SMB2_Query_Info_Request].sprintf("%InfoType% %FileInfoClass%")
1460 )
1461 return
1462 elif pkt.InfoType == 0x02: # SMB2_0_INFO_FILESYSTEM
1463 # [MS-FSCC] sect 2.5
1464 if pkt.FileInfoClass == 0x01: # FileFsVolumeInformation
1465 fileinfo = FileFsVolumeInformation()
1466 elif pkt.FileInfoClass == 0x03: # FileFsSizeInformation
1467 fileinfo = FileFsSizeInformation()
1468 elif pkt.FileInfoClass == 0x05: # FileFsAttributeInformation
1469 fileinfo = FileFsAttributeInformation(
1470 FileSystemAttributes=0x88000F,
1471 )
1472 elif pkt.FileInfoClass == 0x07: # FileEaInformation
1473 fileinfo = FileEaInformation()
1474 else:
1475 log_runtime.warning(
1476 "Unimplemented: %s"
1477 % pkt[SMB2_Query_Info_Request].sprintf("%InfoType% %FileInfoClass%")
1478 )
1479 return
1480 elif pkt.InfoType == 0x03: # SMB2_0_INFO_SECURITY
1481 # [MS-FSCC] 2.4.6
1482 fileinfo = SECURITY_DESCRIPTOR()
1483 # TODO: fill it
1484 if pkt.AdditionalInformation.OWNER_SECURITY_INFORMATION:
1485 pass
1486 if pkt.AdditionalInformation.GROUP_SECURITY_INFORMATION:
1487 pass
1488 if pkt.AdditionalInformation.DACL_SECURITY_INFORMATION:
1489 pass
1490 if pkt.AdditionalInformation.SACL_SECURITY_INFORMATION:
1491 pass
1492 # Observed:
1493 if (
1494 pkt.AdditionalInformation.OWNER_SECURITY_INFORMATION
1495 or pkt.AdditionalInformation.SACL_SECURITY_INFORMATION
1496 or pkt.AdditionalInformation.GROUP_SECURITY_INFORMATION
1497 or pkt.AdditionalInformation.DACL_SECURITY_INFORMATION
1498 ):
1499 pkt = self.smb_header.copy() / SMB2_Error_Response(ErrorData=b"\xff")
1500 pkt.Status = "STATUS_ACCESS_DENIED"
1501 pkt.Command = "SMB2_QUERY_INFO"
1502 self.send(pkt)
1503 return
1504 if pkt.AdditionalInformation.ATTRIBUTE_SECURITY_INFORMATION:
1505 fileinfo.Control = 0x8800
1506 self.send(
1507 self.smb_header.copy()
1508 / SMB2_Query_Info_Response(Buffer=[("Output", fileinfo)])
1509 )
1510
1511 @ATMT.receive_condition(SERVING)
1512 def receive_set_info_request(self, pkt):
1513 if SMB2_Set_Info_Request in pkt:
1514 raise self.SERVING().action_parameters(pkt)
1515
1516 @ATMT.action(receive_set_info_request)
1517 def send_set_info_response(self, pkt):
1518 self.update_smbheader(pkt)
1519 self.send(self.smb_header.copy() / SMB2_Set_Info_Response())
1520
1521 @ATMT.receive_condition(SERVING)
1522 def receive_write_request(self, pkt):
1523 if SMB2_Write_Request in pkt:
1524 raise self.SERVING().action_parameters(pkt)
1525
1526 @ATMT.action(receive_write_request)
1527 def send_write_response(self, pkt):
1528 self.update_smbheader(pkt)
1529 resp = SMB2_Write_Response(Count=len(pkt.Data))
1530 fid = self.get_file_id(pkt)
1531 if self.current_tree() == "IPC$":
1532 if fid in self.PIPES_TABLE.values():
1533 # A pipe
1534 self.rpc_server.recv(pkt.Data)
1535 else:
1536 if self.readonly:
1537 # Read only !
1538 resp = SMB2_Error_Response()
1539 resp.Command = "SMB2_WRITE"
1540 resp.Status = "ERROR_FILE_READ_ONLY"
1541 else:
1542 # Write file
1543 pth, _ = self.current_handles[fid]
1544 length = pkt[SMB2_Write_Request].DataLen
1545 off = pkt[SMB2_Write_Request].Offset
1546 self.vprint("Writing %s bytes at %s" % (length, off))
1547 with open(pth, "r+b") as fd:
1548 fd.seek(off)
1549 resp.Count = fd.write(pkt[SMB2_Write_Request].Data)
1550 self.send(self.smb_header.copy() / resp)
1551
1552 @ATMT.receive_condition(SERVING)
1553 def receive_read_request(self, pkt):
1554 if SMB2_Read_Request in pkt:
1555 raise self.SERVING().action_parameters(pkt)
1556
1557 @ATMT.action(receive_read_request)
1558 def send_read_response(self, pkt):
1559 self.update_smbheader(pkt)
1560 resp = SMB2_Read_Response()
1561 fid = self.get_file_id(pkt)
1562 if self.current_tree() == "IPC$":
1563 # Read output from DCE/RPC server
1564 r = self.rpc_server.get_response()
1565 resp.Data = bytes(r)
1566 else:
1567 # Read file and send content
1568 pth, _ = self.current_handles[fid]
1569 length = pkt[SMB2_Read_Request].Length
1570 off = pkt[SMB2_Read_Request].Offset
1571 self.vprint("Reading %s bytes at %s" % (length, off))
1572 with open(pth, "rb") as fd:
1573 fd.seek(off)
1574 resp.Data = fd.read(length)
1575 self.send(self.smb_header.copy() / resp)
1576
1577 @ATMT.receive_condition(SERVING)
1578 def receive_close_request(self, pkt):
1579 if SMB2_Close_Request in pkt:
1580 raise self.SERVING().action_parameters(pkt)
1581
1582 @ATMT.action(receive_close_request)
1583 def send_close_response(self, pkt):
1584 self.update_smbheader(pkt)
1585 if self.current_tree() != "IPC$":
1586 fid = self.get_file_id(pkt)
1587 pth, attrs = self.current_handles[fid]
1588 if pth:
1589 self.vprint("Closed: " + str(pth))
1590 del self.current_handles[fid]
1591 del self.enumerate_index[fid]
1592 self.send(
1593 self.smb_header.copy()
1594 / SMB2_Close_Response(
1595 Flags=pkt[SMB2_Close_Request].Flags,
1596 **attrs,
1597 )
1598 )
1599 else:
1600 self.send(self.smb_header.copy() / SMB2_Close_Response())
1601
1602 @ATMT.receive_condition(SERVING)
1603 def receive_tree_disconnect_request(self, pkt):
1604 if SMB2_Tree_Disconnect_Request in pkt:
1605 raise self.SERVING().action_parameters(pkt)
1606
1607 @ATMT.action(receive_tree_disconnect_request)
1608 def send_tree_disconnect_response(self, pkt):
1609 self.update_smbheader(pkt)
1610 try:
1611 del self.current_trees[self.smb_header.TID] # clear tree
1612 resp = self.smb_header.copy() / SMB2_Tree_Disconnect_Response()
1613 except KeyError:
1614 resp = self.smb_header.copy() / SMB2_Error_Response()
1615 resp.Command = "SMB2_TREE_DISCONNECT"
1616 resp.Status = "STATUS_NETWORK_NAME_DELETED"
1617 self.send(resp)
1618
1619 @ATMT.receive_condition(SERVING)
1620 def receive_cancel_request(self, pkt):
1621 if SMB2_Cancel_Request in pkt:
1622 raise self.SERVING().action_parameters(pkt)
1623
1624 @ATMT.action(receive_cancel_request)
1625 def send_notify_cancel_response(self, pkt):
1626 self.update_smbheader(pkt)
1627 resp = self.smb_header.copy() / SMB2_Change_Notify_Response()
1628 resp.Status = "STATUS_CANCELLED"
1629 self.send(resp)
1630
1631 @ATMT.receive_condition(SERVING)
1632 def receive_echo_request(self, pkt):
1633 if SMB2_Echo_Request in pkt:
1634 raise self.SERVING().action_parameters(pkt)
1635
1636 @ATMT.action(receive_echo_request)
1637 def send_echo_reply(self, pkt):
1638 self.update_smbheader(pkt)
1639 self.send(self.smb_header.copy() / SMB2_Echo_Response())
1640
1641
1642# DCE/RPC server for SMB
1643
1644
1645class SMB_DCERPC_Server(DCERPC_Server):
1646 """
1647 DCE/RPC server than handles the minimum RPCs for SMB to work:
1648 """
1649
1650 def __init__(self, *args, **kwargs):
1651 self.shares = kwargs.pop("shares")
1652 super(SMB_DCERPC_Server, self).__init__(*args, **kwargs)
1653
1654 @DCERPC_Server.answer(NetrShareEnum_Request)
1655 def netr_share_enum(self, req):
1656 """
1657 NetrShareEnum [MS-SRVS]
1658 "retrieves information about each shared resource on a server."
1659 """
1660 nbEntries = len(self.shares)
1661 return NetrShareEnum_Response(
1662 InfoStruct=LPSHARE_ENUM_STRUCT(
1663 Level=1,
1664 ShareInfo=NDRUnion(
1665 tag=1,
1666 value=SHARE_INFO_1_CONTAINER(
1667 Buffer=[
1668 # Add shares
1669 LPSHARE_INFO_1(
1670 shi1_netname=x.name,
1671 shi1_type=x.type,
1672 shi1_remark=x.remark,
1673 )
1674 for x in self.shares
1675 ],
1676 EntriesRead=nbEntries,
1677 ),
1678 ),
1679 ),
1680 TotalEntries=nbEntries,
1681 ndr64=self.ndr64,
1682 )
1683
1684 @DCERPC_Server.answer(NetrWkstaGetInfo_Request)
1685 def netr_wksta_getinfo(self, req):
1686 """
1687 NetrWkstaGetInfo [MS-SRVS]
1688 "returns information about the configuration of a workstation."
1689 """
1690 return NetrWkstaGetInfo_Response(
1691 WkstaInfo=NDRUnion(
1692 tag=100,
1693 value=LPWKSTA_INFO_100(
1694 wki100_platform_id=500, # NT
1695 wki100_ver_major=5,
1696 ),
1697 ),
1698 ndr64=self.ndr64,
1699 )
1700
1701 @DCERPC_Server.answer(NetrServerGetInfo_Request)
1702 def netr_server_getinfo(self, req):
1703 """
1704 NetrServerGetInfo [MS-WKST]
1705 "retrieves current configuration information for CIFS and
1706 SMB Version 1.0 servers."
1707 """
1708 return NetrServerGetInfo_Response(
1709 ServerInfo=NDRUnion(
1710 tag=101,
1711 value=LPSERVER_INFO_101(
1712 sv101_platform_id=500, # NT
1713 sv101_name=req.ServerName.value.value[0].value,
1714 sv101_version_major=6,
1715 sv101_version_minor=1,
1716 sv101_type=1, # Workstation
1717 ),
1718 ),
1719 ndr64=self.ndr64,
1720 )
1721
1722 @DCERPC_Server.answer(NetrShareGetInfo_Request)
1723 def netr_share_getinfo(self, req):
1724 """
1725 NetrShareGetInfo [MS-SRVS]
1726 "retrieves information about a particular shared resource on a server."
1727 """
1728 return NetrShareGetInfo_Response(
1729 ShareInfo=NDRUnion(
1730 tag=1,
1731 value=LPSHARE_INFO_1(
1732 shi1_netname=req.NetName.value[0].value,
1733 shi1_type=0,
1734 shi1_remark=b"",
1735 ),
1736 ),
1737 ndr64=self.ndr64,
1738 )
1739
1740
1741# Util
1742
1743
1744class smbserver:
1745 r"""
1746 Spawns a simple smbserver
1747
1748 smbserver parameters:
1749
1750 :param shares: the list of shares to announce. Note that IPC$ is appended.
1751 By default, a 'Scapy' share on './'
1752 :param port: (optional) the port to bind on, default 445
1753 :param iface: (optional) the interface to bind on, default conf.iface
1754 :param readonly: (optional) whether the server is read-only or not. default True
1755 :param ssp: (optional) the SSP to use. See the examples below.
1756 Default NTLM with guest
1757
1758 Many more SMB-specific parameters are available in help(SMB_Server)
1759 """
1760
1761 def __init__(
1762 self,
1763 shares=None,
1764 iface: str = None,
1765 port: int = 445,
1766 verb: int = 2,
1767 readonly: bool = True,
1768 # SMB arguments
1769 ssp=None,
1770 **kwargs,
1771 ):
1772 # Default Share
1773 if shares is None:
1774 shares = [
1775 SMBShare(
1776 name="Scapy", path=".", remark="Scapy's SMB server default share"
1777 )
1778 ]
1779 # Verb
1780 if verb >= 2:
1781 log_runtime.info("-- Scapy %s SMB Server --" % conf.version)
1782 log_runtime.info(
1783 "SSP: %s. Read-Only: %s. Serving %s shares:"
1784 % (
1785 conf.color_theme.yellow(ssp or "NTLM (guest)"),
1786 (
1787 conf.color_theme.yellow("YES")
1788 if readonly
1789 else conf.color_theme.format("NO", "bg_red+white")
1790 ),
1791 conf.color_theme.red(len(shares)),
1792 )
1793 )
1794 for share in shares:
1795 log_runtime.info(" * %s" % share)
1796 # Start SMB Server
1797 self.srv = SMB_Server.spawn(
1798 # TCP server
1799 port=port,
1800 iface=iface or conf.loopback_name,
1801 verb=verb,
1802 # SMB server
1803 ssp=ssp,
1804 shares=shares,
1805 readonly=readonly,
1806 # SMB arguments
1807 **kwargs,
1808 )
1809
1810 def close(self):
1811 """
1812 Close the smbserver if started in background mode (bg=True)
1813 """
1814 if self.srv:
1815 try:
1816 self.srv.shutdown(socket.SHUT_RDWR)
1817 except OSError:
1818 pass
1819 self.srv.close()
1820
1821
1822if __name__ == "__main__":
1823 from scapy.utils import AutoArgparse
1824
1825 AutoArgparse(smbserver)