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