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