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