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 2 Server Automaton
8
9This provides a [MS-SMB2] server that can:
10- serve files
11- host a DCE/RPC server
12
13This is a Scapy Automaton that is supposedly easily extendable.
14
15.. note::
16 You will find more complete documentation for this layer over at
17 `SMB <https://scapy.readthedocs.io/en/latest/layers/smb.html#server>`_
18"""
19
20import hashlib
21import pathlib
22import socket
23import struct
24import time
25
26from scapy.arch import get_if_addr
27from scapy.automaton import ATMT, Automaton
28from scapy.config import conf
29from scapy.error import log_runtime, log_interactive
30from scapy.volatile import RandUUID
31
32from scapy.layers.dcerpc import (
33 DCERPC_Transport,
34 NDRUnion,
35)
36from scapy.layers.gssapi import (
37 GSS_S_COMPLETE,
38 GSS_S_CONTINUE_NEEDED,
39 GSS_S_CREDENTIALS_EXPIRED,
40)
41from scapy.layers.msrpce.rpcserver import DCERPC_Server
42from scapy.layers.ntlm import (
43 NTLMSSP,
44)
45from scapy.layers.smb import (
46 SMBNegotiate_Request,
47 SMBNegotiate_Response_Extended_Security,
48 SMBNegotiate_Response_Security,
49 SMBSession_Null,
50 SMBSession_Setup_AndX_Request,
51 SMBSession_Setup_AndX_Request_Extended_Security,
52 SMBSession_Setup_AndX_Response,
53 SMBSession_Setup_AndX_Response_Extended_Security,
54 SMBTree_Connect_AndX,
55 SMB_Header,
56)
57from scapy.layers.smb2 import (
58 DFS_REFERRAL_ENTRY1,
59 DFS_REFERRAL_V3,
60 DirectTCP,
61 FILE_BOTH_DIR_INFORMATION,
62 FILE_FULL_DIR_INFORMATION,
63 FILE_ID_BOTH_DIR_INFORMATION,
64 FILE_NAME_INFORMATION,
65 FileAllInformation,
66 FileAlternateNameInformation,
67 FileBasicInformation,
68 FileEaInformation,
69 FileFsAttributeInformation,
70 FileFsSizeInformation,
71 FileFsVolumeInformation,
72 FileIdBothDirectoryInformation,
73 FileInternalInformation,
74 FileNetworkOpenInformation,
75 FileStandardInformation,
76 FileStreamInformation,
77 NETWORK_INTERFACE_INFO,
78 SECURITY_DESCRIPTOR,
79 SMB2_Cancel_Request,
80 SMB2_Change_Notify_Request,
81 SMB2_Change_Notify_Response,
82 SMB2_Close_Request,
83 SMB2_Close_Response,
84 SMB2_Create_Context,
85 SMB2_CREATE_DURABLE_HANDLE_RESPONSE_V2,
86 SMB2_CREATE_QUERY_MAXIMAL_ACCESS_RESPONSE,
87 SMB2_CREATE_QUERY_ON_DISK_ID,
88 SMB2_Create_Request,
89 SMB2_Create_Response,
90 SMB2_Echo_Request,
91 SMB2_Echo_Response,
92 SMB2_Encryption_Capabilities,
93 SMB2_Error_Response,
94 SMB2_FILEID,
95 SMB2_Header,
96 SMB2_IOCTL_Network_Interface_Info,
97 SMB2_IOCTL_Request,
98 SMB2_IOCTL_RESP_GET_DFS_Referral,
99 SMB2_IOCTL_Response,
100 SMB2_IOCTL_Validate_Negotiate_Info_Response,
101 SMB2_Negotiate_Context,
102 SMB2_Negotiate_Protocol_Request,
103 SMB2_Negotiate_Protocol_Response,
104 SMB2_Preauth_Integrity_Capabilities,
105 SMB2_Query_Directory_Request,
106 SMB2_Query_Directory_Response,
107 SMB2_Query_Info_Request,
108 SMB2_Query_Info_Response,
109 SMB2_Read_Request,
110 SMB2_Read_Response,
111 SMB2_Session_Logoff_Request,
112 SMB2_Session_Logoff_Response,
113 SMB2_Session_Setup_Request,
114 SMB2_Session_Setup_Response,
115 SMB2_Set_Info_Request,
116 SMB2_Set_Info_Response,
117 SMB2_Signing_Capabilities,
118 SMB2_Tree_Connect_Request,
119 SMB2_Tree_Connect_Response,
120 SMB2_Tree_Disconnect_Request,
121 SMB2_Tree_Disconnect_Response,
122 SMB2_Write_Request,
123 SMB2_Write_Response,
124 SMBStreamSocket,
125 SOCKADDR_STORAGE,
126 SRVSVC_SHARE_TYPES,
127)
128from scapy.layers.spnego import SPNEGOSSP
129
130# Import DCE/RPC
131from scapy.layers.msrpce.raw.ms_srvs import (
132 LPSERVER_INFO_101,
133 LPSHARE_ENUM_STRUCT,
134 LPSHARE_INFO_1,
135 NetrServerGetInfo_Request,
136 NetrServerGetInfo_Response,
137 NetrShareEnum_Request,
138 NetrShareEnum_Response,
139 NetrShareGetInfo_Request,
140 NetrShareGetInfo_Response,
141 SHARE_INFO_1_CONTAINER,
142)
143from scapy.layers.msrpce.raw.ms_wkst import (
144 LPWKSTA_INFO_100,
145 NetrWkstaGetInfo_Request,
146 NetrWkstaGetInfo_Response,
147)
148
149
150class SMBShare:
151 """
152 A class used to define a share, used by SMB_Server
153
154 :param name: the share name
155 :param path: the path the the folder hosted by the share
156 :param type: (optional) share type per [MS-SRVS] sect 2.2.2.4
157 :param remark: (optional) a description of the share
158 """
159
160 def __init__(self, name, path=".", type=None, remark=""):
161 # Set the default type
162 if type is None:
163 type = 0 # DISKTREE
164 if name.endswith("$"):
165 type &= 0x80000000 # SPECIAL
166 # Lower case the name for resolution
167 self._name = name.lower()
168 # Resolve path
169 self.path = pathlib.Path(path).resolve()
170 # props
171 self.name = name
172 self.type = type
173 self.remark = remark
174
175 def __repr__(self):
176 type = SRVSVC_SHARE_TYPES[self.type & 0x0FFFFFFF]
177 if self.type & 0x80000000:
178 type = "SPECIAL+" + type
179 if self.type & 0x40000000:
180 type = "TEMPORARY+" + type
181 return "<SMBShare %s [%s]%s = %s>" % (
182 self.name,
183 type,
184 self.remark and (" '%s'" % self.remark) or "",
185 str(self.path),
186 )
187
188
189# The SMB Automaton
190
191
192class SMB_Server(Automaton):
193 """
194 SMB server automaton
195
196 :param shares: the shares to serve. By default, share nothing.
197 Note that IPC$ is appended.
198 :param ssp: the SSP to use
199
200 All other options (in caps) are optional, and SMB specific:
201
202 :param ANONYMOUS_LOGIN: mark the clients as anonymous
203 :param GUEST_LOGIN: mark the clients as guest
204 :param REQUIRE_SIGNATURE: set 'Require Signature'
205 :param MAX_DIALECT: maximum SMB dialect. Defaults to 0x0311 (3.1.1)
206 :param TREE_SHARE_FLAGS: flags to announce on Tree_Connect_Response
207 :param TREE_CAPABILITIES: capabilities to announce on Tree_Connect_Response
208 :param TREE_MAXIMAL_ACCESS: maximal access to announce on Tree_Connect_Response
209 :param FILE_MAXIMAL_ACCESS: maximal access to announce in MxAc Create Context
210 """
211
212 pkt_cls = DirectTCP
213 socketcls = SMBStreamSocket
214
215 def __init__(self, shares=[], ssp=None, verb=True, readonly=True, *args, **kwargs):
216 self.verb = verb
217 if "sock" not in kwargs:
218 raise ValueError(
219 "SMB_Server cannot be started directly ! Use SMB_Server.spawn"
220 )
221 # Various SMB server arguments
222 self.ANONYMOUS_LOGIN = kwargs.pop("ANONYMOUS_LOGIN", False)
223 self.GUEST_LOGIN = kwargs.pop("GUEST_LOGIN", None)
224 self.EXTENDED_SECURITY = kwargs.pop("EXTENDED_SECURITY", True)
225 self.USE_SMB1 = kwargs.pop("USE_SMB1", False)
226 self.REQUIRE_SIGNATURE = kwargs.pop("REQUIRE_SIGNATURE", False)
227 self.MAX_DIALECT = kwargs.pop("MAX_DIALECT", 0x0311)
228 self.TREE_SHARE_FLAGS = kwargs.pop(
229 "TREE_SHARE_FLAGS", "FORCE_LEVELII_OPLOCK+RESTRICT_EXCLUSIVE_OPENS"
230 )
231 self.TREE_CAPABILITIES = kwargs.pop("TREE_CAPABILITIES", 0)
232 self.TREE_MAXIMAL_ACCESS = kwargs.pop(
233 "TREE_MAXIMAL_ACCESS",
234 "+".join(
235 [
236 "FILE_READ_DATA",
237 "FILE_WRITE_DATA",
238 "FILE_APPEND_DATA",
239 "FILE_READ_EA",
240 "FILE_WRITE_EA",
241 "FILE_EXECUTE",
242 "FILE_DELETE_CHILD",
243 "FILE_READ_ATTRIBUTES",
244 "FILE_WRITE_ATTRIBUTES",
245 "DELETE",
246 "READ_CONTROL",
247 "WRITE_DAC",
248 "WRITE_OWNER",
249 "SYNCHRONIZE",
250 ]
251 ),
252 )
253 self.FILE_MAXIMAL_ACCESS = kwargs.pop(
254 # Read-only
255 "FILE_MAXIMAL_ACCESS",
256 "+".join(
257 [
258 "FILE_READ_DATA",
259 "FILE_READ_EA",
260 "FILE_EXECUTE",
261 "FILE_READ_ATTRIBUTES",
262 "READ_CONTROL",
263 "SYNCHRONIZE",
264 ]
265 ),
266 )
267 self.LOCAL_IPS = kwargs.pop(
268 "LOCAL_IPS", [get_if_addr(kwargs.get("iface", conf.iface) or conf.iface)]
269 )
270 self.DOMAIN_REFERRALS = kwargs.pop("DOMAIN_REFERRALS", [])
271 if self.USE_SMB1:
272 log_runtime.warning("Serving SMB1 is not supported :/")
273 self.readonly = readonly
274 # We don't want to update the parent shares argument
275 self.shares = shares.copy()
276 # Append the IPC$ share
277 self.shares.append(
278 SMBShare(
279 name="IPC$",
280 type=0x80000003, # SPECIAL+IPC
281 remark="Remote IPC",
282 )
283 )
284 # Initialize the DCE/RPC server for SMB
285 self.rpc_server = SMB_DCERPC_Server(
286 DCERPC_Transport.NCACN_NP,
287 shares=self.shares,
288 verb=self.verb,
289 )
290 # Extend it if another DCE/RPC server is provided
291 if "DCERPC_SERVER_CLS" in kwargs:
292 self.rpc_server.extend(kwargs.pop("DCERPC_SERVER_CLS"))
293 # Internal Session information
294 self.SMB2 = False
295 self.NegotiateCapabilities = None
296 self.GUID = RandUUID()._fix()
297 # Compounds are handled on receiving by the StreamSocket,
298 # and on aggregated in a CompoundQueue to be sent in one go
299 self.NextCompound = False
300 self.CompoundedHandle = None
301 # SSP provider
302 if ssp is None:
303 # No SSP => fallback on NTLM with guest
304 ssp = SPNEGOSSP(
305 [
306 NTLMSSP(
307 USE_MIC=False,
308 DO_NOT_CHECK_LOGIN=True,
309 ),
310 ]
311 )
312 if self.GUEST_LOGIN is None:
313 self.GUEST_LOGIN = True
314 # Initialize
315 Automaton.__init__(self, *args, **kwargs)
316 # Set session options
317 self.session.ssp = ssp
318 self.session.SecurityMode = kwargs.pop(
319 "SECURITY_MODE",
320 3 if self.REQUIRE_SIGNATURE else bool(ssp),
321 )
322
323 @property
324 def session(self):
325 # session shorthand
326 return self.sock.session
327
328 def vprint(self, s=""):
329 """
330 Verbose print (if enabled)
331 """
332 if self.verb:
333 if conf.interactive:
334 log_interactive.info("> %s", s)
335 else:
336 print("> %s" % s)
337
338 def send(self, pkt):
339 return super(SMB_Server, self).send(pkt, Compounded=self.NextCompound)
340
341 @ATMT.state(initial=1)
342 def BEGIN(self):
343 self.authenticated = False
344
345 @ATMT.receive_condition(BEGIN)
346 def received_negotiate(self, pkt):
347 if SMBNegotiate_Request in pkt:
348 raise self.NEGOTIATED().action_parameters(pkt)
349
350 @ATMT.receive_condition(BEGIN)
351 def received_negotiate_smb2_begin(self, pkt):
352 if SMB2_Negotiate_Protocol_Request in pkt:
353 self.SMB2 = True
354 raise self.NEGOTIATED().action_parameters(pkt)
355
356 @ATMT.action(received_negotiate_smb2_begin)
357 def on_negotiate_smb2_begin(self, pkt):
358 self.on_negotiate(pkt)
359
360 @ATMT.action(received_negotiate)
361 def on_negotiate(self, pkt):
362 self.session.sspcontext, spnego_token = self.session.ssp.NegTokenInit2()
363 # Build negotiate response
364 DialectIndex = None
365 DialectRevision = None
366 if SMB2_Negotiate_Protocol_Request in pkt:
367 # SMB2
368 DialectRevisions = pkt[SMB2_Negotiate_Protocol_Request].Dialects
369 DialectRevisions = [x for x in DialectRevisions if x <= self.MAX_DIALECT]
370 DialectRevisions.sort(reverse=True)
371 if DialectRevisions:
372 DialectRevision = DialectRevisions[0]
373 else:
374 # SMB1
375 DialectIndexes = [
376 x.DialectString for x in pkt[SMBNegotiate_Request].Dialects
377 ]
378 if self.USE_SMB1:
379 # Enforce SMB1
380 DialectIndex = DialectIndexes.index(b"NT LM 0.12")
381 else:
382 # Find a value matching SMB2, fallback to SMB1
383 for key, rev in [(b"SMB 2.???", 0x02FF), (b"SMB 2.002", 0x0202)]:
384 try:
385 DialectIndex = DialectIndexes.index(key)
386 DialectRevision = rev
387 self.SMB2 = True
388 break
389 except ValueError:
390 pass
391 else:
392 DialectIndex = DialectIndexes.index(b"NT LM 0.12")
393 if DialectRevision and DialectRevision & 0xFF != 0xFF:
394 # Version isn't SMB X.???
395 self.session.Dialect = DialectRevision
396 cls = None
397 if self.SMB2:
398 # SMB2
399 cls = SMB2_Negotiate_Protocol_Response
400 self.smb_header = DirectTCP() / SMB2_Header(
401 Flags="SMB2_FLAGS_SERVER_TO_REDIR",
402 CreditRequest=1,
403 CreditCharge=1,
404 )
405 if SMB2_Negotiate_Protocol_Request in pkt:
406 self.update_smbheader(pkt)
407 else:
408 # SMB1
409 self.smb_header = DirectTCP() / SMB_Header(
410 Flags="REPLY+CASE_INSENSITIVE+CANONICALIZED_PATHS",
411 Flags2=(
412 "LONG_NAMES+EAS+NT_STATUS+SMB_SECURITY_SIGNATURE+"
413 "UNICODE+EXTENDED_SECURITY"
414 ),
415 TID=pkt.TID,
416 MID=pkt.MID,
417 UID=pkt.UID,
418 PIDLow=pkt.PIDLow,
419 )
420 if self.EXTENDED_SECURITY:
421 cls = SMBNegotiate_Response_Extended_Security
422 else:
423 cls = SMBNegotiate_Response_Security
424 if DialectRevision is None and DialectIndex is None:
425 # No common dialect found.
426 if self.SMB2:
427 resp = self.smb_header.copy() / SMB2_Error_Response()
428 resp.Command = "SMB2_NEGOTIATE"
429 else:
430 resp = self.smb_header.copy() / SMBSession_Null()
431 resp.Command = "SMB_COM_NEGOTIATE"
432 resp.Status = "STATUS_NOT_SUPPORTED"
433 self.send(resp)
434 return
435 if self.SMB2: # SMB2
436 # Capabilities: [MS-SMB2] 3.3.5.4
437 self.NegotiateCapabilities = "+".join(
438 [
439 "DFS",
440 "LEASING",
441 "LARGE_MTU",
442 ]
443 )
444 if DialectRevision >= 0x0300:
445 # "if Connection.Dialect belongs to the SMB 3.x dialect family,
446 # the server supports..."
447 self.NegotiateCapabilities += "+" + "+".join(
448 [
449 "MULTI_CHANNEL",
450 "PERSISTENT_HANDLES",
451 "DIRECTORY_LEASING",
452 ]
453 )
454 if DialectRevision in [0x0300, 0x0302]:
455 # "if Connection.Dialect is "3.0" or "3.0.2""...
456 # Note: 3.1.1 uses the ENCRYPT_DATA flag in Tree Connect Response
457 self.NegotiateCapabilities += "+ENCRYPTION"
458 # Build response
459 resp = self.smb_header.copy() / cls(
460 DialectRevision=DialectRevision,
461 SecurityMode=self.session.SecurityMode,
462 ServerTime=(time.time() + 11644473600) * 1e7,
463 ServerStartTime=0,
464 MaxTransactionSize=65536,
465 MaxReadSize=65536,
466 MaxWriteSize=65536,
467 Capabilities=self.NegotiateCapabilities,
468 )
469 # SMB >= 3.0.0
470 if DialectRevision >= 0x0300:
471 # [MS-SMB2] sect 3.3.5.3.1 note 253
472 resp.MaxTransactionSize = 0x800000
473 resp.MaxReadSize = 0x800000
474 resp.MaxWriteSize = 0x800000
475 # SMB 3.1.1
476 if DialectRevision >= 0x0311:
477 resp.NegotiateContexts = [
478 # Preauth capabilities
479 SMB2_Negotiate_Context()
480 / SMB2_Preauth_Integrity_Capabilities(
481 # SHA-512 by default
482 HashAlgorithms=[self.session.PreauthIntegrityHashId],
483 Salt=self.session.Salt,
484 ),
485 # Encryption capabilities
486 SMB2_Negotiate_Context()
487 / SMB2_Encryption_Capabilities(
488 # AES-128-CCM by default
489 Ciphers=[self.session.CipherId],
490 ),
491 # Signing capabilities
492 SMB2_Negotiate_Context()
493 / SMB2_Signing_Capabilities(
494 # AES-128-CCM by default
495 SigningAlgorithms=[self.session.SigningAlgorithmId],
496 ),
497 ]
498 else:
499 # SMB1
500 resp = self.smb_header.copy() / cls(
501 DialectIndex=DialectIndex,
502 ServerCapabilities=(
503 "UNICODE+LARGE_FILES+NT_SMBS+RPC_REMOTE_APIS+STATUS32+"
504 "LEVEL_II_OPLOCKS+LOCK_AND_READ+NT_FIND+"
505 "LWIO+INFOLEVEL_PASSTHRU+LARGE_READX+LARGE_WRITEX"
506 ),
507 SecurityMode=self.session.SecurityMode,
508 ServerTime=(time.time() + 11644473600) * 1e7,
509 ServerTimeZone=0x3C,
510 )
511 if self.EXTENDED_SECURITY:
512 resp.ServerCapabilities += "EXTENDED_SECURITY"
513 if self.EXTENDED_SECURITY or self.SMB2:
514 # Extended SMB1 / SMB2
515 resp.GUID = self.GUID
516 # Add security blob
517 resp.SecurityBlob = spnego_token
518 else:
519 # Non-extended SMB1
520 # FIXME never tested.
521 resp.SecurityBlob = spnego_token
522 resp.Flags2 -= "EXTENDED_SECURITY"
523 if not self.SMB2:
524 resp[SMB_Header].Flags2 = (
525 resp[SMB_Header].Flags2
526 - "SMB_SECURITY_SIGNATURE"
527 + "SMB_SECURITY_SIGNATURE_REQUIRED+IS_LONG_NAME"
528 )
529 if SMB2_Header in pkt:
530 # If required, compute sessions
531 self.session.computeSMBConnectionPreauth(
532 bytes(pkt[SMB2_Header]), # nego request
533 bytes(resp[SMB2_Header]), # nego response
534 )
535 self.send(resp)
536
537 @ATMT.state()
538 def NEGOTIATED(self):
539 pass
540
541 def update_smbheader(self, pkt):
542 """
543 Called when receiving a SMB2 packet to update the current smb_header
544 """
545 # [MS-SMB2] sect 3.2.5.1.4 - always grant client its credits
546 self.smb_header.CreditRequest = pkt.CreditRequest
547 # [MS-SMB2] sect 3.3.4.1
548 # "the server SHOULD set the CreditCharge field in the SMB2 header
549 # of the response to the CreditCharge value in the SMB2 header of the request."
550 self.smb_header.CreditCharge = pkt.CreditCharge
551 # If the packet has a NextCommand, set NextCompound to True
552 self.NextCompound = bool(pkt.NextCommand)
553 # [MS-SMB2] sect 3.3.5.2.7.2
554 # Add SMB2_FLAGS_RELATED_OPERATIONS to the response if present
555 if pkt.Flags.SMB2_FLAGS_RELATED_OPERATIONS:
556 self.smb_header.Flags += "SMB2_FLAGS_RELATED_OPERATIONS"
557 else:
558 self.smb_header.Flags -= "SMB2_FLAGS_RELATED_OPERATIONS"
559 # [MS-SMB2] sect 2.2.1.2 - Priority
560 if (self.session.Dialect or 0) >= 0x0311:
561 self.smb_header.Flags &= 0xFF8F
562 self.smb_header.Flags |= int(pkt.Flags) & 0x70
563 # Update IDs
564 self.smb_header.SessionId = pkt.SessionId
565 self.smb_header.TID = pkt.TID
566 self.smb_header.MID = pkt.MID
567 self.smb_header.PID = pkt.PID
568
569 @ATMT.receive_condition(NEGOTIATED)
570 def received_negotiate_smb2(self, pkt):
571 if SMB2_Negotiate_Protocol_Request in pkt:
572 raise self.NEGOTIATED().action_parameters(pkt)
573
574 @ATMT.action(received_negotiate_smb2)
575 def on_negotiate_smb2(self, pkt):
576 self.on_negotiate(pkt)
577
578 @ATMT.receive_condition(NEGOTIATED)
579 def receive_setup_andx_request(self, pkt):
580 if (
581 SMBSession_Setup_AndX_Request_Extended_Security in pkt
582 or SMBSession_Setup_AndX_Request in pkt
583 ):
584 # SMB1
585 if SMBSession_Setup_AndX_Request_Extended_Security in pkt:
586 # Extended
587 ssp_blob = pkt.SecurityBlob
588 else:
589 # Non-extended
590 ssp_blob = pkt[SMBSession_Setup_AndX_Request].UnicodePassword
591 raise self.RECEIVED_SETUP_ANDX_REQUEST().action_parameters(pkt, ssp_blob)
592 elif SMB2_Session_Setup_Request in pkt:
593 # SMB2
594 ssp_blob = pkt.SecurityBlob
595 raise self.RECEIVED_SETUP_ANDX_REQUEST().action_parameters(pkt, ssp_blob)
596
597 @ATMT.state()
598 def RECEIVED_SETUP_ANDX_REQUEST(self):
599 pass
600
601 @ATMT.action(receive_setup_andx_request)
602 def on_setup_andx_request(self, pkt, ssp_blob):
603 self.session.sspcontext, tok, status = self.session.ssp.GSS_Accept_sec_context(
604 self.session.sspcontext, ssp_blob
605 )
606 self.update_smbheader(pkt)
607 if SMB2_Session_Setup_Request in pkt:
608 # SMB2
609 self.smb_header.SessionId = 0x0001000000000015
610 if status not in [GSS_S_CONTINUE_NEEDED, GSS_S_COMPLETE]:
611 # Error
612 if SMB2_Session_Setup_Request in pkt:
613 # SMB2
614 resp = self.smb_header.copy() / SMB2_Session_Setup_Response()
615 # Set security blob (if any)
616 resp.SecurityBlob = tok
617 else:
618 # SMB1
619 resp = self.smb_header.copy() / SMBSession_Null()
620 # Map some GSS return codes to NTStatus
621 if status == GSS_S_CREDENTIALS_EXPIRED:
622 resp.Status = "STATUS_PASSWORD_EXPIRED"
623 else:
624 resp.Status = "STATUS_LOGON_FAILURE"
625 # Reset Session preauth (SMB 3.1.1)
626 self.session.SessionPreauthIntegrityHashValue = None
627 else:
628 # Negotiation
629 if (
630 SMBSession_Setup_AndX_Request_Extended_Security in pkt
631 or SMB2_Session_Setup_Request in pkt
632 ):
633 # SMB1 extended / SMB2
634 if SMB2_Session_Setup_Request in pkt:
635 # SMB2
636 resp = self.smb_header.copy() / SMB2_Session_Setup_Response()
637 if self.GUEST_LOGIN:
638 resp.SessionFlags = "IS_GUEST"
639 if self.ANONYMOUS_LOGIN:
640 resp.SessionFlags = "IS_NULL"
641 else:
642 # SMB1 extended
643 resp = (
644 self.smb_header.copy()
645 / SMBSession_Setup_AndX_Response_Extended_Security(
646 NativeOS="Windows 4.0",
647 NativeLanMan="Windows 4.0",
648 )
649 )
650 if self.GUEST_LOGIN:
651 resp.Action = "SMB_SETUP_GUEST"
652 # Set security blob
653 resp.SecurityBlob = tok
654 elif SMBSession_Setup_AndX_Request in pkt:
655 # Non-extended
656 resp = self.smb_header.copy() / SMBSession_Setup_AndX_Response(
657 NativeOS="Windows 4.0",
658 NativeLanMan="Windows 4.0",
659 )
660 resp.Status = 0x0 if (status == GSS_S_COMPLETE) else 0xC0000016
661 # We have a response. If required, compute sessions
662 if status == GSS_S_CONTINUE_NEEDED:
663 # the setup session response is used in hash
664 self.session.computeSMBSessionPreauth(
665 bytes(pkt[SMB2_Header]), # session setup request
666 bytes(resp[SMB2_Header]), # session setup response
667 )
668 else:
669 # the setup session response is not used in hash
670 self.session.computeSMBSessionPreauth(
671 bytes(pkt[SMB2_Header]), # session setup request
672 )
673 if status == GSS_S_COMPLETE:
674 # Authentication was successful
675 self.session.computeSMBSessionKey()
676 self.authenticated = True
677 # and send
678 self.send(resp)
679
680 @ATMT.condition(RECEIVED_SETUP_ANDX_REQUEST)
681 def wait_for_next_request(self):
682 if self.authenticated:
683 self.vprint(
684 "User authenticated %s!" % (self.GUEST_LOGIN and " as guest" or "")
685 )
686 raise self.AUTHENTICATED()
687 else:
688 raise self.NEGOTIATED()
689
690 @ATMT.state()
691 def AUTHENTICATED(self):
692 """Dev: overload this"""
693 pass
694
695 # DEV: add a condition on AUTHENTICATED with prio=0
696
697 @ATMT.condition(AUTHENTICATED, prio=1)
698 def should_serve(self):
699 # Serve files
700 self.current_trees = {}
701 self.current_handles = {}
702 self.enumerate_index = {} # used for query directory enumeration
703 self.tree_id = 0
704 self.base_time_t = self.current_smb_time()
705 raise self.SERVING()
706
707 def _ioctl_error(self, Status="STATUS_NOT_SUPPORTED"):
708 pkt = self.smb_header.copy() / SMB2_Error_Response(ErrorData=b"\xff")
709 pkt.Status = Status
710 pkt.Command = "SMB2_IOCTL"
711 self.send(pkt)
712
713 @ATMT.state(final=1)
714 def END(self):
715 self.end()
716
717 # SERVE FILES
718
719 def current_tree(self):
720 """
721 Return the current tree name
722 """
723 return self.current_trees[self.smb_header.TID]
724
725 def root_path(self):
726 """
727 Return the root path of the current tree
728 """
729 curtree = self.current_tree()
730 try:
731 share_path = next(x.path for x in self.shares if x._name == curtree.lower())
732 except StopIteration:
733 return None
734 return pathlib.Path(share_path).resolve()
735
736 @ATMT.state()
737 def SERVING(self):
738 """
739 Main state when serving files
740 """
741 pass
742
743 @ATMT.receive_condition(SERVING)
744 def receive_logoff_request(self, pkt):
745 if SMB2_Session_Logoff_Request in pkt:
746 raise self.NEGOTIATED().action_parameters(pkt)
747
748 @ATMT.action(receive_logoff_request)
749 def send_logoff_response(self, pkt):
750 self.update_smbheader(pkt)
751 self.send(self.smb_header.copy() / SMB2_Session_Logoff_Response())
752
753 @ATMT.receive_condition(SERVING)
754 def receive_setup_andx_request_in_serving(self, pkt):
755 self.receive_setup_andx_request(pkt)
756
757 @ATMT.receive_condition(SERVING)
758 def is_smb1_tree(self, pkt):
759 if SMBTree_Connect_AndX in pkt:
760 # Unsupported
761 log_runtime.warning("Tree request in SMB1: unimplemented. Quit")
762 raise self.END()
763
764 @ATMT.receive_condition(SERVING)
765 def receive_tree_connect(self, pkt):
766 if SMB2_Tree_Connect_Request in pkt:
767 tree_name = pkt[SMB2_Tree_Connect_Request].Path.split("\\")[-1]
768 raise self.SERVING().action_parameters(pkt, tree_name)
769
770 @ATMT.action(receive_tree_connect)
771 def send_tree_connect_response(self, pkt, tree_name):
772 self.update_smbheader(pkt)
773 # Check the tree name against the shares we're serving
774 if not any(x._name == tree_name.lower() for x in self.shares):
775 # Unknown tree
776 resp = self.smb_header.copy() / SMB2_Error_Response()
777 resp.Command = "SMB2_TREE_CONNECT"
778 resp.Status = "STATUS_BAD_NETWORK_NAME"
779 self.send(resp)
780 return
781 # Add tree to current trees
782 if tree_name not in self.current_trees:
783 self.tree_id += 1
784 self.smb_header.TID = self.tree_id
785 self.current_trees[self.smb_header.TID] = tree_name
786 self.vprint("Tree Connect on: %s" % tree_name)
787 self.send(
788 self.smb_header
789 / SMB2_Tree_Connect_Response(
790 ShareType="PIPE" if self.current_tree() == "IPC$" else "DISK",
791 ShareFlags="AUTO_CACHING+NO_CACHING"
792 if self.current_tree() == "IPC$"
793 else self.TREE_SHARE_FLAGS,
794 Capabilities=0
795 if self.current_tree() == "IPC$"
796 else self.TREE_CAPABILITIES,
797 MaximalAccess=self.TREE_MAXIMAL_ACCESS,
798 )
799 )
800
801 @ATMT.receive_condition(SERVING)
802 def receive_ioctl(self, pkt):
803 if SMB2_IOCTL_Request in pkt:
804 raise self.SERVING().action_parameters(pkt)
805
806 @ATMT.action(receive_ioctl)
807 def send_ioctl_response(self, pkt):
808 self.update_smbheader(pkt)
809 if pkt.CtlCode == 0x11C017:
810 # FSCTL_PIPE_TRANSCEIVE
811 self.rpc_server.recv(pkt.Input.load)
812 self.send(
813 self.smb_header.copy()
814 / SMB2_IOCTL_Response(
815 CtlCode=0x11C017,
816 FileId=pkt[SMB2_IOCTL_Request].FileId,
817 Buffer=[("Output", self.rpc_server.get_response())],
818 )
819 )
820 elif pkt.CtlCode == 0x00140204 and self.session.sspcontext.SessionKey:
821 # FSCTL_VALIDATE_NEGOTIATE_INFO
822 # This is a security measure asking the server to validate
823 # what flags were negotiated during the SMBNegotiate exchange.
824 # This packet is ALWAYS signed, and expects a signed response.
825
826 # https://docs.microsoft.com/en-us/archive/blogs/openspecification/smb3-secure-dialect-negotiation
827 # > "Down-level servers (pre-Windows 2012) will return
828 # > STATUS_NOT_SUPPORTED or STATUS_INVALID_DEVICE_REQUEST
829 # > since they do not allow or implement
830 # > FSCTL_VALIDATE_NEGOTIATE_INFO.
831 # > The client should accept the
832 # > response provided it's properly signed".
833
834 if (self.session.Dialect or 0) < 0x0300:
835 # SMB < 3 isn't supposed to support FSCTL_VALIDATE_NEGOTIATE_INFO
836 self._ioctl_error(Status="STATUS_FILE_CLOSED")
837 return
838
839 # SMB3
840 self.send(
841 self.smb_header.copy()
842 / SMB2_IOCTL_Response(
843 CtlCode=0x00140204,
844 FileId=pkt[SMB2_IOCTL_Request].FileId,
845 Buffer=[
846 (
847 "Output",
848 SMB2_IOCTL_Validate_Negotiate_Info_Response(
849 GUID=self.GUID,
850 DialectRevision=self.session.Dialect,
851 SecurityMode=self.session.SecurityMode,
852 Capabilities=self.NegotiateCapabilities,
853 ),
854 )
855 ],
856 )
857 )
858 elif pkt.CtlCode == 0x001401FC:
859 # FSCTL_QUERY_NETWORK_INTERFACE_INFO
860 self.send(
861 self.smb_header.copy()
862 / SMB2_IOCTL_Response(
863 CtlCode=0x001401FC,
864 FileId=pkt[SMB2_IOCTL_Request].FileId,
865 Output=SMB2_IOCTL_Network_Interface_Info(
866 interfaces=[
867 NETWORK_INTERFACE_INFO(
868 SockAddr_Storage=SOCKADDR_STORAGE(
869 Family=0x0002,
870 IPv4Adddress=x,
871 )
872 )
873 for x in self.LOCAL_IPS
874 ]
875 ),
876 )
877 )
878 elif pkt.CtlCode == 0x00060194:
879 # FSCTL_DFS_GET_REFERRALS
880 if (
881 self.DOMAIN_REFERRALS
882 and not pkt[SMB2_IOCTL_Request].Input.RequestFileName
883 ):
884 # Requesting domain referrals
885 self.send(
886 self.smb_header.copy()
887 / SMB2_IOCTL_Response(
888 CtlCode=0x00060194,
889 FileId=pkt[SMB2_IOCTL_Request].FileId,
890 Output=SMB2_IOCTL_RESP_GET_DFS_Referral(
891 ReferralEntries=[
892 DFS_REFERRAL_V3(
893 ReferralEntryFlags="NameListReferral",
894 TimeToLive=600,
895 )
896 for _ in self.DOMAIN_REFERRALS
897 ],
898 ReferralBuffer=[
899 DFS_REFERRAL_ENTRY1(SpecialName=name)
900 for name in self.DOMAIN_REFERRALS
901 ],
902 ),
903 )
904 )
905 return
906 resp = self.smb_header.copy() / SMB2_Error_Response()
907 resp.Command = "SMB2_IOCTL"
908 resp.Status = "STATUS_FS_DRIVER_REQUIRED"
909 self.send(resp)
910 else:
911 # Among other things, FSCTL_VALIDATE_NEGOTIATE_INFO
912 self._ioctl_error(Status="STATUS_NOT_SUPPORTED")
913
914 @ATMT.receive_condition(SERVING)
915 def receive_create_file(self, pkt):
916 if SMB2_Create_Request in pkt:
917 raise self.SERVING().action_parameters(pkt)
918
919 PIPES_TABLE = {
920 "srvsvc": SMB2_FILEID(Persistent=0x4000000012, Volatile=0x4000000001),
921 "wkssvc": SMB2_FILEID(Persistent=0x4000000013, Volatile=0x4000000002),
922 "NETLOGON": SMB2_FILEID(Persistent=0x4000000014, Volatile=0x4000000003),
923 }
924
925 # special handle in case of compounded requests ([MS-SMB2] 3.2.4.1.4)
926 # that points to the chained opened file handle
927 LAST_HANDLE = SMB2_FILEID(
928 Persistent=0xFFFFFFFFFFFFFFFF, Volatile=0xFFFFFFFFFFFFFFFF
929 )
930
931 def current_smb_time(self):
932 return (
933 FileNetworkOpenInformation().get_field("CreationTime").i2m(None, None)
934 - 864000000000 # one day ago
935 )
936
937 def make_file_id(self, fname):
938 """
939 Generate deterministic FileId based on the fname
940 """
941 hash = hashlib.md5((fname or "").encode()).digest()
942 return 0x4000000000 | struct.unpack("<I", hash[:4])[0]
943
944 def lookup_file(self, fname, durable_handle=None, create=False, createOptions=None):
945 """
946 Lookup the file and build it's SMB2_FILEID
947 """
948 root = self.root_path()
949 if isinstance(fname, pathlib.Path):
950 path = fname
951 fname = path.name
952 else:
953 path = root / (fname or "").replace("\\", "/")
954 path = path.resolve()
955 # Word of caution: this check ONLY works because root and path have been
956 # resolve(). Be careful
957 # Note: symbolic links are currently unsupported.
958 if root not in path.parents and path != root:
959 raise FileNotFoundError
960 if path.is_reserved():
961 raise FileNotFoundError
962 if not path.exists():
963 if create and createOptions:
964 if createOptions.FILE_DIRECTORY_FILE:
965 # Folder creation
966 path.mkdir()
967 self.vprint("Created folder:" + fname)
968 else:
969 # File creation
970 path.touch()
971 self.vprint("Created file:" + fname)
972 else:
973 raise FileNotFoundError
974 if durable_handle is None:
975 handle = SMB2_FILEID(
976 Persistent=self.make_file_id(fname) + self.smb_header.MID,
977 )
978 else:
979 # We were given a durable handle. Use it
980 handle = durable_handle
981 attrs = {
982 "CreationTime": self.base_time_t,
983 "LastAccessTime": self.base_time_t,
984 "LastWriteTime": self.base_time_t,
985 "ChangeTime": self.base_time_t,
986 "EndOfFile": 0,
987 "AllocationSize": 0,
988 }
989 path_stat = path.stat()
990 attrs["EndOfFile"] = attrs["AllocationSize"] = path_stat.st_size
991 if fname is None:
992 # special case
993 attrs["FileAttributes"] = "+".join(
994 [
995 "FILE_ATTRIBUTE_HIDDEN",
996 "FILE_ATTRIBUTE_SYSTEM",
997 "FILE_ATTRIBUTE_DIRECTORY",
998 ]
999 )
1000 elif path.is_dir():
1001 attrs["FileAttributes"] = "FILE_ATTRIBUTE_DIRECTORY"
1002 else:
1003 attrs["FileAttributes"] = "FILE_ATTRIBUTE_ARCHIVE"
1004 self.current_handles[handle] = (
1005 path, # file path
1006 attrs, # file attributes
1007 )
1008 self.enumerate_index[handle] = 0
1009 return handle
1010
1011 def set_compounded_handle(self, handle):
1012 """
1013 Mark a handle as the current one being compounded.
1014 """
1015 self.CompoundedHandle = handle
1016
1017 def get_file_id(self, pkt):
1018 """
1019 Return the FileId attribute of pkt, accounting for compounded requests.
1020 """
1021 fid = pkt.FileId
1022 if fid == self.LAST_HANDLE:
1023 return self.CompoundedHandle
1024 return fid
1025
1026 def lookup_folder(self, handle, filter, offset, cls):
1027 """
1028 Lookup a folder handle
1029 """
1030 path = self.current_handles[handle][0]
1031 self.vprint("Query directory: " + str(path))
1032 self.current_handles[handle][1]["LastAccessTime"] = self.current_smb_time()
1033 if not path.is_dir():
1034 raise NotADirectoryError
1035 return sorted(
1036 [
1037 cls(FileName=x.name, **self.current_handles[self.lookup_file(x)][1])
1038 for x in path.glob(filter)
1039 # Note: symbolic links are unsupported because it's hard to check
1040 # for path traversal on them.
1041 if not x.is_symlink()
1042 ]
1043 + [
1044 cls(
1045 FileAttributes=("FILE_ATTRIBUTE_DIRECTORY"),
1046 FileName=".",
1047 )
1048 ]
1049 + (
1050 [
1051 cls(
1052 FileAttributes=("FILE_ATTRIBUTE_DIRECTORY"),
1053 FileName="..",
1054 )
1055 ]
1056 if path.resolve() != self.root_path()
1057 else []
1058 ),
1059 key=lambda x: x.FileName,
1060 )[offset:]
1061
1062 @ATMT.action(receive_create_file)
1063 def send_create_file_response(self, pkt):
1064 """
1065 Handle CreateFile request
1066
1067 See [MS-SMB2] 3.3.5.9 ()
1068 """
1069 self.update_smbheader(pkt)
1070 if pkt[SMB2_Create_Request].NameLen:
1071 fname = pkt[SMB2_Create_Request].Name
1072 else:
1073 fname = None
1074 if fname:
1075 self.vprint("Opened: " + fname)
1076 if self.current_tree() == "IPC$":
1077 # Special IPC$ case: opening a pipe
1078 FILE_ID = self.PIPES_TABLE.get(fname, None)
1079 if FILE_ID:
1080 attrs = {
1081 "CreationTime": 0,
1082 "LastAccessTime": 0,
1083 "LastWriteTime": 0,
1084 "ChangeTime": 0,
1085 "EndOfFile": 0,
1086 "AllocationSize": 4096,
1087 }
1088 self.current_handles[FILE_ID] = (
1089 fname,
1090 attrs,
1091 )
1092 self.send(
1093 self.smb_header.copy()
1094 / SMB2_Create_Response(
1095 OplockLevel=pkt.RequestedOplockLevel,
1096 FileId=FILE_ID,
1097 **attrs,
1098 )
1099 )
1100 else:
1101 # NOT_FOUND
1102 resp = self.smb_header.copy() / SMB2_Error_Response()
1103 resp.Command = "SMB2_CREATE"
1104 resp.Status = "STATUS_OBJECT_NAME_NOT_FOUND"
1105 self.send(resp)
1106 return
1107 else:
1108 # Check if there is a Durable Handle Reconnect Request
1109 durable_handle = None
1110 if pkt[SMB2_Create_Request].CreateContextsLen:
1111 try:
1112 durable_handle = next(
1113 x.Data.FileId
1114 for x in pkt[SMB2_Create_Request].CreateContexts
1115 if x.Name == b"DH2C"
1116 )
1117 except StopIteration:
1118 pass
1119 # Lookup file handle
1120 try:
1121 handle = self.lookup_file(fname, durable_handle=durable_handle)
1122 except FileNotFoundError:
1123 # NOT_FOUND
1124 if pkt[SMB2_Create_Request].CreateDisposition in [
1125 0x00000002, # FILE_CREATE
1126 0x00000005, # FILE_OVERWRITE_IF
1127 ]:
1128 if self.readonly:
1129 resp = self.smb_header.copy() / SMB2_Error_Response()
1130 resp.Command = "SMB2_CREATE"
1131 resp.Status = "STATUS_ACCESS_DENIED"
1132 self.send(resp)
1133 return
1134 else:
1135 # Create file
1136 handle = self.lookup_file(
1137 fname,
1138 durable_handle=durable_handle,
1139 create=True,
1140 createOptions=pkt[SMB2_Create_Request].CreateOptions,
1141 )
1142 else:
1143 resp = self.smb_header.copy() / SMB2_Error_Response()
1144 resp.Command = "SMB2_CREATE"
1145 resp.Status = "STATUS_OBJECT_NAME_NOT_FOUND"
1146 self.send(resp)
1147 return
1148 # Store compounded handle
1149 self.set_compounded_handle(handle)
1150 # Build response
1151 attrs = self.current_handles[handle][1]
1152 resp = self.smb_header.copy() / SMB2_Create_Response(
1153 OplockLevel=pkt.RequestedOplockLevel,
1154 FileId=handle,
1155 **attrs,
1156 )
1157 # Handle the various chain elements
1158 if pkt[SMB2_Create_Request].CreateContextsLen:
1159 CreateContexts = []
1160 # Note: failing to provide context elements when the client asks for
1161 # them will make the windows implementation fall into a weird
1162 # "the-server-is-dumb" mode. So provide them 'quoi qu'il en coûte'.
1163 for elt in pkt[SMB2_Create_Request].CreateContexts:
1164 if elt.Name == b"QFid":
1165 # [MS-SMB2] sect 3.3.5.9.9
1166 CreateContexts.append(
1167 SMB2_Create_Context(
1168 Name=b"QFid",
1169 Data=SMB2_CREATE_QUERY_ON_DISK_ID(
1170 DiskFileId=self.make_file_id(fname),
1171 VolumeId=0xBA39CD11,
1172 ),
1173 )
1174 )
1175 elif elt.Name == b"MxAc":
1176 # [MS-SMB2] sect 3.3.5.9.5
1177 CreateContexts.append(
1178 SMB2_Create_Context(
1179 Name=b"MxAc",
1180 Data=SMB2_CREATE_QUERY_MAXIMAL_ACCESS_RESPONSE(
1181 QueryStatus=0,
1182 MaximalAccess=self.FILE_MAXIMAL_ACCESS,
1183 ),
1184 )
1185 )
1186 elif elt.Name == b"DH2Q":
1187 # [MS-SMB2] sect 3.3.5.9.10
1188 if "FILE_ATTRIBUTE_DIRECTORY" in attrs["FileAttributes"]:
1189 continue
1190 CreateContexts.append(
1191 SMB2_Create_Context(
1192 Name=b"DH2Q",
1193 Data=SMB2_CREATE_DURABLE_HANDLE_RESPONSE_V2(
1194 Timeout=180000
1195 ),
1196 )
1197 )
1198 elif elt.Name == b"RqLs":
1199 # [MS-SMB2] sect 3.3.5.9.11
1200 # TODO: hmm, we are probably supposed to do something here
1201 CreateContexts.append(
1202 SMB2_Create_Context(
1203 Name=b"RqLs",
1204 Data=elt.Data,
1205 )
1206 )
1207 resp.CreateContexts = CreateContexts
1208 self.send(resp)
1209
1210 @ATMT.receive_condition(SERVING)
1211 def receive_change_notify_info(self, pkt):
1212 if SMB2_Change_Notify_Request in pkt:
1213 raise self.SERVING().action_parameters(pkt)
1214
1215 @ATMT.action(receive_change_notify_info)
1216 def send_change_notify_info_response(self, pkt):
1217 # [MS-SMB2] sect 3.3.5.19
1218 # "If the underlying object store does not support change notifications, the
1219 # server MUST fail this request with STATUS_NOT_SUPPORTED."
1220 self.update_smbheader(pkt)
1221 resp = self.smb_header.copy() / SMB2_Error_Response()
1222 resp.Command = "SMB2_CHANGE_NOTIFY"
1223 # ScapyFS doesn't support notifications
1224 resp.Status = "STATUS_NOT_SUPPORTED"
1225 self.send(resp)
1226
1227 @ATMT.receive_condition(SERVING)
1228 def receive_query_directory_info(self, pkt):
1229 if SMB2_Query_Directory_Request in pkt:
1230 raise self.SERVING().action_parameters(pkt)
1231
1232 @ATMT.action(receive_query_directory_info)
1233 def send_query_directory_response(self, pkt):
1234 self.update_smbheader(pkt)
1235 if not pkt.FileNameLen:
1236 # this is broken.
1237 return
1238 query = pkt.FileName
1239 fid = self.get_file_id(pkt)
1240 # Check for handled FileInformationClass
1241 # 0x02: FileFullDirectoryInformation
1242 # 0x03: FileBothDirectoryInformation
1243 # 0x25: FileIdBothDirectoryInformation
1244 if pkt.FileInformationClass not in [0x02, 0x03, 0x25]:
1245 # Unknown FileInformationClass
1246 resp = self.smb_header.copy() / SMB2_Error_Response()
1247 resp.Command = "SMB2_QUERY_DIRECTORY"
1248 resp.Status = "STATUS_INVALID_INFO_CLASS"
1249 self.send(resp)
1250 return
1251 # Handle SMB2_RESTART_SCANS
1252 if pkt[SMB2_Query_Directory_Request].Flags.SMB2_RESTART_SCANS:
1253 self.enumerate_index[fid] = 0
1254 # Lookup the files
1255 try:
1256 files = self.lookup_folder(
1257 fid,
1258 query,
1259 self.enumerate_index[fid],
1260 {
1261 0x02: FILE_FULL_DIR_INFORMATION,
1262 0x03: FILE_BOTH_DIR_INFORMATION,
1263 0x25: FILE_ID_BOTH_DIR_INFORMATION,
1264 }[pkt.FileInformationClass],
1265 )
1266 except NotADirectoryError:
1267 resp = self.smb_header.copy() / SMB2_Error_Response()
1268 resp.Command = "SMB2_QUERY_DIRECTORY"
1269 resp.Status = "STATUS_INVALID_PARAMETER"
1270 self.send(resp)
1271 return
1272 if not files:
1273 # No more files !
1274 self.enumerate_index[fid] = 0
1275 resp = self.smb_header.copy() / SMB2_Error_Response()
1276 resp.Command = "SMB2_QUERY_DIRECTORY"
1277 resp.Status = "STATUS_NO_MORE_FILES"
1278 self.send(resp)
1279 return
1280 # Handle SMB2_RETURN_SINGLE_ENTRY
1281 if pkt[SMB2_Query_Directory_Request].Flags.SMB2_RETURN_SINGLE_ENTRY:
1282 files = files[:1]
1283 # Increment index
1284 self.enumerate_index[fid] += len(files)
1285 # Build response based on the FileInformationClass
1286 fileinfo = FileIdBothDirectoryInformation(
1287 files=files,
1288 )
1289 self.send(
1290 self.smb_header.copy()
1291 / SMB2_Query_Directory_Response(Buffer=[("Output", fileinfo)])
1292 )
1293
1294 @ATMT.receive_condition(SERVING)
1295 def receive_query_info(self, pkt):
1296 if SMB2_Query_Info_Request in pkt:
1297 raise self.SERVING().action_parameters(pkt)
1298
1299 @ATMT.action(receive_query_info)
1300 def send_query_info_response(self, pkt):
1301 self.update_smbheader(pkt)
1302 # [MS-FSCC] + [MS-SMB2] sect 2.2.37 / 3.3.5.20.1
1303 fid = self.get_file_id(pkt)
1304 if pkt.InfoType == 0x01: # SMB2_0_INFO_FILE
1305 if pkt.FileInfoClass == 0x05: # FileStandardInformation
1306 attrs = self.current_handles[fid][1]
1307 fileinfo = FileStandardInformation(
1308 EndOfFile=attrs["EndOfFile"],
1309 AllocationSize=attrs["AllocationSize"],
1310 )
1311 elif pkt.FileInfoClass == 0x06: # FileInternalInformation
1312 pth = self.current_handles[fid][0]
1313 fileinfo = FileInternalInformation(
1314 IndexNumber=hash(pth) & 0xFFFFFFFFFFFFFFFF,
1315 )
1316 elif pkt.FileInfoClass == 0x07: # FileEaInformation
1317 fileinfo = FileEaInformation()
1318 elif pkt.FileInfoClass == 0x12: # FileAllInformation
1319 attrs = self.current_handles[fid][1]
1320 fileinfo = FileAllInformation(
1321 BasicInformation=FileBasicInformation(
1322 CreationTime=attrs["CreationTime"],
1323 LastAccessTime=attrs["LastAccessTime"],
1324 LastWriteTime=attrs["LastWriteTime"],
1325 ChangeTime=attrs["ChangeTime"],
1326 FileAttributes=attrs["FileAttributes"],
1327 ),
1328 StandardInformation=FileStandardInformation(
1329 EndOfFile=attrs["EndOfFile"],
1330 AllocationSize=attrs["AllocationSize"],
1331 ),
1332 )
1333 elif pkt.FileInfoClass == 0x15: # FileAlternateNameInformation
1334 pth = self.current_handles[fid][0]
1335 fileinfo = FileAlternateNameInformation(
1336 FileName=pth.name,
1337 )
1338 elif pkt.FileInfoClass == 0x16: # FileStreamInformation
1339 attrs = self.current_handles[fid][1]
1340 fileinfo = FileStreamInformation(
1341 StreamSize=attrs["EndOfFile"],
1342 StreamAllocationSize=attrs["AllocationSize"],
1343 )
1344 elif pkt.FileInfoClass == 0x22: # FileNetworkOpenInformation
1345 attrs = self.current_handles[fid][1]
1346 fileinfo = FileNetworkOpenInformation(
1347 **attrs,
1348 )
1349 elif pkt.FileInfoClass == 0x30: # FileNormalizedNameInformation
1350 pth = self.current_handles[fid][0]
1351 fileinfo = FILE_NAME_INFORMATION(
1352 FileName=pth.name,
1353 )
1354 else:
1355 log_runtime.warning(
1356 "Unimplemented: %s"
1357 % pkt[SMB2_Query_Info_Request].sprintf("%InfoType% %FileInfoClass%")
1358 )
1359 return
1360 elif pkt.InfoType == 0x02: # SMB2_0_INFO_FILESYSTEM
1361 # [MS-FSCC] sect 2.5
1362 if pkt.FileInfoClass == 0x01: # FileFsVolumeInformation
1363 fileinfo = FileFsVolumeInformation()
1364 elif pkt.FileInfoClass == 0x03: # FileFsSizeInformation
1365 fileinfo = FileFsSizeInformation()
1366 elif pkt.FileInfoClass == 0x05: # FileFsAttributeInformation
1367 fileinfo = FileFsAttributeInformation(
1368 FileSystemAttributes=0x88000F,
1369 )
1370 elif pkt.FileInfoClass == 0x07: # FileEaInformation
1371 fileinfo = FileEaInformation()
1372 else:
1373 log_runtime.warning(
1374 "Unimplemented: %s"
1375 % pkt[SMB2_Query_Info_Request].sprintf("%InfoType% %FileInfoClass%")
1376 )
1377 return
1378 elif pkt.InfoType == 0x03: # SMB2_0_INFO_SECURITY
1379 # [MS-FSCC] 2.4.6
1380 fileinfo = SECURITY_DESCRIPTOR()
1381 # TODO: fill it
1382 if pkt.AdditionalInformation.OWNER_SECURITY_INFORMATION:
1383 pass
1384 if pkt.AdditionalInformation.GROUP_SECURITY_INFORMATION:
1385 pass
1386 if pkt.AdditionalInformation.DACL_SECURITY_INFORMATION:
1387 pass
1388 if pkt.AdditionalInformation.SACL_SECURITY_INFORMATION:
1389 pass
1390 # Observed:
1391 if (
1392 pkt.AdditionalInformation.OWNER_SECURITY_INFORMATION
1393 or pkt.AdditionalInformation.SACL_SECURITY_INFORMATION
1394 or pkt.AdditionalInformation.GROUP_SECURITY_INFORMATION
1395 or pkt.AdditionalInformation.DACL_SECURITY_INFORMATION
1396 ):
1397 pkt = self.smb_header.copy() / SMB2_Error_Response(ErrorData=b"\xff")
1398 pkt.Status = "STATUS_ACCESS_DENIED"
1399 pkt.Command = "SMB2_QUERY_INFO"
1400 self.send(pkt)
1401 return
1402 if pkt.AdditionalInformation.ATTRIBUTE_SECURITY_INFORMATION:
1403 fileinfo.Control = 0x8800
1404 self.send(
1405 self.smb_header.copy()
1406 / SMB2_Query_Info_Response(Buffer=[("Output", fileinfo)])
1407 )
1408
1409 @ATMT.receive_condition(SERVING)
1410 def receive_set_info_request(self, pkt):
1411 if SMB2_Set_Info_Request in pkt:
1412 raise self.SERVING().action_parameters(pkt)
1413
1414 @ATMT.action(receive_set_info_request)
1415 def send_set_info_response(self, pkt):
1416 self.update_smbheader(pkt)
1417 self.send(self.smb_header.copy() / SMB2_Set_Info_Response())
1418
1419 @ATMT.receive_condition(SERVING)
1420 def receive_write_request(self, pkt):
1421 if SMB2_Write_Request in pkt:
1422 raise self.SERVING().action_parameters(pkt)
1423
1424 @ATMT.action(receive_write_request)
1425 def send_write_response(self, pkt):
1426 self.update_smbheader(pkt)
1427 resp = SMB2_Write_Response(Count=len(pkt.Data))
1428 fid = self.get_file_id(pkt)
1429 if self.current_tree() == "IPC$":
1430 if fid in self.PIPES_TABLE.values():
1431 # A pipe
1432 self.rpc_server.recv(pkt.Data)
1433 else:
1434 if self.readonly:
1435 # Read only !
1436 resp = SMB2_Error_Response()
1437 resp.Command = "SMB2_WRITE"
1438 resp.Status = "ERROR_FILE_READ_ONLY"
1439 else:
1440 # Write file
1441 pth, _ = self.current_handles[fid]
1442 length = pkt[SMB2_Write_Request].DataLen
1443 off = pkt[SMB2_Write_Request].Offset
1444 self.vprint("Writing %s bytes at %s" % (length, off))
1445 with open(pth, "r+b") as fd:
1446 fd.seek(off)
1447 resp.Count = fd.write(pkt[SMB2_Write_Request].Data)
1448 self.send(self.smb_header.copy() / resp)
1449
1450 @ATMT.receive_condition(SERVING)
1451 def receive_read_request(self, pkt):
1452 if SMB2_Read_Request in pkt:
1453 raise self.SERVING().action_parameters(pkt)
1454
1455 @ATMT.action(receive_read_request)
1456 def send_read_response(self, pkt):
1457 self.update_smbheader(pkt)
1458 resp = SMB2_Read_Response()
1459 fid = self.get_file_id(pkt)
1460 if self.current_tree() == "IPC$":
1461 # Read output from DCE/RPC server
1462 r = self.rpc_server.get_response()
1463 resp.Data = bytes(r)
1464 else:
1465 # Read file and send content
1466 pth, _ = self.current_handles[fid]
1467 length = pkt[SMB2_Read_Request].Length
1468 off = pkt[SMB2_Read_Request].Offset
1469 self.vprint("Reading %s bytes at %s" % (length, off))
1470 with open(pth, "rb") as fd:
1471 fd.seek(off)
1472 resp.Data = fd.read(length)
1473 self.send(self.smb_header.copy() / resp)
1474
1475 @ATMT.receive_condition(SERVING)
1476 def receive_close_request(self, pkt):
1477 if SMB2_Close_Request in pkt:
1478 raise self.SERVING().action_parameters(pkt)
1479
1480 @ATMT.action(receive_close_request)
1481 def send_close_response(self, pkt):
1482 self.update_smbheader(pkt)
1483 if self.current_tree() != "IPC$":
1484 fid = self.get_file_id(pkt)
1485 pth, attrs = self.current_handles[fid]
1486 if pth:
1487 self.vprint("Closed: " + str(pth))
1488 del self.current_handles[fid]
1489 del self.enumerate_index[fid]
1490 self.send(
1491 self.smb_header.copy()
1492 / SMB2_Close_Response(
1493 Flags=pkt[SMB2_Close_Request].Flags,
1494 **attrs,
1495 )
1496 )
1497 else:
1498 self.send(self.smb_header.copy() / SMB2_Close_Response())
1499
1500 @ATMT.receive_condition(SERVING)
1501 def receive_tree_disconnect_request(self, pkt):
1502 if SMB2_Tree_Disconnect_Request in pkt:
1503 raise self.SERVING().action_parameters(pkt)
1504
1505 @ATMT.action(receive_tree_disconnect_request)
1506 def send_tree_disconnect_response(self, pkt):
1507 self.update_smbheader(pkt)
1508 try:
1509 del self.current_trees[self.smb_header.TID] # clear tree
1510 resp = self.smb_header.copy() / SMB2_Tree_Disconnect_Response()
1511 except KeyError:
1512 resp = self.smb_header.copy() / SMB2_Error_Response()
1513 resp.Command = "SMB2_TREE_DISCONNECT"
1514 resp.Status = "STATUS_NETWORK_NAME_DELETED"
1515 self.send(resp)
1516
1517 @ATMT.receive_condition(SERVING)
1518 def receive_cancel_request(self, pkt):
1519 if SMB2_Cancel_Request in pkt:
1520 raise self.SERVING().action_parameters(pkt)
1521
1522 @ATMT.action(receive_cancel_request)
1523 def send_notify_cancel_response(self, pkt):
1524 self.update_smbheader(pkt)
1525 resp = self.smb_header.copy() / SMB2_Change_Notify_Response()
1526 resp.Status = "STATUS_CANCELLED"
1527 self.send(resp)
1528
1529 @ATMT.receive_condition(SERVING)
1530 def receive_echo_request(self, pkt):
1531 if SMB2_Echo_Request in pkt:
1532 raise self.SERVING().action_parameters(pkt)
1533
1534 @ATMT.action(receive_echo_request)
1535 def send_echo_reply(self, pkt):
1536 self.update_smbheader(pkt)
1537 self.send(self.smb_header.copy() / SMB2_Echo_Response())
1538
1539
1540# DCE/RPC server for SMB
1541
1542
1543class SMB_DCERPC_Server(DCERPC_Server):
1544 """
1545 DCE/RPC server than handles the minimum RPCs for SMB to work:
1546 """
1547
1548 def __init__(self, *args, **kwargs):
1549 self.shares = kwargs.pop("shares")
1550 super(SMB_DCERPC_Server, self).__init__(*args, **kwargs)
1551
1552 @DCERPC_Server.answer(NetrShareEnum_Request)
1553 def netr_share_enum(self, req):
1554 """
1555 NetrShareEnum [MS-SRVS]
1556 "retrieves information about each shared resource on a server."
1557 """
1558 nbEntries = len(self.shares)
1559 return NetrShareEnum_Response(
1560 InfoStruct=LPSHARE_ENUM_STRUCT(
1561 Level=1,
1562 ShareInfo=NDRUnion(
1563 tag=1,
1564 value=SHARE_INFO_1_CONTAINER(
1565 Buffer=[
1566 # Add shares
1567 LPSHARE_INFO_1(
1568 shi1_netname=x.name,
1569 shi1_type=x.type,
1570 shi1_remark=x.remark,
1571 )
1572 for x in self.shares
1573 ],
1574 EntriesRead=nbEntries,
1575 ),
1576 ),
1577 ),
1578 TotalEntries=nbEntries,
1579 ndr64=self.ndr64,
1580 )
1581
1582 @DCERPC_Server.answer(NetrWkstaGetInfo_Request)
1583 def netr_wksta_getinfo(self, req):
1584 """
1585 NetrWkstaGetInfo [MS-SRVS]
1586 "returns information about the configuration of a workstation."
1587 """
1588 return NetrWkstaGetInfo_Response(
1589 WkstaInfo=NDRUnion(
1590 tag=100,
1591 value=LPWKSTA_INFO_100(
1592 wki100_platform_id=500, # NT
1593 wki100_ver_major=5,
1594 ),
1595 ),
1596 ndr64=self.ndr64,
1597 )
1598
1599 @DCERPC_Server.answer(NetrServerGetInfo_Request)
1600 def netr_server_getinfo(self, req):
1601 """
1602 NetrServerGetInfo [MS-WKST]
1603 "retrieves current configuration information for CIFS and
1604 SMB Version 1.0 servers."
1605 """
1606 return NetrServerGetInfo_Response(
1607 ServerInfo=NDRUnion(
1608 tag=101,
1609 value=LPSERVER_INFO_101(
1610 sv101_platform_id=500, # NT
1611 sv101_name=req.ServerName.value.value[0].value,
1612 sv101_version_major=6,
1613 sv101_version_minor=1,
1614 sv101_type=1, # Workstation
1615 ),
1616 ),
1617 ndr64=self.ndr64,
1618 )
1619
1620 @DCERPC_Server.answer(NetrShareGetInfo_Request)
1621 def netr_share_getinfo(self, req):
1622 """
1623 NetrShareGetInfo [MS-SRVS]
1624 "retrieves information about a particular shared resource on a server."
1625 """
1626 return NetrShareGetInfo_Response(
1627 ShareInfo=NDRUnion(
1628 tag=1,
1629 value=LPSHARE_INFO_1(
1630 shi1_netname=req.NetName.value[0].value,
1631 shi1_type=0,
1632 shi1_remark=b"",
1633 ),
1634 ),
1635 ndr64=self.ndr64,
1636 )
1637
1638
1639# Util
1640
1641
1642class smbserver:
1643 r"""
1644 Spawns a simple smbserver
1645
1646 smbserver parameters:
1647
1648 :param shares: the list of shares to announce. Note that IPC$ is appended.
1649 By default, a 'Scapy' share on './'
1650 :param port: (optional) the port to bind on, default 445
1651 :param iface: (optional) the interface to bind on, default conf.iface
1652 :param readonly: (optional) whether the server is read-only or not. default True
1653 :param ssp: (optional) the SSP to use. See the examples below.
1654 Default NTLM with guest
1655
1656 Many more SMB-specific parameters are available in help(SMB_Server)
1657 """
1658
1659 def __init__(
1660 self,
1661 shares=None,
1662 iface: str = None,
1663 port: int = 445,
1664 verb: int = 2,
1665 readonly: bool = True,
1666 # SMB arguments
1667 ssp=None,
1668 **kwargs,
1669 ):
1670 # Default Share
1671 if shares is None:
1672 shares = [
1673 SMBShare(
1674 name="Scapy", path=".", remark="Scapy's SMB server default share"
1675 )
1676 ]
1677 # Verb
1678 if verb >= 2:
1679 log_runtime.info("-- Scapy %s SMB Server --" % conf.version)
1680 log_runtime.info(
1681 "SSP: %s. Read-Only: %s. Serving %s shares:"
1682 % (
1683 conf.color_theme.yellow(ssp or "NTLM (guest)"),
1684 (
1685 conf.color_theme.yellow("YES")
1686 if readonly
1687 else conf.color_theme.format("NO", "bg_red+white")
1688 ),
1689 conf.color_theme.red(len(shares)),
1690 )
1691 )
1692 for share in shares:
1693 log_runtime.info(" * %s" % share)
1694 # Start SMB Server
1695 self.srv = SMB_Server.spawn(
1696 # TCP server
1697 port=port,
1698 iface=iface or conf.loopback_name,
1699 verb=verb,
1700 # SMB server
1701 ssp=ssp,
1702 shares=shares,
1703 readonly=readonly,
1704 # SMB arguments
1705 **kwargs,
1706 )
1707
1708 def close(self):
1709 """
1710 Close the smbserver if started in background mode (bg=True)
1711 """
1712 if self.srv:
1713 self.srv.shutdown(socket.SHUT_RDWR)
1714 self.srv.close()
1715
1716
1717if __name__ == "__main__":
1718 from scapy.utils import AutoArgparse
1719
1720 AutoArgparse(smbserver)