• 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 1 / 2 Client Automaton
8
9
10.. note::
11    You will find more complete documentation for this layer over at
12    `SMB <https://scapy.readthedocs.io/en/latest/layers/smb.html#client>`_
13"""
14
15import io
16import os
17import pathlib
18import socket
19import time
20import threading
21
22from scapy.automaton import ATMT, Automaton, ObjectPipe
23from scapy.base_classes import Net
24from scapy.config import conf
25from scapy.error import Scapy_Exception
26from scapy.fields import UTCTimeField
27from scapy.supersocket import SuperSocket
28from scapy.utils import (
29    CLIUtil,
30    pretty_list,
31    human_size,
32    valid_ip,
33    valid_ip6,
34)
35from scapy.volatile import RandUUID
36
37from scapy.layers.dcerpc import NDRUnion, find_dcerpc_interface
38from scapy.layers.gssapi import (
39    GSS_S_COMPLETE,
40    GSS_S_CONTINUE_NEEDED,
41    GSS_C_FLAGS,
42)
43from scapy.layers.inet6 import Net6
44from scapy.layers.kerberos import (
45    KerberosSSP,
46    krb_as_and_tgs,
47    _parse_upn,
48)
49from scapy.layers.msrpce.raw.ms_srvs import (
50    LPSHARE_ENUM_STRUCT,
51    NetrShareEnum_Request,
52    NetrShareEnum_Response,
53    SHARE_INFO_1_CONTAINER,
54)
55from scapy.layers.ntlm import (
56    NTLMSSP,
57    MD4le,
58)
59from scapy.layers.smb import (
60    SMBNegotiate_Request,
61    SMBNegotiate_Response_Extended_Security,
62    SMBNegotiate_Response_Security,
63    SMBSession_Null,
64    SMBSession_Setup_AndX_Request,
65    SMBSession_Setup_AndX_Request_Extended_Security,
66    SMBSession_Setup_AndX_Response,
67    SMBSession_Setup_AndX_Response_Extended_Security,
68    SMB_Dialect,
69    SMB_Header,
70)
71from scapy.layers.smb2 import (
72    DirectTCP,
73    FileAllInformation,
74    FileIdBothDirectoryInformation,
75    SMB_DIALECTS,
76    SMB2_Change_Notify_Request,
77    SMB2_Change_Notify_Response,
78    SMB2_Close_Request,
79    SMB2_Close_Response,
80    SMB2_Create_Context,
81    SMB2_CREATE_DURABLE_HANDLE_REQUEST_V2,
82    SMB2_CREATE_REQUEST_LEASE_V2,
83    SMB2_CREATE_REQUEST_LEASE,
84    SMB2_Create_Request,
85    SMB2_Create_Response,
86    SMB2_Encryption_Capabilities,
87    SMB2_ENCRYPTION_CIPHERS,
88    SMB2_Error_Response,
89    SMB2_Header,
90    SMB2_IOCTL_Request,
91    SMB2_IOCTL_Response,
92    SMB2_Negotiate_Context,
93    SMB2_Negotiate_Protocol_Request,
94    SMB2_Negotiate_Protocol_Response,
95    SMB2_Netname_Negotiate_Context_ID,
96    SMB2_Preauth_Integrity_Capabilities,
97    SMB2_Query_Directory_Request,
98    SMB2_Query_Directory_Response,
99    SMB2_Query_Info_Request,
100    SMB2_Query_Info_Response,
101    SMB2_Read_Request,
102    SMB2_Read_Response,
103    SMB2_Session_Setup_Request,
104    SMB2_Session_Setup_Response,
105    SMB2_SIGNING_ALGORITHMS,
106    SMB2_Signing_Capabilities,
107    SMB2_Tree_Connect_Request,
108    SMB2_Tree_Connect_Response,
109    SMB2_Tree_Disconnect_Request,
110    SMB2_Tree_Disconnect_Response,
111    SMB2_Write_Request,
112    SMB2_Write_Response,
113    SMBStreamSocket,
114    SRVSVC_SHARE_TYPES,
115    STATUS_ERREF,
116)
117from scapy.layers.spnego import SPNEGOSSP
118
119
120class SMB_Client(Automaton):
121    """
122    SMB client automaton
123
124    :param sock: the SMBStreamSocket to use
125    :param ssp: the SSP to use
126
127    All other options (in caps) are optional, and SMB specific:
128
129    :param REQUIRE_SIGNATURE: set 'Require Signature'
130    :param MIN_DIALECT: minimum SMB dialect. Defaults to 0x0202 (2.0.2)
131    :param MAX_DIALECT: maximum SMB dialect. Defaults to 0x0311 (3.1.1)
132    :param DIALECTS: list of supported SMB2 dialects.
133                     Constructed from MIN_DIALECT, MAX_DIALECT otherwise.
134    """
135
136    port = 445
137    cls = DirectTCP
138
139    def __init__(self, sock, ssp=None, *args, **kwargs):
140        # Various SMB client arguments
141        self.EXTENDED_SECURITY = kwargs.pop("EXTENDED_SECURITY", True)
142        self.USE_SMB1 = kwargs.pop("USE_SMB1", False)
143        self.REQUIRE_SIGNATURE = kwargs.pop("REQUIRE_SIGNATURE", False)
144        self.RETRY = kwargs.pop("RETRY", 0)  # optionally: retry n times session setup
145        self.SMB2 = kwargs.pop("SMB2", False)  # optionally: start directly in SMB2
146        self.SERVER_NAME = kwargs.pop("SERVER_NAME", "")
147        # Store supported dialects
148        if "DIALECTS" in kwargs:
149            self.DIALECTS = kwargs.pop("DIALECTS")
150        else:
151            MIN_DIALECT = kwargs.pop("MIN_DIALECT", 0x0202)
152            self.MAX_DIALECT = kwargs.pop("MAX_DIALECT", 0x0311)
153            self.DIALECTS = sorted(
154                [
155                    x
156                    for x in [0x0202, 0x0210, 0x0300, 0x0302, 0x0311]
157                    if x >= MIN_DIALECT and x <= self.MAX_DIALECT
158                ]
159            )
160        # Internal Session information
161        self.IsGuest = False
162        self.ErrorStatus = None
163        self.NegotiateCapabilities = None
164        self.GUID = RandUUID()._fix()
165        self.SequenceWindow = (0, 0)  # keep track of allowed MIDs
166        self.MaxTransactionSize = 0
167        self.MaxReadSize = 0
168        self.MaxWriteSize = 0
169        if ssp is None:
170            # We got no SSP. Assuming the server allows anonymous
171            ssp = SPNEGOSSP(
172                [
173                    NTLMSSP(
174                        UPN="guest",
175                        HASHNT=b"",
176                    )
177                ]
178            )
179        # Initialize
180        kwargs["sock"] = sock
181        Automaton.__init__(
182            self,
183            *args,
184            **kwargs,
185        )
186        if self.is_atmt_socket:
187            self.smb_sock_ready = threading.Event()
188        # Set session options
189        self.session.ssp = ssp
190        self.session.SecurityMode = kwargs.pop(
191            "SECURITY_MODE",
192            3 if self.REQUIRE_SIGNATURE else int(bool(ssp)),
193        )
194        self.session.Dialect = self.MAX_DIALECT
195
196    @classmethod
197    def from_tcpsock(cls, sock, **kwargs):
198        return cls.smblink(
199            None,
200            SMBStreamSocket(sock, DirectTCP),
201            **kwargs,
202        )
203
204    @property
205    def session(self):
206        # session shorthand
207        return self.sock.session
208
209    def send(self, pkt):
210        # Calculate what CreditCharge to send.
211        if self.session.Dialect > 0x0202 and isinstance(pkt.payload, SMB2_Header):
212            # [MS-SMB2] sect 3.2.4.1.5
213            typ = type(pkt.payload.payload)
214            if typ is SMB2_Negotiate_Protocol_Request:
215                # See [MS-SMB2] 3.2.4.1.2 note
216                pkt.CreditCharge = 0
217            elif typ in [
218                SMB2_Read_Request,
219                SMB2_Write_Request,
220                SMB2_IOCTL_Request,
221                SMB2_Query_Directory_Request,
222                SMB2_Change_Notify_Request,
223                SMB2_Query_Info_Request,
224            ]:
225                # [MS-SMB2] 3.1.5.2
226                # "For READ, WRITE, IOCTL, and QUERY_DIRECTORY requests"
227                # "CHANGE_NOTIFY, QUERY_INFO, or SET_INFO"
228                if typ == SMB2_Read_Request:
229                    Length = pkt.payload.Length
230                elif typ == SMB2_Write_Request:
231                    Length = len(pkt.payload.Data)
232                elif typ == SMB2_IOCTL_Request:
233                    # [MS-SMB2] 3.3.5.15
234                    Length = max(len(pkt.payload.Input), pkt.payload.MaxOutputResponse)
235                elif typ in [
236                    SMB2_Query_Directory_Request,
237                    SMB2_Change_Notify_Request,
238                    SMB2_Query_Info_Request,
239                ]:
240                    Length = pkt.payload.OutputBufferLength
241                else:
242                    raise RuntimeError("impossible case")
243                pkt.CreditCharge = 1 + (Length - 1) // 65536
244            else:
245                # "For all other requests, the client MUST set CreditCharge to 1"
246                pkt.CreditCharge = 1
247            # [MS-SMB2] 3.2.4.1.2
248            pkt.CreditRequest = pkt.CreditCharge + 1  # this code is a bit lazy
249        # Get first available message ID: [MS-SMB2] 3.2.4.1.3 and 3.2.4.1.5
250        pkt.MID = self.SequenceWindow[0]
251        return super(SMB_Client, self).send(pkt)
252
253    @ATMT.state(initial=1)
254    def BEGIN(self):
255        pass
256
257    @ATMT.condition(BEGIN)
258    def continue_smb2(self):
259        if self.SMB2:  # Directly started in SMB2
260            self.smb_header = DirectTCP() / SMB2_Header(PID=0xFEFF)
261            raise self.SMB2_NEGOTIATE()
262
263    @ATMT.condition(BEGIN, prio=1)
264    def send_negotiate(self):
265        raise self.SENT_NEGOTIATE()
266
267    @ATMT.action(send_negotiate)
268    def on_negotiate(self):
269        # [MS-SMB2] sect 3.2.4.2.2.1 - Multi-Protocol Negotiate
270        self.smb_header = DirectTCP() / SMB_Header(
271            Flags2=(
272                "LONG_NAMES+EAS+NT_STATUS+UNICODE+"
273                "SMB_SECURITY_SIGNATURE+EXTENDED_SECURITY"
274            ),
275            TID=0xFFFF,
276            PIDLow=0xFEFF,
277            UID=0,
278            MID=0,
279        )
280        if self.EXTENDED_SECURITY:
281            self.smb_header.Flags2 += "EXTENDED_SECURITY"
282        pkt = self.smb_header.copy() / SMBNegotiate_Request(
283            Dialects=[
284                SMB_Dialect(DialectString=x)
285                for x in [
286                    "PC NETWORK PROGRAM 1.0",
287                    "LANMAN1.0",
288                    "Windows for Workgroups 3.1a",
289                    "LM1.2X002",
290                    "LANMAN2.1",
291                    "NT LM 0.12",
292                ]
293                + (["SMB 2.002", "SMB 2.???"] if not self.USE_SMB1 else [])
294            ],
295        )
296        if not self.EXTENDED_SECURITY:
297            pkt.Flags2 -= "EXTENDED_SECURITY"
298        pkt[SMB_Header].Flags2 = (
299            pkt[SMB_Header].Flags2
300            - "SMB_SECURITY_SIGNATURE"
301            + "SMB_SECURITY_SIGNATURE_REQUIRED+IS_LONG_NAME"
302        )
303        self.send(pkt)
304
305    @ATMT.state()
306    def SENT_NEGOTIATE(self):
307        pass
308
309    @ATMT.state()
310    def SMB2_NEGOTIATE(self):
311        pass
312
313    @ATMT.condition(SMB2_NEGOTIATE)
314    def send_negotiate_smb2(self):
315        raise self.SENT_NEGOTIATE()
316
317    @ATMT.action(send_negotiate_smb2)
318    def on_negotiate_smb2(self):
319        # [MS-SMB2] sect 3.2.4.2.2.2 - SMB2-Only Negotiate
320        pkt = self.smb_header.copy() / SMB2_Negotiate_Protocol_Request(
321            Dialects=self.DIALECTS,
322            SecurityMode=self.session.SecurityMode,
323        )
324        if self.MAX_DIALECT >= 0x0210:
325            # "If the client implements the SMB 2.1 or SMB 3.x dialect, ClientGuid
326            # MUST be set to the global ClientGuid value"
327            pkt.ClientGUID = self.GUID
328        # Capabilities: same as [MS-SMB2] 3.3.5.4
329        self.NegotiateCapabilities = "+".join(
330            [
331                "DFS",
332                "LEASING",
333                "LARGE_MTU",
334            ]
335        )
336        if self.MAX_DIALECT >= 0x0300:
337            # "if Connection.Dialect belongs to the SMB 3.x dialect family ..."
338            self.NegotiateCapabilities += "+" + "+".join(
339                [
340                    "MULTI_CHANNEL",
341                    "PERSISTENT_HANDLES",
342                    "DIRECTORY_LEASING",
343                ]
344            )
345        if self.MAX_DIALECT >= 0x0300:
346            # "If the client implements the SMB 3.x dialect family, the client MUST
347            # set the Capabilities field as follows"
348            self.NegotiateCapabilities += "+ENCRYPTION"
349        if self.MAX_DIALECT >= 0x0311:
350            # "If the client implements the SMB 3.1.1 dialect, it MUST do"
351            pkt.NegotiateContexts = [
352                SMB2_Negotiate_Context()
353                / SMB2_Preauth_Integrity_Capabilities(
354                    # SHA-512 by default
355                    HashAlgorithms=[self.session.PreauthIntegrityHashId],
356                    Salt=self.session.Salt,
357                ),
358                SMB2_Negotiate_Context()
359                / SMB2_Encryption_Capabilities(
360                    # AES-128-CCM by default
361                    Ciphers=[self.session.CipherId],
362                ),
363                # TODO support compression and RDMA
364                SMB2_Negotiate_Context()
365                / SMB2_Netname_Negotiate_Context_ID(
366                    NetName=self.SERVER_NAME,
367                ),
368                SMB2_Negotiate_Context()
369                / SMB2_Signing_Capabilities(
370                    # AES-128-CCM by default
371                    SigningAlgorithms=[self.session.SigningAlgorithmId],
372                ),
373            ]
374        pkt.Capabilities = self.NegotiateCapabilities
375        # Send
376        self.send(pkt)
377        # If required, compute sessions
378        self.session.computeSMBConnectionPreauth(
379            bytes(pkt[SMB2_Header]),  # nego request
380        )
381
382    @ATMT.receive_condition(SENT_NEGOTIATE)
383    def receive_negotiate_response(self, pkt):
384        if (
385            SMBNegotiate_Response_Extended_Security in pkt
386            or SMB2_Negotiate_Protocol_Response in pkt
387        ):
388            # Extended SMB1 / SMB2
389            try:
390                ssp_blob = pkt.SecurityBlob  # eventually SPNEGO server initiation
391            except AttributeError:
392                ssp_blob = None
393            if (
394                SMB2_Negotiate_Protocol_Response in pkt
395                and pkt.DialectRevision & 0xFF == 0xFF
396            ):
397                # Version is SMB X.???
398                # [MS-SMB2] 3.2.5.2
399                # If the DialectRevision field in the SMB2 NEGOTIATE Response is
400                # 0x02FF ... the client MUST allocate sequence number 1 from
401                # Connection.SequenceWindow, and MUST set MessageId field of the
402                # SMB2 header to 1.
403                self.SequenceWindow = (1, 1)
404                self.smb_header = DirectTCP() / SMB2_Header(PID=0xFEFF, MID=1)
405                self.SMB2 = True  # We're now using SMB2 to talk to the server
406                raise self.SMB2_NEGOTIATE()
407            else:
408                if SMB2_Negotiate_Protocol_Response in pkt:
409                    # SMB2 was negotiated !
410                    self.session.Dialect = pkt.DialectRevision
411                    # If required, compute sessions
412                    self.session.computeSMBConnectionPreauth(
413                        bytes(pkt[SMB2_Header]),  # nego response
414                    )
415                    # Process max sizes
416                    self.MaxReadSize = pkt.MaxReadSize
417                    self.MaxTransactionSize = pkt.MaxTransactionSize
418                    self.MaxWriteSize = pkt.MaxWriteSize
419                    # Process NegotiateContext
420                    if self.session.Dialect >= 0x0311 and pkt.NegotiateContextsCount:
421                        for ngctx in pkt.NegotiateContexts:
422                            if ngctx.ContextType == 0x0002:
423                                # SMB2_ENCRYPTION_CAPABILITIES
424                                self.session.CipherId = SMB2_ENCRYPTION_CIPHERS[
425                                    ngctx.Ciphers[0]
426                                ]
427                            elif ngctx.ContextType == 0x0008:
428                                # SMB2_SIGNING_CAPABILITIES
429                                self.session.SigningAlgorithmId = (
430                                    SMB2_SIGNING_ALGORITHMS[ngctx.SigningAlgorithms[0]]
431                                )
432                self.update_smbheader(pkt)
433                raise self.NEGOTIATED(ssp_blob)
434        elif SMBNegotiate_Response_Security in pkt:
435            # Non-extended SMB1
436            # Never tested. FIXME. probably broken
437            raise self.NEGOTIATED(pkt.Challenge)
438
439    @ATMT.state()
440    def NEGOTIATED(self, ssp_blob=None):
441        # Negotiated ! We now know the Dialect
442        if self.session.Dialect > 0x0202:
443            # [MS-SMB2] sect 3.2.5.1.4
444            self.smb_header.CreditRequest = 1
445        # Begin session establishment
446        ssp_tuple = self.session.ssp.GSS_Init_sec_context(
447            self.session.sspcontext,
448            ssp_blob,
449            req_flags=(
450                GSS_C_FLAGS.GSS_C_MUTUAL_FLAG
451                | (
452                    GSS_C_FLAGS.GSS_C_INTEG_FLAG
453                    if self.session.SecurityMode != 0
454                    else 0
455                )
456            ),
457        )
458        return ssp_tuple
459
460    def update_smbheader(self, pkt):
461        """
462        Called when receiving a SMB2 packet to update the current smb_header
463        """
464        # Some values should not be updated when ASYNC
465        if not pkt.Flags.SMB2_FLAGS_ASYNC_COMMAND:
466            # Update IDs
467            self.smb_header.SessionId = pkt.SessionId
468            self.smb_header.TID = pkt.TID
469            self.smb_header.PID = pkt.PID
470        # [MS-SMB2] 3.2.5.1.4
471        self.SequenceWindow = (
472            self.SequenceWindow[0] + max(pkt.CreditCharge, 1),
473            self.SequenceWindow[1] + pkt.CreditRequest,
474        )
475
476    # DEV: add a condition on NEGOTIATED with prio=0
477
478    @ATMT.condition(NEGOTIATED, prio=1)
479    def should_send_setup_andx_request(self, ssp_tuple):
480        _, _, negResult = ssp_tuple
481        if negResult not in [GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED]:
482            raise ValueError("Internal error: the SSP completed with an error.")
483        raise self.SENT_SETUP_ANDX_REQUEST().action_parameters(ssp_tuple)
484
485    @ATMT.state()
486    def SENT_SETUP_ANDX_REQUEST(self):
487        pass
488
489    @ATMT.action(should_send_setup_andx_request)
490    def send_setup_andx_request(self, ssp_tuple):
491        self.session.sspcontext, token, negResult = ssp_tuple
492        if self.SMB2 and negResult == GSS_S_CONTINUE_NEEDED:
493            # New session: force 0
494            self.SessionId = 0
495        if self.SMB2 or self.EXTENDED_SECURITY:
496            # SMB1 extended / SMB2
497            if self.SMB2:
498                # SMB2
499                pkt = self.smb_header.copy() / SMB2_Session_Setup_Request(
500                    Capabilities="DFS",
501                    SecurityMode=self.session.SecurityMode,
502                )
503            else:
504                # SMB1 extended
505                pkt = (
506                    self.smb_header.copy()
507                    / SMBSession_Setup_AndX_Request_Extended_Security(
508                        ServerCapabilities=(
509                            "UNICODE+NT_SMBS+STATUS32+LEVEL_II_OPLOCKS+"
510                            "DYNAMIC_REAUTH+EXTENDED_SECURITY"
511                        ),
512                        NativeOS=b"",
513                        NativeLanMan=b"",
514                    )
515                )
516            pkt.SecurityBlob = token
517        else:
518            # Non-extended security.
519            pkt = self.smb_header.copy() / SMBSession_Setup_AndX_Request(
520                ServerCapabilities="UNICODE+NT_SMBS+STATUS32+LEVEL_II_OPLOCKS",
521                NativeOS=b"",
522                NativeLanMan=b"",
523                OEMPassword=b"\0" * 24,
524                UnicodePassword=token,
525            )
526        self.send(pkt)
527        if self.SMB2:
528            # If required, compute sessions
529            self.session.computeSMBSessionPreauth(
530                bytes(pkt[SMB2_Header]),  # session request
531            )
532
533    @ATMT.receive_condition(SENT_SETUP_ANDX_REQUEST)
534    def receive_setup_andx_response(self, pkt):
535        if (
536            SMBSession_Null in pkt
537            or SMBSession_Setup_AndX_Response_Extended_Security in pkt
538            or SMBSession_Setup_AndX_Response in pkt
539        ):
540            # SMB1
541            if SMBSession_Null in pkt:
542                # Likely an error
543                raise self.NEGOTIATED()
544        # Logging
545        if pkt.Status != 0 and pkt.Status != 0xC0000016:
546            # Not SUCCESS nor MORE_PROCESSING_REQUIRED: log
547            self.ErrorStatus = pkt.sprintf("%SMB2_Header.Status%")
548            self.debug(
549                lvl=1,
550                msg=conf.color_theme.red(
551                    pkt.sprintf("SMB Session Setup Response: %SMB2_Header.Status%")
552                ),
553            )
554        if self.SMB2:
555            self.update_smbheader(pkt)
556        # Cases depending on the response packet
557        if (
558            SMBSession_Setup_AndX_Response_Extended_Security in pkt
559            or SMB2_Session_Setup_Response in pkt
560        ):
561            # The server assigns us a SessionId
562            self.smb_header.SessionId = pkt.SessionId
563            # SMB1 extended / SMB2
564            if pkt.Status == 0:  # Authenticated
565                if SMB2_Session_Setup_Response in pkt and pkt.SessionFlags.IS_GUEST:
566                    # We were 'authenticated' in GUEST
567                    self.IsGuest = True
568                raise self.AUTHENTICATED(pkt.SecurityBlob)
569            else:
570                if SMB2_Header in pkt:
571                    # If required, compute sessions
572                    self.session.computeSMBSessionPreauth(
573                        bytes(pkt[SMB2_Header]),  # session response
574                    )
575                # Ongoing auth
576                raise self.NEGOTIATED(pkt.SecurityBlob)
577        elif SMBSession_Setup_AndX_Response_Extended_Security in pkt:
578            # SMB1 non-extended
579            pass
580        elif SMB2_Error_Response in pkt:
581            # Authentication failure
582            self.session.sspcontext.clifailure()
583            # Reset Session preauth (SMB 3.1.1)
584            self.session.SessionPreauthIntegrityHashValue = None
585            if not self.RETRY:
586                raise self.AUTH_FAILED()
587            self.debug(lvl=2, msg="RETRY: %s" % self.RETRY)
588            self.RETRY -= 1
589            raise self.NEGOTIATED()
590
591    @ATMT.state(final=1)
592    def AUTH_FAILED(self):
593        self.smb_sock_ready.set()
594
595    @ATMT.state()
596    def AUTHENTICATED(self, ssp_blob=None):
597        self.session.sspcontext, _, status = self.session.ssp.GSS_Init_sec_context(
598            self.session.sspcontext, ssp_blob
599        )
600        if status != GSS_S_COMPLETE:
601            raise ValueError("Internal error: the SSP completed with an error.")
602        # Authentication was successful
603        self.session.computeSMBSessionKey()
604        if self.IsGuest:
605            # When authenticated in Guest, the sessionkey the client has is invalid
606            self.session.SMBSessionKey = None
607
608    # DEV: add a condition on AUTHENTICATED with prio=0
609
610    @ATMT.condition(AUTHENTICATED, prio=1)
611    def authenticated_post_actions(self):
612        raise self.SOCKET_BIND()
613
614    # Plain SMB Socket
615
616    @ATMT.state()
617    def SOCKET_BIND(self):
618        self.smb_sock_ready.set()
619
620    @ATMT.condition(SOCKET_BIND)
621    def start_smb_socket(self):
622        raise self.SOCKET_MODE_SMB()
623
624    @ATMT.state()
625    def SOCKET_MODE_SMB(self):
626        pass
627
628    @ATMT.receive_condition(SOCKET_MODE_SMB)
629    def incoming_data_received_smb(self, pkt):
630        raise self.SOCKET_MODE_SMB().action_parameters(pkt)
631
632    @ATMT.action(incoming_data_received_smb)
633    def receive_data_smb(self, pkt):
634        resp = pkt[SMB2_Header].payload
635        if isinstance(resp, SMB2_Error_Response):
636            if pkt.Status == 0x00000103:  # STATUS_PENDING
637                # answer is coming later.. just wait...
638                return
639            if pkt.Status == 0x0000010B:  # STATUS_NOTIFY_CLEANUP
640                # this is a notify cleanup. ignore
641                return
642        self.update_smbheader(pkt)
643        # Add the status to the response as metadata
644        resp.NTStatus = pkt.sprintf("%SMB2_Header.Status%")
645        self.oi.smbpipe.send(resp)
646
647    @ATMT.ioevent(SOCKET_MODE_SMB, name="smbpipe", as_supersocket="smblink")
648    def outgoing_data_received_smb(self, fd):
649        raise self.SOCKET_MODE_SMB().action_parameters(fd.recv())
650
651    @ATMT.action(outgoing_data_received_smb)
652    def send_data(self, d):
653        self.send(self.smb_header.copy() / d)
654
655
656class SMB_SOCKET(SuperSocket):
657    """
658    Mid-level wrapper over SMB_Client.smblink that provides some basic SMB
659    client functions, such as tree connect, directory query, etc.
660    """
661
662    def __init__(self, smbsock, use_ioctl=True, timeout=3):
663        self.ins = smbsock
664        self.timeout = timeout
665        if not self.ins.atmt.smb_sock_ready.wait(timeout=timeout):
666            raise TimeoutError(
667                "The SMB handshake timed out ! (enable debug=1 for logs)"
668            )
669        if self.ins.atmt.ErrorStatus:
670            raise Scapy_Exception(
671                "SMB Session Setup failed: %s" % self.ins.atmt.ErrorStatus
672            )
673
674    @classmethod
675    def from_tcpsock(cls, sock, **kwargs):
676        """
677        Wraps the tcp socket in a SMB_Client.smblink first, then into the
678        SMB_SOCKET/SMB_RPC_SOCKET
679        """
680        return cls(
681            use_ioctl=kwargs.pop("use_ioctl", True),
682            timeout=kwargs.pop("timeout", 3),
683            smbsock=SMB_Client.from_tcpsock(sock, **kwargs),
684        )
685
686    @property
687    def session(self):
688        return self.ins.atmt.session
689
690    def set_TID(self, TID):
691        """
692        Set the TID (Tree ID).
693        This can be called before sending a packet
694        """
695        self.ins.atmt.smb_header.TID = TID
696
697    def get_TID(self):
698        """
699        Get the current TID from the underlying socket
700        """
701        return self.ins.atmt.smb_header.TID
702
703    def tree_connect(self, name):
704        """
705        Send a TreeConnect request
706        """
707        resp = self.ins.sr1(
708            SMB2_Tree_Connect_Request(
709                Buffer=[
710                    (
711                        "Path",
712                        "\\\\%s\\%s"
713                        % (
714                            self.session.sspcontext.ServerHostname,
715                            name,
716                        ),
717                    )
718                ]
719            ),
720            verbose=False,
721            timeout=self.timeout,
722        )
723        if not resp:
724            raise ValueError("TreeConnect timed out !")
725        if SMB2_Tree_Connect_Response not in resp:
726            raise ValueError("Failed TreeConnect ! %s" % resp.NTStatus)
727        return self.get_TID()
728
729    def tree_disconnect(self):
730        """
731        Send a TreeDisconnect request
732        """
733        resp = self.ins.sr1(
734            SMB2_Tree_Disconnect_Request(),
735            verbose=False,
736            timeout=self.timeout,
737        )
738        if not resp:
739            raise ValueError("TreeDisconnect timed out !")
740        if SMB2_Tree_Disconnect_Response not in resp:
741            raise ValueError("Failed TreeDisconnect ! %s" % resp.NTStatus)
742
743    def create_request(
744        self,
745        name,
746        mode="r",
747        type="pipe",
748        extra_create_options=[],
749        extra_desired_access=[],
750    ):
751        """
752        Open a file/pipe by its name
753
754        :param name: the name of the file or named pipe. e.g. 'srvsvc'
755        """
756        ShareAccess = []
757        DesiredAccess = []
758        # Common params depending on the access
759        if "r" in mode:
760            ShareAccess.append("FILE_SHARE_READ")
761            DesiredAccess.extend(["FILE_READ_DATA", "FILE_READ_ATTRIBUTES"])
762        if "w" in mode:
763            ShareAccess.append("FILE_SHARE_WRITE")
764            DesiredAccess.extend(["FILE_WRITE_DATA", "FILE_WRITE_ATTRIBUTES"])
765        if "d" in mode:
766            ShareAccess.append("FILE_SHARE_DELETE")
767        # Params depending on the type
768        FileAttributes = []
769        CreateOptions = []
770        CreateContexts = []
771        CreateDisposition = "FILE_OPEN"
772        if type == "folder":
773            FileAttributes.append("FILE_ATTRIBUTE_DIRECTORY")
774            CreateOptions.append("FILE_DIRECTORY_FILE")
775        elif type in ["file", "pipe"]:
776            CreateOptions = ["FILE_NON_DIRECTORY_FILE"]
777            if "r" in mode:
778                DesiredAccess.extend(["FILE_READ_EA", "READ_CONTROL", "SYNCHRONIZE"])
779            if "w" in mode:
780                CreateDisposition = "FILE_OVERWRITE_IF"
781                DesiredAccess.append("FILE_WRITE_EA")
782            if "d" in mode:
783                DesiredAccess.append("DELETE")
784                CreateOptions.append("FILE_DELETE_ON_CLOSE")
785            if type == "file":
786                FileAttributes.append("FILE_ATTRIBUTE_NORMAL")
787        elif type:
788            raise ValueError("Unknown type: %s" % type)
789        # [MS-SMB2] 3.2.4.3.8
790        RequestedOplockLevel = 0
791        if self.session.Dialect >= 0x0300:
792            RequestedOplockLevel = "SMB2_OPLOCK_LEVEL_LEASE"
793        elif self.session.Dialect >= 0x0210 and type == "file":
794            RequestedOplockLevel = "SMB2_OPLOCK_LEVEL_LEASE"
795        # SMB 3.X
796        if self.session.Dialect >= 0x0300 and type in ["file", "folder"]:
797            CreateContexts.extend(
798                [
799                    # [SMB2] sect 3.2.4.3.5
800                    SMB2_Create_Context(
801                        Name=b"DH2Q",
802                        Data=SMB2_CREATE_DURABLE_HANDLE_REQUEST_V2(
803                            CreateGuid=RandUUID()._fix()
804                        ),
805                    ),
806                    # [SMB2] sect 3.2.4.3.9
807                    SMB2_Create_Context(
808                        Name=b"MxAc",
809                    ),
810                    # [SMB2] sect 3.2.4.3.10
811                    SMB2_Create_Context(
812                        Name=b"QFid",
813                    ),
814                    # [SMB2] sect 3.2.4.3.8
815                    SMB2_Create_Context(
816                        Name=b"RqLs",
817                        Data=SMB2_CREATE_REQUEST_LEASE_V2(LeaseKey=RandUUID()._fix()),
818                    ),
819                ]
820            )
821        elif self.session.Dialect == 0x0210 and type == "file":
822            CreateContexts.extend(
823                [
824                    # [SMB2] sect 3.2.4.3.8
825                    SMB2_Create_Context(
826                        Name=b"RqLs",
827                        Data=SMB2_CREATE_REQUEST_LEASE(LeaseKey=RandUUID()._fix()),
828                    ),
829                ]
830            )
831        # Extra options
832        if extra_create_options:
833            CreateOptions.extend(extra_create_options)
834        if extra_desired_access:
835            DesiredAccess.extend(extra_desired_access)
836        # Request
837        resp = self.ins.sr1(
838            SMB2_Create_Request(
839                ImpersonationLevel="Impersonation",
840                DesiredAccess="+".join(DesiredAccess),
841                CreateDisposition=CreateDisposition,
842                CreateOptions="+".join(CreateOptions),
843                ShareAccess="+".join(ShareAccess),
844                FileAttributes="+".join(FileAttributes),
845                CreateContexts=CreateContexts,
846                RequestedOplockLevel=RequestedOplockLevel,
847                Name=name,
848            ),
849            verbose=0,
850            timeout=self.timeout,
851        )
852        if not resp:
853            raise ValueError("CreateRequest timed out !")
854        if SMB2_Create_Response not in resp:
855            raise ValueError("Failed CreateRequest ! %s" % resp.NTStatus)
856        return resp[SMB2_Create_Response].FileId
857
858    def close_request(self, FileId):
859        """
860        Close the FileId
861        """
862        pkt = SMB2_Close_Request(FileId=FileId)
863        resp = self.ins.sr1(pkt, verbose=0, timeout=self.timeout)
864        if not resp:
865            raise ValueError("CloseRequest timed out !")
866        if SMB2_Close_Response not in resp:
867            raise ValueError("Failed CloseRequest ! %s" % resp.NTStatus)
868
869    def read_request(self, FileId, Length, Offset=0):
870        """
871        Read request
872        """
873        resp = self.ins.sr1(
874            SMB2_Read_Request(
875                FileId=FileId,
876                Length=Length,
877                Offset=Offset,
878            ),
879            verbose=0,
880            timeout=self.timeout,
881        )
882        if not resp:
883            raise ValueError("ReadRequest timed out !")
884        if SMB2_Read_Response not in resp:
885            raise ValueError("Failed ReadRequest ! %s" % resp.NTStatus)
886        return resp.Data
887
888    def write_request(self, Data, FileId, Offset=0):
889        """
890        Write request
891        """
892        resp = self.ins.sr1(
893            SMB2_Write_Request(
894                FileId=FileId,
895                Data=Data,
896                Offset=Offset,
897            ),
898            verbose=0,
899            timeout=self.timeout,
900        )
901        if not resp:
902            raise ValueError("WriteRequest timed out !")
903        if SMB2_Write_Response not in resp:
904            raise ValueError("Failed WriteRequest ! %s" % resp.NTStatus)
905        return resp.Count
906
907    def query_directory(self, FileId, FileName="*"):
908        """
909        Query the Directory with FileId
910        """
911        results = []
912        Flags = "SMB2_RESTART_SCANS"
913        while True:
914            pkt = SMB2_Query_Directory_Request(
915                FileInformationClass="FileIdBothDirectoryInformation",
916                FileId=FileId,
917                FileName=FileName,
918                Flags=Flags,
919            )
920            resp = self.ins.sr1(pkt, verbose=0, timeout=self.timeout)
921            Flags = 0  # only the first one is RESTART_SCANS
922            if not resp:
923                raise ValueError("QueryDirectory timed out !")
924            if SMB2_Error_Response in resp:
925                break
926            elif SMB2_Query_Directory_Response not in resp:
927                raise ValueError("Failed QueryDirectory ! %s" % resp.NTStatus)
928            res = FileIdBothDirectoryInformation(resp.Output)
929            results.extend(
930                [
931                    (
932                        x.FileName,
933                        x.FileAttributes,
934                        x.EndOfFile,
935                        x.LastWriteTime,
936                    )
937                    for x in res.files
938                ]
939            )
940        return results
941
942    def query_info(self, FileId, InfoType, FileInfoClass, AdditionalInformation=0):
943        """
944        Query the Info
945        """
946        pkt = SMB2_Query_Info_Request(
947            InfoType=InfoType,
948            FileInfoClass=FileInfoClass,
949            OutputBufferLength=65535,
950            FileId=FileId,
951            AdditionalInformation=AdditionalInformation,
952        )
953        resp = self.ins.sr1(pkt, verbose=0, timeout=self.timeout)
954        if not resp:
955            raise ValueError("QueryInfo timed out !")
956        if SMB2_Query_Info_Response not in resp:
957            raise ValueError("Failed QueryInfo ! %s" % resp.NTStatus)
958        return resp.Output
959
960    def changenotify(self, FileId):
961        """
962        Register change notify
963        """
964        pkt = SMB2_Change_Notify_Request(
965            Flags="SMB2_WATCH_TREE",
966            OutputBufferLength=65535,
967            FileId=FileId,
968            CompletionFilter=0x0FFF,
969        )
970        # we can wait forever, not a problem in this one
971        resp = self.ins.sr1(pkt, verbose=0, chainCC=True)
972        if SMB2_Change_Notify_Response not in resp:
973            raise ValueError("Failed ChangeNotify ! %s" % resp.NTStatus)
974        return resp.Output
975
976
977class SMB_RPC_SOCKET(ObjectPipe, SMB_SOCKET):
978    """
979    Extends SMB_SOCKET (which is a wrapper over SMB_Client.smblink) to send
980    DCE/RPC messages (bind, reqs, etc.)
981
982    This is usable as a normal SuperSocket (sr1, etc.) and performs the
983    wrapping of the DCE/RPC messages into SMB2_Write/Read packets.
984    """
985
986    def __init__(self, smbsock, use_ioctl=True, timeout=3):
987        self.use_ioctl = use_ioctl
988        ObjectPipe.__init__(self, "SMB_RPC_SOCKET")
989        SMB_SOCKET.__init__(self, smbsock, timeout=timeout)
990
991    def open_pipe(self, name):
992        self.PipeFileId = self.create_request(name, mode="rw", type="pipe")
993
994    def close_pipe(self):
995        self.close_request(self.PipeFileId)
996        self.PipeFileId = None
997
998    def send(self, x):
999        """
1000        Internal ObjectPipe function.
1001        """
1002        # Reminder: this class is an ObjectPipe, it's just a queue
1003        if self.use_ioctl:
1004            # Use IOCTLRequest
1005            pkt = SMB2_IOCTL_Request(
1006                FileId=self.PipeFileId,
1007                Flags="SMB2_0_IOCTL_IS_FSCTL",
1008                CtlCode="FSCTL_PIPE_TRANSCEIVE",
1009            )
1010            pkt.Input = bytes(x)
1011            resp = self.ins.sr1(pkt, verbose=0)
1012            if SMB2_IOCTL_Response not in resp:
1013                raise ValueError("Failed reading IOCTL_Response ! %s" % resp.NTStatus)
1014            data = bytes(resp.Output)
1015            # Handle BUFFER_OVERFLOW (big DCE/RPC response)
1016            while resp.NTStatus == "STATUS_BUFFER_OVERFLOW":
1017                # Retrieve DCE/RPC full size
1018                resp = self.ins.sr1(
1019                    SMB2_Read_Request(
1020                        FileId=self.PipeFileId,
1021                    ),
1022                    verbose=0,
1023                )
1024                data += resp.Data
1025            super(SMB_RPC_SOCKET, self).send(data)
1026        else:
1027            # Use WriteRequest/ReadRequest
1028            pkt = SMB2_Write_Request(
1029                FileId=self.PipeFileId,
1030            )
1031            pkt.Data = bytes(x)
1032            # We send the Write Request
1033            resp = self.ins.sr1(pkt, verbose=0)
1034            if SMB2_Write_Response not in resp:
1035                raise ValueError("Failed sending WriteResponse ! %s" % resp.NTStatus)
1036            # We send a Read Request afterwards
1037            resp = self.ins.sr1(
1038                SMB2_Read_Request(
1039                    FileId=self.PipeFileId,
1040                ),
1041                verbose=0,
1042            )
1043            if SMB2_Read_Response not in resp:
1044                raise ValueError("Failed reading ReadResponse ! %s" % resp.NTStatus)
1045            data = bytes(resp.Data)
1046            # Handle BUFFER_OVERFLOW (big DCE/RPC response)
1047            while resp.NTStatus == "STATUS_BUFFER_OVERFLOW":
1048                # Retrieve DCE/RPC full size
1049                resp = self.ins.sr1(
1050                    SMB2_Read_Request(
1051                        FileId=self.PipeFileId,
1052                    ),
1053                    verbose=0,
1054                )
1055                data += resp.Data
1056            super(SMB_RPC_SOCKET, self).send(data)
1057
1058    def close(self):
1059        SMB_SOCKET.close(self)
1060        ObjectPipe.close(self)
1061
1062
1063@conf.commands.register
1064class smbclient(CLIUtil):
1065    r"""
1066    A simple smbclient CLI
1067
1068    :param target: can be a hostname, the IPv4 or the IPv6 to connect to
1069    :param UPN: the upn to use (DOMAIN/USER, DOMAIN\USER, USER@DOMAIN or USER)
1070    :param guest: use guest mode (over NTLM)
1071    :param ssp: if provided, use this SSP for auth.
1072    :param kerberos: if available, whether to use Kerberos or not
1073    :param kerberos_required: require kerberos
1074    :param port: the TCP port. default 445
1075    :param password: (string) if provided, used for auth
1076    :param HashNt: (bytes) if provided, used for auth (NTLM)
1077    :param ST: if provided, the service ticket to use (Kerberos)
1078    :param KEY: if provided, the session key associated to the ticket (Kerberos)
1079    :param cli: CLI mode (default True). False to use for scripting
1080    """
1081
1082    def __init__(
1083        self,
1084        target: str,
1085        UPN: str = None,
1086        password: str = None,
1087        guest: bool = False,
1088        kerberos: bool = True,
1089        kerberos_required: bool = False,
1090        HashNt: str = None,
1091        port: int = 445,
1092        timeout: int = 2,
1093        debug: int = 0,
1094        ssp=None,
1095        ST=None,
1096        KEY=None,
1097        cli=True,
1098        # SMB arguments
1099        **kwargs,
1100    ):
1101        if cli:
1102            self._depcheck()
1103        hostname = None
1104        # Check if target is a hostname / Check IP
1105        if ":" in target:
1106            family = socket.AF_INET6
1107            if not valid_ip6(target):
1108                hostname = target
1109            target = str(Net6(target))
1110        else:
1111            family = socket.AF_INET
1112            if not valid_ip(target):
1113                hostname = target
1114            target = str(Net(target))
1115        assert UPN or ssp or guest, "Either UPN, ssp or guest must be provided !"
1116        # Do we need to build a SSP?
1117        if ssp is None:
1118            # Create the SSP (only if not guest mode)
1119            if not guest:
1120                # Check UPN
1121                try:
1122                    _, realm = _parse_upn(UPN)
1123                    if realm == ".":
1124                        # Local
1125                        kerberos = False
1126                except ValueError:
1127                    # not a UPN: NTLM
1128                    kerberos = False
1129                # Do we need to ask the password?
1130                if HashNt is None and password is None and ST is None:
1131                    # yes.
1132                    from prompt_toolkit import prompt
1133
1134                    password = prompt("Password: ", is_password=True)
1135                ssps = []
1136                # Kerberos
1137                if kerberos and hostname:
1138                    if ST is None:
1139                        resp = krb_as_and_tgs(
1140                            upn=UPN,
1141                            spn="cifs/%s" % hostname,
1142                            password=password,
1143                            debug=debug,
1144                        )
1145                        if resp is not None:
1146                            ST, KEY = resp.tgsrep.ticket, resp.sessionkey
1147                    if ST:
1148                        ssps.append(KerberosSSP(UPN=UPN, ST=ST, KEY=KEY, debug=debug))
1149                    elif kerberos_required:
1150                        raise ValueError(
1151                            "Kerberos required but target isn't a hostname !"
1152                        )
1153                elif kerberos_required:
1154                    raise ValueError(
1155                        "Kerberos required but domain not specified in the UPN, "
1156                        "or target isn't a hostname !"
1157                    )
1158                # NTLM
1159                if not kerberos_required:
1160                    if HashNt is None and password is not None:
1161                        HashNt = MD4le(password)
1162                    ssps.append(NTLMSSP(UPN=UPN, HASHNT=HashNt))
1163                # Build the SSP
1164                ssp = SPNEGOSSP(ssps)
1165            else:
1166                # Guest mode
1167                ssp = None
1168        # Open socket
1169        sock = socket.socket(family, socket.SOCK_STREAM)
1170        # Configure socket for SMB:
1171        # - TCP KEEPALIVE, TCP_KEEPIDLE and TCP_KEEPINTVL. Against a Windows server this
1172        #   isn't necessary, but samba kills the socket VERY fast otherwise.
1173        # - set TCP_NODELAY to disable Nagle's algorithm (we're streaming data)
1174        sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
1175        sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
1176        sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 10)
1177        sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10)
1178        # Timeout & connect
1179        sock.settimeout(timeout)
1180        sock.connect((target, port))
1181        self.extra_create_options = []
1182        # Wrap with the automaton
1183        self.timeout = timeout
1184        kwargs.setdefault("SERVER_NAME", target)
1185        self.sock = SMB_Client.from_tcpsock(
1186            sock,
1187            ssp=ssp,
1188            debug=debug,
1189            **kwargs,
1190        )
1191        try:
1192            # Wrap with SMB_SOCKET
1193            self.smbsock = SMB_SOCKET(self.sock)
1194            # Wait for either the atmt to fail, or the smb_sock_ready to timeout
1195            _t = time.time()
1196            while True:
1197                if self.sock.atmt.smb_sock_ready.is_set():
1198                    # yay
1199                    break
1200                if not self.sock.atmt.isrunning():
1201                    status = self.sock.atmt.get("Status")
1202                    raise Scapy_Exception(
1203                        "%s with status %s"
1204                        % (
1205                            self.sock.atmt.state.state,
1206                            STATUS_ERREF.get(status, hex(status)),
1207                        )
1208                    )
1209                if time.time() - _t > timeout:
1210                    self.sock.close()
1211                    raise TimeoutError("The SMB handshake timed out.")
1212                time.sleep(0.1)
1213        except Exception:
1214            # Something bad happened, end the socket/automaton
1215            self.sock.close()
1216            raise
1217
1218        # For some usages, we will also need the RPC wrapper
1219        from scapy.layers.msrpce.rpcclient import DCERPC_Client
1220
1221        self.rpcclient = DCERPC_Client.from_smblink(self.sock, ndr64=False, verb=False)
1222        # We have a valid smb connection !
1223        print(
1224            "%s authentication successful using %s%s !"
1225            % (
1226                SMB_DIALECTS.get(
1227                    self.smbsock.session.Dialect,
1228                    "SMB %s" % self.smbsock.session.Dialect,
1229                ),
1230                repr(self.smbsock.session.sspcontext),
1231                " as GUEST" if self.sock.atmt.IsGuest else "",
1232            )
1233        )
1234        # Now define some variables for our CLI
1235        self.pwd = pathlib.PureWindowsPath("/")
1236        self.localpwd = pathlib.Path(".").resolve()
1237        self.current_tree = None
1238        self.ls_cache = {}  # cache the listing of the current directory
1239        self.sh_cache = []  # cache the shares
1240        # Start CLI
1241        if cli:
1242            self.loop(debug=debug)
1243
1244    def ps1(self):
1245        return r"smb: \%s> " % self.normalize_path(self.pwd)
1246
1247    def close(self):
1248        print("Connection closed")
1249        self.smbsock.close()
1250
1251    def _require_share(self, silent=False):
1252        if self.current_tree is None:
1253            if not silent:
1254                print("No share selected ! Try 'shares' then 'use'.")
1255            return True
1256
1257    def collapse_path(self, path):
1258        # the amount of pathlib.wtf you need to do to resolve .. on all platforms
1259        # is ridiculous
1260        return pathlib.PureWindowsPath(os.path.normpath(path.as_posix()))
1261
1262    def normalize_path(self, path):
1263        """
1264        Normalize path for CIFS usage
1265        """
1266        return str(self.collapse_path(path)).lstrip("\\")
1267
1268    @CLIUtil.addcommand()
1269    def shares(self):
1270        """
1271        List the shares available
1272        """
1273        # Poll cache
1274        if self.sh_cache:
1275            return self.sh_cache
1276        # One of the 'hardest' considering it's an RPC
1277        self.rpcclient.open_smbpipe("srvsvc")
1278        self.rpcclient.bind(find_dcerpc_interface("srvsvc"))
1279        req = NetrShareEnum_Request(
1280            InfoStruct=LPSHARE_ENUM_STRUCT(
1281                Level=1,
1282                ShareInfo=NDRUnion(
1283                    tag=1,
1284                    value=SHARE_INFO_1_CONTAINER(Buffer=None),
1285                ),
1286            ),
1287            PreferedMaximumLength=0xFFFFFFFF,
1288        )
1289        resp = self.rpcclient.sr1_req(req, timeout=self.timeout)
1290        self.rpcclient.close_smbpipe()
1291        if not isinstance(resp, NetrShareEnum_Response):
1292            raise ValueError("NetrShareEnum_Request failed !")
1293        results = []
1294        for share in resp.valueof("InfoStruct.ShareInfo.Buffer"):
1295            shi1_type = share.valueof("shi1_type") & 0x0FFFFFFF
1296            results.append(
1297                (
1298                    share.valueof("shi1_netname").decode(),
1299                    SRVSVC_SHARE_TYPES.get(shi1_type, shi1_type),
1300                    share.valueof("shi1_remark").decode(),
1301                )
1302            )
1303        self.sh_cache = results  # cache
1304        return results
1305
1306    @CLIUtil.addoutput(shares)
1307    def shares_output(self, results):
1308        """
1309        Print the output of 'shares'
1310        """
1311        print(pretty_list(results, [("ShareName", "ShareType", "Comment")]))
1312
1313    @CLIUtil.addcommand()
1314    def use(self, share):
1315        """
1316        Open a share
1317        """
1318        self.current_tree = self.smbsock.tree_connect(share)
1319        self.pwd = pathlib.PureWindowsPath("/")
1320        self.ls_cache.clear()
1321
1322    @CLIUtil.addcomplete(use)
1323    def use_complete(self, share):
1324        """
1325        Auto-complete 'use'
1326        """
1327        return [
1328            x[0] for x in self.shares() if x[0].startswith(share) and x[0] != "IPC$"
1329        ]
1330
1331    def _parsepath(self, arg, remote=True):
1332        """
1333        Parse a path. Returns the parent folder and file name
1334        """
1335        # Find parent directory if it exists
1336        elt = (pathlib.PureWindowsPath if remote else pathlib.Path)(arg)
1337        eltpar = (pathlib.PureWindowsPath if remote else pathlib.Path)(".")
1338        eltname = elt.name
1339        if arg.endswith("/") or arg.endswith("\\"):
1340            eltpar = elt
1341            eltname = ""
1342        elif elt.parent and elt.parent.name or elt.is_absolute():
1343            eltpar = elt.parent
1344        return eltpar, eltname
1345
1346    def _fs_complete(self, arg, cond=None):
1347        """
1348        Return a listing of the remote files for completion purposes
1349        """
1350        if cond is None:
1351            cond = lambda _: True
1352        eltpar, eltname = self._parsepath(arg)
1353        # ls in that directory
1354        try:
1355            files = self.ls(parent=eltpar)
1356        except ValueError:
1357            return []
1358        return [
1359            str(eltpar / x[0])
1360            for x in files
1361            if (
1362                x[0].lower().startswith(eltname.lower())
1363                and x[0] not in [".", ".."]
1364                and cond(x[1])
1365            )
1366        ]
1367
1368    def _dir_complete(self, arg):
1369        """
1370        Return a directories of remote files for completion purposes
1371        """
1372        results = self._fs_complete(
1373            arg,
1374            cond=lambda x: x.FILE_ATTRIBUTE_DIRECTORY,
1375        )
1376        if len(results) == 1 and results[0].startswith(arg):
1377            # skip through folders
1378            return [results[0] + "\\"]
1379        return results
1380
1381    @CLIUtil.addcommand(spaces=True)
1382    def ls(self, parent=None):
1383        """
1384        List the files in the remote directory
1385        -t: sort by timestamp
1386        -S: sort by size
1387        -r: reverse while sorting
1388        """
1389        if self._require_share():
1390            return
1391        # Get pwd of the ls
1392        pwd = self.pwd
1393        if parent is not None:
1394            pwd /= parent
1395        pwd = self.normalize_path(pwd)
1396        # Poll the cache
1397        if self.ls_cache and pwd in self.ls_cache:
1398            return self.ls_cache[pwd]
1399        self.smbsock.set_TID(self.current_tree)
1400        # Open folder
1401        fileId = self.smbsock.create_request(
1402            pwd,
1403            type="folder",
1404            extra_create_options=self.extra_create_options,
1405        )
1406        # Query the folder
1407        files = self.smbsock.query_directory(fileId)
1408        # Close the folder
1409        self.smbsock.close_request(fileId)
1410        self.ls_cache[pwd] = files  # Store cache
1411        return files
1412
1413    @CLIUtil.addoutput(ls)
1414    def ls_output(self, results, *, t=False, S=False, r=False):
1415        """
1416        Print the output of 'ls'
1417        """
1418        fld = UTCTimeField(
1419            "", None, fmt="<Q", epoch=[1601, 1, 1, 0, 0, 0], custom_scaling=1e7
1420        )
1421        if t:
1422            # Sort by time
1423            results.sort(key=lambda x: -x[3])
1424        if S:
1425            # Sort by size
1426            results.sort(key=lambda x: -x[2])
1427        if r:
1428            # Reverse sort
1429            results = results[::-1]
1430        results = [
1431            (
1432                x[0],
1433                "+".join(y.lstrip("FILE_ATTRIBUTE_") for y in str(x[1]).split("+")),
1434                human_size(x[2]),
1435                fld.i2repr(None, x[3]),
1436            )
1437            for x in results
1438        ]
1439        print(
1440            pretty_list(
1441                results,
1442                [("FileName", "FileAttributes", "EndOfFile", "LastWriteTime")],
1443                sortBy=None,
1444            )
1445        )
1446
1447    @CLIUtil.addcomplete(ls)
1448    def ls_complete(self, folder):
1449        """
1450        Auto-complete ls
1451        """
1452        if self._require_share(silent=True):
1453            return []
1454        return self._dir_complete(folder)
1455
1456    @CLIUtil.addcommand(spaces=True)
1457    def cd(self, folder):
1458        """
1459        Change the remote current directory
1460        """
1461        if self._require_share():
1462            return
1463        if not folder:
1464            # show mode
1465            return str(self.pwd)
1466        self.pwd /= folder
1467        self.pwd = self.collapse_path(self.pwd)
1468        self.ls_cache.clear()
1469
1470    @CLIUtil.addcomplete(cd)
1471    def cd_complete(self, folder):
1472        """
1473        Auto-complete cd
1474        """
1475        if self._require_share(silent=True):
1476            return []
1477        return self._dir_complete(folder)
1478
1479    def _lfs_complete(self, arg, cond):
1480        """
1481        Return a listing of local files for completion purposes
1482        """
1483        eltpar, eltname = self._parsepath(arg, remote=False)
1484        eltpar = self.localpwd / eltpar
1485        return [
1486            # trickery so that ../<TAB> works
1487            str(eltpar / x.name)
1488            for x in eltpar.resolve().glob("*")
1489            if (x.name.lower().startswith(eltname.lower()) and cond(x))
1490        ]
1491
1492    @CLIUtil.addoutput(cd)
1493    def cd_output(self, result):
1494        """
1495        Print the output of 'cd'
1496        """
1497        if result:
1498            print(result)
1499
1500    @CLIUtil.addcommand()
1501    def lls(self):
1502        """
1503        List the files in the local directory
1504        """
1505        return list(self.localpwd.glob("*"))
1506
1507    @CLIUtil.addoutput(lls)
1508    def lls_output(self, results):
1509        """
1510        Print the output of 'lls'
1511        """
1512        results = [
1513            (
1514                x.name,
1515                human_size(stat.st_size),
1516                time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(stat.st_mtime)),
1517            )
1518            for x, stat in ((x, x.stat()) for x in results)
1519        ]
1520        print(
1521            pretty_list(results, [("FileName", "File Size", "Last Modification Time")])
1522        )
1523
1524    @CLIUtil.addcommand(spaces=True)
1525    def lcd(self, folder):
1526        """
1527        Change the local current directory
1528        """
1529        if not folder:
1530            # show mode
1531            return str(self.localpwd)
1532        self.localpwd /= folder
1533        self.localpwd = self.localpwd.resolve()
1534
1535    @CLIUtil.addcomplete(lcd)
1536    def lcd_complete(self, folder):
1537        """
1538        Auto-complete lcd
1539        """
1540        return self._lfs_complete(folder, lambda x: x.is_dir())
1541
1542    @CLIUtil.addoutput(lcd)
1543    def lcd_output(self, result):
1544        """
1545        Print the output of 'lcd'
1546        """
1547        if result:
1548            print(result)
1549
1550    def _get_file(self, file, fd):
1551        """
1552        Gets the file bytes from a remote host
1553        """
1554        # Get pwd of the ls
1555        fpath = self.pwd / file
1556        self.smbsock.set_TID(self.current_tree)
1557        # Open file
1558        fileId = self.smbsock.create_request(
1559            self.normalize_path(fpath),
1560            type="file",
1561            extra_create_options=[
1562                "FILE_SEQUENTIAL_ONLY",
1563            ]
1564            + self.extra_create_options,
1565        )
1566        # Get the file size
1567        info = FileAllInformation(
1568            self.smbsock.query_info(
1569                FileId=fileId,
1570                InfoType="SMB2_0_INFO_FILE",
1571                FileInfoClass="FileAllInformation",
1572            )
1573        )
1574        length = info.StandardInformation.EndOfFile
1575        offset = 0
1576        # Read the file
1577        while length:
1578            lengthRead = min(self.sock.atmt.MaxReadSize, length)
1579            fd.write(
1580                self.smbsock.read_request(fileId, Length=lengthRead, Offset=offset)
1581            )
1582            offset += lengthRead
1583            length -= lengthRead
1584        # Close the file
1585        self.smbsock.close_request(fileId)
1586        return offset
1587
1588    def _send_file(self, fname, fd):
1589        """
1590        Send the file bytes to a remote host
1591        """
1592        # Get destination file
1593        fpath = self.pwd / fname
1594        self.smbsock.set_TID(self.current_tree)
1595        # Open file
1596        fileId = self.smbsock.create_request(
1597            self.normalize_path(fpath),
1598            type="file",
1599            mode="w",
1600            extra_create_options=self.extra_create_options,
1601        )
1602        # Send the file
1603        offset = 0
1604        while True:
1605            data = fd.read(self.sock.atmt.MaxWriteSize)
1606            if not data:
1607                # end of file
1608                break
1609            offset += self.smbsock.write_request(
1610                Data=data,
1611                FileId=fileId,
1612                Offset=offset,
1613            )
1614        # Close the file
1615        self.smbsock.close_request(fileId)
1616        return offset
1617
1618    def _getr(self, directory, _root, _verb=True):
1619        """
1620        Internal recursive function to get a directory
1621
1622        :param directory: the remote directory to get
1623        :param _root: locally, the directory to store any found files
1624        """
1625        size = 0
1626        if not _root.exists():
1627            _root.mkdir()
1628        # ls the directory
1629        for x in self.ls(parent=directory):
1630            if x[0] in [".", ".."]:
1631                # Discard . and ..
1632                continue
1633            remote = directory / x[0]
1634            local = _root / x[0]
1635            try:
1636                if x[1].FILE_ATTRIBUTE_DIRECTORY:
1637                    # Sub-directory
1638                    size += self._getr(remote, local)
1639                else:
1640                    # Sub-file
1641                    size += self.get(remote, local)[1]
1642                if _verb:
1643                    print(remote)
1644            except ValueError as ex:
1645                if _verb:
1646                    print(conf.color_theme.red(remote), "->", str(ex))
1647        return size
1648
1649    @CLIUtil.addcommand(spaces=True, globsupport=True)
1650    def get(self, file, _dest=None, _verb=True, *, r=False):
1651        """
1652        Retrieve a file
1653        -r: recursively download a directory
1654        """
1655        if self._require_share():
1656            return
1657        if r:
1658            dirpar, dirname = self._parsepath(file)
1659            return file, self._getr(
1660                dirpar / dirname,  # Remotely
1661                _root=self.localpwd / dirname,  # Locally
1662                _verb=_verb,
1663            )
1664        else:
1665            fname = pathlib.PureWindowsPath(file).name
1666            # Write the buffer
1667            if _dest is None:
1668                _dest = self.localpwd / fname
1669            with _dest.open("wb") as fd:
1670                size = self._get_file(file, fd)
1671            return fname, size
1672
1673    @CLIUtil.addoutput(get)
1674    def get_output(self, info):
1675        """
1676        Print the output of 'get'
1677        """
1678        print("Retrieved '%s' of size %s" % (info[0], human_size(info[1])))
1679
1680    @CLIUtil.addcomplete(get)
1681    def get_complete(self, file):
1682        """
1683        Auto-complete get
1684        """
1685        if self._require_share(silent=True):
1686            return []
1687        return self._fs_complete(file)
1688
1689    @CLIUtil.addcommand(spaces=True, globsupport=True)
1690    def cat(self, file):
1691        """
1692        Print a file
1693        """
1694        if self._require_share():
1695            return
1696        # Write the buffer to buffer
1697        buf = io.BytesIO()
1698        self._get_file(file, buf)
1699        return buf.getvalue()
1700
1701    @CLIUtil.addoutput(cat)
1702    def cat_output(self, result):
1703        """
1704        Print the output of 'cat'
1705        """
1706        print(result.decode(errors="backslashreplace"))
1707
1708    @CLIUtil.addcomplete(cat)
1709    def cat_complete(self, file):
1710        """
1711        Auto-complete cat
1712        """
1713        if self._require_share(silent=True):
1714            return []
1715        return self._fs_complete(file)
1716
1717    @CLIUtil.addcommand(spaces=True)
1718    def put(self, file):
1719        """
1720        Upload a file
1721        """
1722        if self._require_share():
1723            return
1724        local_file = self.localpwd / file
1725        if local_file.is_dir():
1726            # Directory
1727            raise ValueError("put on dir not impl")
1728        else:
1729            fname = pathlib.Path(file).name
1730            with local_file.open("rb") as fd:
1731                size = self._send_file(fname, fd)
1732        self.ls_cache.clear()
1733        return fname, size
1734
1735    @CLIUtil.addcomplete(put)
1736    def put_complete(self, folder):
1737        """
1738        Auto-complete put
1739        """
1740        return self._lfs_complete(folder, lambda x: not x.is_dir())
1741
1742    @CLIUtil.addcommand(spaces=True)
1743    def rm(self, file):
1744        """
1745        Delete a file
1746        """
1747        if self._require_share():
1748            return
1749        # Get pwd of the ls
1750        fpath = self.pwd / file
1751        self.smbsock.set_TID(self.current_tree)
1752        # Open file
1753        fileId = self.smbsock.create_request(
1754            self.normalize_path(fpath),
1755            type="file",
1756            mode="d",
1757            extra_create_options=self.extra_create_options,
1758        )
1759        # Close the file
1760        self.smbsock.close_request(fileId)
1761        self.ls_cache.clear()
1762        return fpath.name
1763
1764    @CLIUtil.addcomplete(rm)
1765    def rm_complete(self, file):
1766        """
1767        Auto-complete rm
1768        """
1769        if self._require_share(silent=True):
1770            return []
1771        return self._fs_complete(file)
1772
1773    @CLIUtil.addcommand()
1774    def backup(self):
1775        """
1776        Turn on or off backup intent
1777        """
1778        if "FILE_OPEN_FOR_BACKUP_INTENT" in self.extra_create_options:
1779            print("Backup Intent: Off")
1780            self.extra_create_options.remove("FILE_OPEN_FOR_BACKUP_INTENT")
1781        else:
1782            print("Backup Intent: On")
1783            self.extra_create_options.append("FILE_OPEN_FOR_BACKUP_INTENT")
1784
1785
1786if __name__ == "__main__":
1787    from scapy.utils import AutoArgparse
1788
1789    AutoArgparse(smbclient)
1790