4 from Crypto.Hash import CMAC
5 from Crypto.Cipher import AES
16 from config import Config
18 print('FATAL: No configuration file found.')
20 if Config.ntlm_user_file:
21 os.environ['NTLM_USER_FILE'] = Config.ntlm_user_file
23 from smb2.header import *
24 from smb2.error_response import *
25 from smb2.negotiate_protocol import *
26 from smb2.session_setup import *
27 from smb2.session_logoff import *
28 from smb2.tree_connect import *
29 from smb2.tree_disconnect import *
30 from smb2.create import *
31 from smb2.close import *
32 from smb2.read import *
33 from smb2.query_info import *
34 from smb2.query_directory import *
35 from smb2.file_info import *
36 from smb2.filesystem_info import *
37 from smb2.dir_info import *
43 def __init__(self, path, flags, at, **kwargs):
44 self.path = '.' if not path else path
45 self.fd = os.open(self.path, flags, dir_fd=at)
46 _st = os.stat(self.fd)
48 if stat.S_ISDIR(_st.st_mode):
56 return os.fstat(self.fd)
60 with os.scandir(self.fd) as it:
62 _st = _e.stat(follow_symlinks=False)
63 _a = FILE_ATTRIBUTE_SPARSE_FILE
64 if stat.S_ISDIR(_st.st_mode):
65 _a = _a | FILE_ATTRIBUTE_DIRECTORY
66 _de = {'file_index': 0,
67 'creation_time': (0, 0, 0),
68 'last_access_time': (int(_st.st_atime), 0, 0),
69 'last_write_time': (int(_st.st_mtime), 0, 0),
70 'change_time': (int(_st.st_ctime), 0, 0),
71 'end_of_file': _st.st_size,
72 'allocation_size': _st.st_size,
73 'file_attributes': _a,
75 'file_id': _st.st_ino,
76 'file_name': bytes(_e.name, encoding='utf=8'),
78 _dirents.append((_e, _de,))
81 def pread(self, length, offset):
82 return os.pread(self.fd, length, offset)
87 A class for a SMB2 Server
95 def __init__(self, s, **kwargs):
97 self._sp = spnego.server(socket.gethostname())
102 self._last_fid = (0, 0)
103 self.signing_key = None
104 self._use_signing = False
106 print('Socket', self._s)
112 def srv_read(self, hdr, pdu):
116 if not hdr['tree_id'] in self.trees:
117 self._compound_error = Status.INVALID_PARAMETER
118 return (self._compound_error,
119 ErrorResponse.encode({'error_data' : bytes(1)}))
120 _fid = pdu['file_id']
121 if _fid == (0xffffffffffffffff, 0xffffffffffffffff):
122 _fid = self._last_fid
125 _f = self.files[_fid]
127 self._compound_error = Status.INVALID_PARAMETER
128 return (self._compound_error,
129 ErrorResponse.encode({'error_data' : bytes(1)}))
132 if pdu['offset'] >= _st.st_size:
133 self._compound_error = Status.END_OF_FILE
134 return (self._compound_error,
135 ErrorResponse.encode({'error_data' : bytes(1)}))
137 _b = _f.pread(pdu['length'], pdu['offset'])
138 return (Status.SUCCESS,
139 Read.encode(Direction.REPLY,
140 {'data_remaining': 0,
145 def srv_close(self, hdr, pdu):
149 if not hdr['tree_id'] in self.trees:
150 self._compound_error = Status.INVALID_PARAMETER
151 return (self._compound_error,
152 ErrorResponse.encode({'error_data' : bytes(1)}))
153 _fid = pdu['file_id']
154 if _fid == (0xffffffffffffffff, 0xffffffffffffffff):
155 _fid = self._last_fid
158 _f = self.files[_fid]
160 self._compound_error = Status.INVALID_PARAMETER
161 return (self._compound_error,
162 ErrorResponse.encode({'error_data' : bytes(1)}))
165 return (Status.SUCCESS,
166 Close.encode(Direction.REPLY,
171 def srv_query_dir(self, hdr, pdu):
175 if not hdr['tree_id'] in self.trees:
176 self._compound_error = Status.INVALID_PARAMETER
177 return (self._compound_error,
178 ErrorResponse.encode({'error_data' : bytes(1)}))
180 DirInfoClass(pdu['info_class'])
182 print('QueryDir: Can not handle info_type', pdu['info_class'])
183 self._compound_error = Status.INVALID_PARAMETER
184 return (self._compound_error,
185 ErrorResponse.encode({'error_data' : bytes(1)}))
187 _fid = pdu['file_id']
188 if _fid == (0xffffffffffffffff, 0xffffffffffffffff):
189 _fid = self._last_fid
192 _f = self.files[_fid]
194 self._compound_error = Status.INVALID_PARAMETER
195 return (self._compound_error,
196 ErrorResponse.encode({'error_data' : bytes(1)}))
198 if pdu['flags'] & (SMB2_RESTART_SCANS | SMB2_REOPEN):
202 self._compound_error = Status.NO_MORE_FILES
203 return (self._compound_error,
204 ErrorResponse.encode({'error_data' : bytes(1)}))
207 _obl = pdu['output_buffer_length']
210 _i = DirInfo.encode_single(
211 DirInfoClass(pdu['info_class']), _f.de[0][1])
216 _obl = _obl - len(_i)
219 struct.pack_into('<I', _b, _pos, 0)
220 return (Status.SUCCESS,
221 QueryDirectory.encode(Direction.REPLY,
225 def _query_file_info(self, f, c):
227 _a = FILE_ATTRIBUTE_SPARSE_FILE
228 if stat.S_ISDIR(_st.st_mode):
229 _a = _a | FILE_ATTRIBUTE_DIRECTORY
232 if stat.S_IRUSR & _st.st_mode:
233 _ac = _ac | FILE_READ_DATA
234 if stat.S_IWUSR & _st.st_mode:
235 _ac = _ac | FILE_WRITE_DATA
236 if stat.S_IXUSR & _st.st_mode:
237 _ac = _ac | FILE_EXECUTE
238 _fi = FileInfo.encode(FileInfoClass(c),
239 {'creation_time': (0, 0, 0),
240 'last_access_time': (int(_st.st_atime), 0, 0),
241 'last_write_time': (int(_st.st_mtime), 0, 0),
242 'change_time': (int(_st.st_ctime), 0, 0),
243 'file_attributes': _a,
244 'allocation_size': _st.st_size,
245 'end_of_file': _st.st_size,
246 'number_of_links': _st.st_nlink,
248 'directory': 0 if stat.S_ISDIR(_st.st_mode) else 1,
249 'index_number': _st.st_ino,
252 'current_byte_offset': 0,
254 'alignment_requirement': 0,
256 if FileInfoClass(c) == FileInfoClass.ALL_INFORMATION:
257 # windows adds 4 bytes of junk here
258 _fi = _fi + bytearray(4)
260 return (Status.SUCCESS,
261 QueryInfo.encode(Direction.REPLY,
265 def _query_fs_info(self, t, c):
266 if FSInfoClass(c) == FSInfoClass.ATTRIBUTE:
267 _fi = FSInfo.encode(FSInfoClass.ATTRIBUTE,
268 {'attributes': SUPPORTS_OBJECT_IDS | SUPPORTS_SPARSE_FILES | UNICODE_ON_DISK | CASE_PRESERVED_NAMES | CASE_SENSITIVE_SEARCH,
269 'maximum_component_name_length': 255,
270 'file_system_name': 'pysmb3d'
272 return (Status.SUCCESS,
273 QueryInfo.encode(Direction.REPLY,
276 if FSInfoClass(c) == FSInfoClass.DEVICE:
277 _fi = FSInfo.encode(FSInfoClass.DEVICE,
278 {'device_type': DeviceType.DISK.value,
279 'characteristics': DEVICE_IS_MOUNTED
281 return (Status.SUCCESS,
282 QueryInfo.encode(Direction.REPLY,
285 if FSInfoClass(c) == FSInfoClass.VOLUME:
286 _fi = FSInfo.encode(FSInfoClass.VOLUME,
287 {'creation_time': (0, 0, 0),
289 'supports_objects': 0,
292 return (Status.SUCCESS,
293 QueryInfo.encode(Direction.REPLY,
297 if FSInfoClass(c) == FSInfoClass.SECTOR_SIZE:
298 _stfs = os.fstatvfs(t[0])
299 _fi = FSInfo.encode(FSInfoClass.SECTOR_SIZE,
300 {'logical_bytes_per_sector': _stfs.f_bsize,
301 'physical_bytes_per_sector_for_atomicity': _stfs.f_bsize,
302 'physical_bytes_per_sector_for_performance': _stfs.f_bsize,
303 'effective_physical_bytes_per_sector_for_atomicity': _stfs.f_bsize,
304 'flags': SSINFO_FLAGS_ALIGNED_DEVICE | SSINFO_FLAGS_PARTITION_ALIGNED_ON_DEVICE,
305 'byte_offset_for_sector_alignment': 0,
306 'byte_offset_for_partition_alignment': 0
308 return (Status.SUCCESS,
309 QueryInfo.encode(Direction.REPLY,
313 if FSInfoClass(c) == FSInfoClass.FULL_SIZE:
314 _stfs = os.fstatvfs(t[0])
315 _fi = FSInfo.encode(FSInfoClass.FULL_SIZE,
316 {'total_allocation_units': _stfs.f_blocks,
317 'caller_available_allocation_units': _stfs.f_bavail,
318 'actual_available_allocation_units': _stfs.f_bfree,
319 'sectors_per_allocation_unit': 1,
320 'bytes_per_sector': _stfs.f_bsize,
322 return (Status.SUCCESS,
323 QueryInfo.encode(Direction.REPLY,
327 return (Status.INVALID_PARAMETER,
328 ErrorResponse.encode({'error_data' : bytes(1)}))
330 def srv_query_info(self, hdr, pdu):
333 # can only handle INFO_FILE for now
335 if not hdr['tree_id'] in self.trees:
336 self._compound_error = Status.INVALID_PARAMETER
337 return (self._compound_error,
338 ErrorResponse.encode({'error_data' : bytes(1)}))
340 _fid = pdu['file_id']
341 if _fid == (0xffffffffffffffff, 0xffffffffffffffff):
342 _fid = self._last_fid
345 _f = self.files[_fid]
347 self._compound_error = Status.INVALID_PARAMETER
348 return (self._compound_error,
349 ErrorResponse.encode({'error_data' : bytes(1)}))
351 if pdu['info_type'] == SMB2_0_INFO_FILE:
352 return self._query_file_info(_f, pdu['file_info_class'])
354 if pdu['info_type'] == SMB2_0_INFO_FILESYSTEM:
355 return self._query_fs_info(self.trees[hdr['tree_id']], pdu['file_info_class'])
357 print('QueryInfo: Can not handle info type', pdu['info_type'])
358 self._compound_error = Status.INVALID_PARAMETER
359 return (self._compound_error,
360 ErrorResponse.encode({'error_data' : bytes(1)}))
363 def srv_create(self, hdr, pdu):
367 if not hdr['tree_id'] in self.trees:
368 self._compound_error = Status.INVALID_PARAMETER
369 return (self._compound_error,
370 ErrorResponse.encode({'error_data' : bytes(1)}))
371 t = self.trees[hdr['tree_id']]
374 if Disposition(pdu['create_disposition']) != Disposition.OPEN:
375 self._compound_error = Status.INVALID_PARAMETER
376 return (self._compound_error,
377 ErrorResponse.encode({'error_data' : bytes(1)}))
378 if pdu['desired_access'] & (FILE_GENERIC_READ | FILE_GENERIC_ALL | FILE_READ_ATTRIBUTES | FILE_READ_EA | FILE_READ_DATA):
380 flags = flags | os.O_RDONLY
383 f = File(pdu['path'].decode(), flags, t[0])
385 except FileNotFoundError:
386 self._compound_error = Status.OBJECT_NAME_NOT_FOUND
387 return (self._compound_error,
388 ErrorResponse.encode({'error_data' : bytes(1)}))
390 self._last_fid = (self._fileid, self._fileid)
391 self._fileid = self._fileid + 1
392 self.files.update({self._last_fid: f})
394 _a = FILE_ATTRIBUTE_SPARSE_FILE
395 if stat.S_ISDIR(_st.st_mode):
396 _a = _a | FILE_ATTRIBUTE_DIRECTORY
398 return (Status.SUCCESS,
399 Create.encode(Direction.REPLY,
400 {'oplock_level': Oplock.LEVEL_NONE.value,
402 'create_action': Action.OPENED.value,
403 'creation_time': (0, 0, 0),
404 'last_access_time': (int(_st.st_atime), 0, 0),
405 'last_write_time': (int(_st.st_mtime), 0, 0),
406 'change_time': (int(_st.st_ctime), 0, 0),
407 'allocation_size': _st.st_size,
408 'end_of_file': _st.st_size,
409 'file_attributes': _a,
410 'file_id': self._last_fid,
413 def srv_tree_disconn(self, hdr, pdu):
417 if not hdr['tree_id'] in self.trees:
418 self._compound_error = Status.INVALID_PARAMETER
419 return (self._compound_error,
420 ErrorResponse.encode({'error_data' : bytes(1)}))
422 os.close(self.trees[hdr['tree_id']][0])
423 del self.trees[hdr['tree_id']]
424 return (Status.SUCCESS,
425 TreeDisconnect.encode(Direction.REPLY, {}))
427 def srv_tree_conn(self, hdr, pdu):
431 _p = pdu['path'][2:].decode().replace('\\', '/')
432 _p = _p[_p.find('/') + 1:]
433 if not _p in Config.shares:
434 print('Share not found', _p)
435 return (Status.BAD_NETWORK_NAME,
436 ErrorResponse.encode({'error_data' : bytes(1)}))
438 fd = os.open(Config.shares[_p], os.O_RDONLY)
440 hdr['tree_id'] = self._treeid
441 self.trees.update({self._treeid: (fd, _p,)})
442 self._treeid = self._treeid + 1
444 return (Status.SUCCESS,
445 TreeConnect.encode(Direction.REPLY,
446 {'share_type': SMB2_SHARE_TYPE_DISK,
449 'maximal_access': 0x001f00a9,
452 def generate_keys(self, session_key):
453 def derive_key(session_key, label, context):
454 input_key = session_key[:16]
456 hm = hmac.new(input_key, None, hashlib.sha256)
458 struct.pack_into('>I', counter, 0, 1)
464 struct.pack_into('>I', keylen, 0, SMB2_KEY_SIZE * 8)
468 if self.dialect <= VERSION_0210:
469 self.signing_key = session_key
470 elif self.dialect <= VERSION_0302:
471 self.signing_key = derive_key(session_key,
472 bytes('SMB2AESCMAC', encoding='ascii') + bytes(1),
473 bytes('SmbSign', encoding='ascii') + bytes(1),
475 else: # dialect >= VERSION_0311
476 self.signing_key = derive_key(session_key,
477 bytes('SMBSigningKey', encoding='ascii') + bytes(1),
481 def srv_sess_setup(self, hdr, pdu):
483 sm = self._sp.step(pdu['security_buffer'])
484 except Exception as e:
485 if Config.guest_login:
486 print('Authentication failed. Logging in as guest')
488 hdr['session_id'] = self._sesid
489 self.sessions.update({self._sesid: (None,)})
490 self._sesid = self._sesid + 1
491 return (Status.SUCCESS,
492 SessionSetup.encode(Direction.REPLY,
493 {'session_flags': SMB2_SESSION_FLAG_IS_GUEST,
496 print('Exception', e)
499 if self._sp.complete:
500 # self._sp.session_key
501 # self._sp.negotiated_protocol == 'ntlm'
502 print('Authenticated as', self._sp.client_principal)
503 self.generate_keys(self._sp.session_key)
505 # TODO store user/session data in this tuple
506 hdr['session_id'] = self._sesid
507 self.sessions.update({self._sesid: (None,)})
508 self._sesid = self._sesid + 1
510 return (Status.SUCCESS,
511 SessionSetup.encode(Direction.REPLY,
515 return (Status.MORE_PROCESSING_REQUIRED,
516 SessionSetup.encode(Direction.REPLY,
518 'security_buffer': sm,
521 def srv_sess_logoff(self, hdr, pdu):
522 del self.sessions[hdr['session_id']]
523 return (Status.SUCCESS,
524 TreeDisconnect.encode(Direction.REPLY, {}))
526 def srv_neg_prot(self, hdr, pdu):
527 if Config.signing_required:
528 if pdu['security_mode'] & SMB2_NEGOTIATE_SIGNING_ENABLED == 0:
529 print('Signing required but client does not offer signing')
531 if Config.signing_enabled:
532 if pdu['security_mode'] & SMB2_NEGOTIATE_SIGNING_ENABLED != 0:
533 self._use_signing = True
535 if pdu['security_mode'] & SMB2_NEGOTIATE_SIGNING_REQUIRED != 0:
536 print('Signing disabled but client requires it')
539 # Only allow version 3.02
540 if not VERSION_0302 in pdu['dialects']:
541 print('No supported dialect in Negotiate Protocol')
542 return (Status.INVALID_PARAMETER,
543 ErrorResponse.encode({'error_data' : bytes(1)}))
544 self.dialect = VERSION_0302
545 return (Status.SUCCESS,
546 NegotiateProtocol.encode(Direction.REPLY,
547 {'security_mode': SMB2_NEGOTIATE_SIGNING_ENABLED,
548 'dialect_revision': self.dialect,
550 'max_transact_size': 65536,
551 'max_read_size': 65536,
552 'max_write_size': 65536,
553 'system_time': (int(time.time()), 0, 0)}))
555 def VerifySignature(self, hdr, cmd):
556 if self.dialect == VERSION_0202:
557 print('Can not compute signature for 2.02 yet')
561 zsc = cmd[:48] + bytes(16) + cmd[64:]
562 co = CMAC.new(self.signing_key, ciphermod=AES)
566 def ComputeSignature(self, buf):
567 if self.dialect == VERSION_0202:
568 print('Can not compute signature for 2.02 yet')
571 co = CMAC.new(self.signing_key, ciphermod=AES)
576 def ProcessCommands(self, cmds):
578 self._compound_error = Status.SUCCESS
581 # Decode the command pdu
584 if h['flags'] & SIGNED:
585 self.VerifySignature(cmd[0], cmd[1])
587 Command.NEGOTIATE_PROTOCOL: (NegotiateProtocol, self.srv_neg_prot),
588 Command.SESSION_SETUP: (SessionSetup, self.srv_sess_setup),
589 Command.SESSION_LOGOFF: (SessionLogoff, self.srv_sess_logoff),
590 Command.TREE_CONNECT: (TreeConnect, self.srv_tree_conn),
591 Command.TREE_DISCONNECT: (TreeDisconnect, self.srv_tree_disconn),
592 Command.CREATE: (Create, self.srv_create),
593 Command.CLOSE: (Close, self.srv_close),
594 Command.READ: (Read, self.srv_read),
595 Command.QUERY_INFO: (QueryInfo, self.srv_query_info),
596 Command.QUERY_DIRECTORY: (QueryDirectory, self.srv_query_dir),
600 f = f | (h['flags'] & RELATED)
601 _sign = self._use_signing
602 if h['command'] == Command.NEGOTIATE_PROTOCOL.value:
604 if h['command'] == Command.SESSION_SETUP.value:
609 if self._compound_error != Status.SUCCESS:
610 rh = Header.encode({'protocol_id': SMB2_MAGIC,
611 'credit_charge': h['credit_charge'],
612 'status': self._compound_error.value,
613 'command': h['command'],
614 'credit_response': h['credit_request'],
616 'message_id': h['message_id'],
617 'process_id': h['process_id'],
618 'tree_id': h['tree_id'],
619 'session_id': h['session_id']})
621 ErrorResponse.encode({'error_data' : bytes(1)})))
625 c = ct.get(Command(h['command']))
626 req = c[0].decode(Direction.REQUEST, cmd[1][64:])
628 print('Can not handle command', h['command'], 'yet.')
629 rh = Header.encode({'protocol_id': SMB2_MAGIC,
630 'credit_charge': h['credit_charge'],
631 'status': Status.INVALID_PARAMETER.value,
632 'command': h['command'],
633 'credit_response': h['credit_request'],
635 'message_id': h['message_id'],
636 'process_id': h['process_id'],
637 'tree_id': h['tree_id'],
638 'session_id': h['session_id']})
640 ErrorResponse.encode({'error_data' : bytes(1)})))
645 if h['command'] == Command.SESSION_SETUP.value and self._use_signing and rep[0].value == 0:
648 rh = Header.encode({'protocol_id': SMB2_MAGIC,
649 'credit_charge': h['credit_charge'],
650 'status': rep[0].value,
651 'command': h['command'],
652 'credit_response': h['credit_request'],
654 'message_id': h['message_id'],
655 'process_id': h['process_id'],
656 'tree_id': h['tree_id'],
657 'session_id': h['session_id']})
658 r.append((rh, rep[1]))
660 # next command need special handling to write straight into the
661 # encoded buffer and adding padding
664 def SplitBuffer(self, buf):
667 _h = Header.decode(buf[:64])
668 if _h['protocol_id'] != SMB2_MAGIC:
669 print('Not a SMB2 header')
671 if _h['next_command']:
672 cmds.append((_h, buf[:_h['next_command']]))
673 buf = buf[_h['next_command']:]
675 cmds.append((_h, buf[:]))
682 # Read the SPL and data from the socket
686 _b = self._s.recv(4 - len(spl))
688 print('Socket closed by client')
691 _spl = struct.unpack_from('>I', spl, 0)[0]
694 while len(buf) < _spl:
695 _b = self._s.recv(_spl - len(buf))
697 print('Socket closed by client')
702 # Split the buffer into a list of (header, command) tuples
704 cmds = self.SplitBuffer(buf)
707 # Process the commands
709 rep = self.ProcessCommands(cmds)
712 # Concatenate them into a single bytearray, take care of padding
719 for idx, r in enumerate(rep):
720 buf = buf + r[0] + r[1]
723 _pad = ((_len + 7) & 0xfff8) - _len
724 buf = buf + bytearray(_pad)
727 struct.pack_into('<I', buf, _pos + 20, len(buf) - _pos)
728 if buf[_pos + 16] & SIGNED:
729 buf[_pos + 48:_pos + 64] = self.ComputeSignature(buf[_pos:])
735 struct.pack_into('>I', spl, 0, len(buf))
737 l = self._s.send(spl)
740 l = self._s.send(buf)