1# -*- coding: utf-8 -*-
2# Copyright: (c) 2019, Jordan Borean (@jborean93) <jborean93@gmail.com>
3# MIT License (see LICENSE or https://opensource.org/licenses/MIT)
4
5import logging
6
7from collections import (
8    OrderedDict,
9)
10
11from smbprotocol import (
12    Dialects,
13)
14
15from smbprotocol.exceptions import (
16    FileClosed,
17    InvalidDeviceRequest,
18    NotSupported,
19    SMBException,
20)
21
22from smbprotocol.header import (
23    Commands,
24)
25
26from smbprotocol.ioctl import (
27    CtlCode,
28    IOCTLFlags,
29    SMB2IOCTLRequest,
30    SMB2IOCTLResponse,
31    SMB2ValidateNegotiateInfoRequest,
32    SMB2ValidateNegotiateInfoResponse,
33)
34
35from smbprotocol.structure import (
36    BytesField,
37    EnumField,
38    FlagField,
39    IntField,
40    Structure,
41)
42
43log = logging.getLogger(__name__)
44
45
46class TreeFlags(object):
47    """
48    [MS-SMB2] v53.0 2017-09-15
49
50    2.2.9 SMB2 TREE_CONNECT Response Flags
51    Flags used in SMB 3.1.1  to indicate how to process the operation.
52    """
53    SMB2_TREE_CONNECT_FLAG_CLUSTER_RECONNECT = 0x0004
54    SMB2_TREE_CONNECT_FLAG_REDIRECT_TO_OWNER = 0x0002
55    SMB2_TREE_CONNECT_FLAG_EXTENSION_PRESENT = 0x0001
56
57
58class ShareType(object):
59    """
60    [MS-SMB2] v53.0 2017-09-15
61
62    2.2.10 SMB2 TREE_CONNECT Response Capabilities
63    The type of share being accessed
64    """
65    SMB2_SHARE_TYPE_DISK = 0x01
66    SMB2_SHARE_TYPE_PIPE = 0x02
67    SMB2_SHARE_TYPE_PRINT = 0x03
68
69
70class ShareFlags(object):
71    """
72    [MS-SMB2] v53.0 2017-09-15
73
74    2.2.10 SMB2 TREE_CONNECT Response Capabilities
75    Properties for the share
76    """
77    SMB2_SHAREFLAG_MANUAL_CACHING = 0x00000000
78    SMB2_SHAREFLAG_AUTO_CACHING = 0x00000010
79    SMB2_SHAREFLAG_VDO_CACHING = 0x00000020
80    SMB2_SHAREFLAG_NO_CACHING = 0x00000030
81    SMB2_SHAREFLAG_DFS = 0x00000001
82    SMB2_SHAREFLAG_DFS_ROOT = 0x00000002
83    SMB2_SHAREFLAG_RESTRICT_EXCLUSIVE_OPENS = 0x00000100
84    SMB2_SHAREFLAG_FORCE_SHARED_DELETE = 0x00000200
85    SMB2_SHAREFLAG_ALLOW_NAMESPACE_CACHING = 0x00000400
86    SMB2_SHAREFLAG_ACCESS_BASED_DIRECTORY_ENUM = 0x00000800
87    SMB2_SHAREFLAG_FORCE_LEVELII_OPLOCK = 0x00001000
88    SMB2_SHAREFLAG_ENABLE_HASH_V1 = 0x00002000
89    SMB2_SHAREFLAG_ENABLE_HASH_V2 = 0x00004000
90    SMB2_SHAREFLAG_ENCRYPT_DATA = 0x00008000
91    SMB2_SHAREFLAG_IDENTITY_REMOTING = 0x00040000
92
93
94class ShareCapabilities(object):
95    """
96    [MS-SMB2] v53.0 2017-09-15
97
98    2.2.10 SMB2 TREE_CONNECT Response Capabilities
99    Indicates various capabilities for a share
100    """
101    SMB2_SHARE_CAP_DFS = 0x00000008
102    SMB2_SHARE_CAP_CONTINUOUS_AVAILABILITY = 0x00000010
103    SMB2_SHARE_CAP_SCALEOUT = 0x00000020
104    SMB2_SHARE_CAP_CLUSTER = 0x00000040
105    SMB2_SHARE_CAP_ASYMMETRIC = 0x00000080
106    SMB2_SHARE_CAP_REDIRECT_TO_OWNER = 0x00000100
107
108
109class SMB2TreeConnectRequest(Structure):
110    """
111    [MS-SMB2] v53.0 2017-09-15
112
113    2.2.9 SMB2 TREE_CONNECT Request
114    Sent by the client to request access to a particular share on the server
115    """
116    COMMAND = Commands.SMB2_TREE_CONNECT
117
118    def __init__(self):
119        self.fields = OrderedDict([
120            ('structure_size', IntField(
121                size=2,
122                default=9
123            )),
124            ('flags', FlagField(
125                size=2,
126                flag_type=TreeFlags,
127            )),
128            ('path_offset', IntField(
129                size=2,
130                default=64 + 8,
131            )),
132            ('path_length', IntField(
133                size=2,
134                default=lambda s: len(s['buffer']),
135            )),
136            ('buffer', BytesField(
137                size=lambda s: s['path_length'].get_value()
138            ))
139        ])
140        super(SMB2TreeConnectRequest, self).__init__()
141
142
143class SMB2TreeConnectResponse(Structure):
144    """
145    [MS-SMB2] v53.0 2017-09-15
146
147    2.2.10 SMB2 TREE_CONNECT Response
148    Sent by the server when an SMB2 TREE_CONNECT request is processed
149    successfully.
150    """
151    COMMAND = Commands.SMB2_TREE_CONNECT
152
153    def __init__(self):
154        self.fields = OrderedDict([
155            ('structure_size', IntField(
156                size=2,
157                default=16
158            )),
159            ('share_type', EnumField(
160                size=1,
161                enum_type=ShareType,
162            )),
163            ('reserved', IntField(size=1)),
164            ('share_flags', FlagField(
165                size=4,
166                flag_type=ShareFlags,
167            )),
168            ('capabilities', FlagField(
169                size=4,
170                flag_type=ShareCapabilities,
171            )),
172            ('maximal_access', IntField(size=4))
173        ])
174        super(SMB2TreeConnectResponse, self).__init__()
175
176
177class SMB2TreeDisconnect(Structure):
178    """
179    [MS-SMB2] v53.0 2017-09-15
180
181    2.2.11/12 SMB2 TREE_DISCONNECT Request and Response
182    Sent by the client to request that the tree connect specific by tree_id in
183    the header is disconnected.
184    """
185    COMMAND = Commands.SMB2_TREE_DISCONNECT
186
187    def __init__(self):
188        self.fields = OrderedDict([
189            ('structure_size', IntField(
190                size=2,
191                default=4,
192            )),
193            ('reserved', IntField(size=2))
194        ])
195        super(SMB2TreeDisconnect, self).__init__()
196
197
198class TreeConnect(object):
199
200    def __init__(self, session, share_name):
201        """
202        [MS-SMB2] v53.0 2017-09-15
203
204        3.2.1.4 Per Tree Connect
205        Attributes per Tree Connect (share connections)
206
207        :param session: The Session to connect to the tree with.
208        :param share_name: The name of the share, including the server name.
209        """
210        self._connected = False
211        self.open_table = {}
212
213        self.share_name = share_name
214        self.tree_connect_id = None
215        self.session = session
216        self.is_dfs_share = None
217
218        # SMB 3.x+
219        self.is_ca_share = None
220        self.encrypt_data = None
221        self.is_scaleout_share = None
222
223    def connect(self, require_secure_negotiate=True):
224        """
225        Connect to the share.
226
227        :param require_secure_negotiate: For Dialects 3.0 and 3.0.2, will
228            verify the negotiation parameters with the server to prevent
229            SMB downgrade attacks
230        """
231        log.info("Session: %s - Creating connection to share %s"
232                 % (self.session.username, self.share_name))
233        utf_share_name = self.share_name.encode('utf-16-le')
234        connect = SMB2TreeConnectRequest()
235        connect['buffer'] = utf_share_name
236
237        log.info("Session: %s - Sending Tree Connect message"
238                 % self.session.username)
239        log.debug(connect)
240        request = self.session.connection.send(connect,
241                                               sid=self.session.session_id)
242
243        log.info("Session: %s - Receiving Tree Connect response"
244                 % self.session.username)
245        response = self.session.connection.receive(request)
246        tree_response = SMB2TreeConnectResponse()
247        tree_response.unpack(response['data'].get_value())
248        log.debug(tree_response)
249
250        # https://msdn.microsoft.com/en-us/library/cc246687.aspx
251        self.tree_connect_id = response['tree_id'].get_value()
252        log.info("Session: %s - Created tree connection with ID %d"
253                 % (self.session.username, self.tree_connect_id))
254        self._connected = True
255        self.session.tree_connect_table[self.tree_connect_id] = self
256
257        capabilities = tree_response['capabilities']
258        self.is_dfs_share = capabilities.has_flag(
259            ShareCapabilities.SMB2_SHARE_CAP_DFS)
260        self.is_ca_share = capabilities.has_flag(
261            ShareCapabilities.SMB2_SHARE_CAP_CONTINUOUS_AVAILABILITY)
262
263        dialect = self.session.connection.dialect
264        if dialect >= Dialects.SMB_3_0_0 and \
265                self.session.connection.supports_encryption:
266            self.encrypt_data = tree_response['share_flags'].has_flag(
267                ShareFlags.SMB2_SHAREFLAG_ENCRYPT_DATA)
268
269            self.is_scaleout_share = capabilities.has_flag(
270                ShareCapabilities.SMB2_SHARE_CAP_SCALEOUT)
271
272        if require_secure_negotiate:
273            self._verify_dialect_negotiate()
274
275    def disconnect(self):
276        """
277        Disconnects the tree connection.
278        """
279        if not self._connected:
280            return
281
282        log.info("Session: %s, Tree: %s - Disconnecting from Tree Connect"
283                 % (self.session.username, self.share_name))
284
285        req = SMB2TreeDisconnect()
286        log.info("Session: %s, Tree: %s - Sending Tree Disconnect message"
287                 % (self.session.username, self.share_name))
288        log.debug(req)
289        request = self.session.connection.send(req,
290                                               sid=self.session.session_id,
291                                               tid=self.tree_connect_id)
292
293        log.info("Session: %s, Tree: %s - Receiving Tree Disconnect response"
294                 % (self.session.username, self.share_name))
295        res = self.session.connection.receive(request)
296        res_disconnect = SMB2TreeDisconnect()
297        res_disconnect.unpack(res['data'].get_value())
298        log.debug(res_disconnect)
299        self._connected = False
300        del self.session.tree_connect_table[self.tree_connect_id]
301
302    def _verify_dialect_negotiate(self):
303        log_header = "Session: %s, Tree: %s" \
304                     % (self.session.username, self.share_name)
305        log.info("%s - Running secure negotiate process" % log_header)
306
307        if not self.session.signing_key:
308            # This will only happen if we authenticated with the guest or anonymous user.
309            raise SMBException('Cannot verify negotiate information without a session signing key. Authenticate with '
310                               'a non-guest or anonymous account or set require_secure_negotiate=False to disable the '
311                               'negotiation info verification checks.')
312
313        dialect = self.session.connection.dialect
314        if dialect >= Dialects.SMB_3_1_1:
315            # SMB 3.1.1+ uses the negotiation info to generate the signing key so doesn't need this extra exchange.
316            return
317
318        ioctl_request = SMB2IOCTLRequest()
319        ioctl_request['ctl_code'] = \
320            CtlCode.FSCTL_VALIDATE_NEGOTIATE_INFO
321        ioctl_request['file_id'] = b"\xff" * 16
322
323        val_neg = SMB2ValidateNegotiateInfoRequest()
324        val_neg['capabilities'] = \
325            self.session.connection.client_capabilities
326        val_neg['guid'] = self.session.connection.client_guid
327        val_neg['security_mode'] = \
328            self.session.connection.client_security_mode
329        val_neg['dialects'] = \
330            self.session.connection.negotiated_dialects
331
332        ioctl_request['buffer'] = val_neg
333        ioctl_request['max_output_response'] = len(val_neg)
334        ioctl_request['flags'] = IOCTLFlags.SMB2_0_IOCTL_IS_FSCTL
335        log.info("%s - Sending Secure Negotiate Validation message"
336                 % log_header)
337        log.debug(ioctl_request)
338        request = self.session.connection.send(ioctl_request,
339                                               sid=self.session.session_id,
340                                               tid=self.tree_connect_id,
341                                               force_signature=True)
342
343        log.info("%s - Receiving secure negotiation response" % log_header)
344        try:
345            response = self.session.connection.receive(request)
346
347        except (FileClosed, InvalidDeviceRequest, NotSupported) as e:
348            # https://docs.microsoft.com/en-us/archive/blogs/openspecification/smb3-secure-dialect-negotiation
349            # Older dialects may respond with these exceptions, this is expected and we only want to fail if
350            # they are not signed. Check that header signature was signed, fail if it wasn't. The signature, if
351            # present, would have been verified when the connection received the data.
352            if e.header['signature'].get_value() == b'\x00' * 16:
353                raise
354
355            return
356
357        # If we received an actual response we want to validate the info provided matches with what was negotiated.
358        ioctl_resp = SMB2IOCTLResponse()
359        ioctl_resp.unpack(response['data'].get_value())
360        log.debug(ioctl_resp)
361
362        log.info("%s - Unpacking secure negotiate response info" % log_header)
363        val_resp = SMB2ValidateNegotiateInfoResponse()
364        val_resp.unpack(ioctl_resp['buffer'].get_value())
365        log.debug(val_resp)
366
367        self._verify("server capabilities",
368                     val_resp['capabilities'].get_value(),
369                     self.session.connection.server_capabilities.get_value())
370        self._verify("server guid",
371                     val_resp['guid'].get_value(),
372                     self.session.connection.server_guid)
373        self._verify("server security mode",
374                     val_resp['security_mode'].get_value(),
375                     self.session.connection.server_security_mode)
376        self._verify("server dialect",
377                     val_resp['dialect'].get_value(),
378                     self.session.connection.dialect)
379        log.info("Session: %d, Tree: %d - Secure negotiate complete"
380                 % (self.session.session_id, self.tree_connect_id))
381
382    def _verify(self, check, actual, expected):
383        log_header = "Session: %d, Tree: %d"\
384                     % (self.session.session_id, self.tree_connect_id)
385        if actual != expected:
386            raise SMBException("%s - Secure negotiate failed to verify %s, "
387                               "Actual: %s, Expected: %s"
388                               % (log_header, check, actual, expected))
389