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