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