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