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