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