def _delete_directory_smbprotocol(path, share='C$', conn=None, host=None, username=None, password=None): if conn is None: conn = get_conn(host, username, password) if conn is False: return False log.warn("PATH: %s %s", share, path) tree = conn.tree_connect(share) dir_open = Open(tree, path) delete_msgs = [ dir_open.create( ImpersonationLevel.Impersonation, DirectoryAccessMask.DELETE, FileAttributes.FILE_ATTRIBUTE_DIRECTORY, 0, CreateDisposition.FILE_OPEN, CreateOptions.FILE_DIRECTORY_FILE | CreateOptions.FILE_DELETE_ON_CLOSE, send=False ), dir_open.close(False, send=False) ] delete_reqs = conn.connection.send_compound([x[0] for x in delete_msgs], sid=conn.session.session_id, tid=tree.tree_connect_id, related=True) for i, request in enumerate(delete_reqs): # A SMBResponseException will be raised if something went wrong response = delete_msgs[i][1](request)
def _delete_file_smbprotocol(path, share='C$', conn=None, host=None, username=None, password=None): if conn is None: conn = get_conn(host, username, password) if conn is False: return False tree = conn.tree_connect(share) file_open = Open(tree, path) delete_msgs = [ file_open.create( ImpersonationLevel.Impersonation, FilePipePrinterAccessMask.GENERIC_READ | FilePipePrinterAccessMask.DELETE, FileAttributes.FILE_ATTRIBUTE_NORMAL, ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE, CreateDisposition.FILE_OPEN, CreateOptions.FILE_NON_DIRECTORY_FILE | CreateOptions.FILE_DELETE_ON_CLOSE, send=False ), file_open.close(False, send=False) ] requests = conn.connection.send_compound([x[0] for x in delete_msgs], conn.session.session_id, tree.tree_connect_id, related=True) responses = [] for i, request in enumerate(requests): # A SMBResponseException will be raised if something went wrong response = delete_msgs[i][1](request) responses.append(response)
def open_file(cls, tree, file): file = cls.normalize_filename(file) # ensure file is created, get maximal access, and set everybody read access max_req = SMB2CreateContextRequest() max_req[ "buffer_name"] = CreateContextName.SMB2_CREATE_QUERY_MAXIMAL_ACCESS_REQUEST max_req["buffer_data"] = SMB2CreateQueryMaximalAccessRequest() # create security buffer that sets the ACL for everyone to have read access everyone_sid = SIDPacket() everyone_sid.from_string("S-1-1-0") ace = AccessAllowedAce() ace["mask"] = AccessMask.GENERIC_ALL ace["sid"] = everyone_sid acl = AclPacket() acl["aces"] = [ace] sec_desc = SMB2CreateSDBuffer() sec_desc["control"].set_flag(SDControl.SELF_RELATIVE) sec_desc.set_dacl(acl) sd_buffer = SMB2CreateContextRequest() sd_buffer["buffer_name"] = CreateContextName.SMB2_CREATE_SD_BUFFER sd_buffer["buffer_data"] = sec_desc create_contexts = [max_req, sd_buffer] file_open = Open(tree, file) open_info = file_open.create( ImpersonationLevel.Impersonation, FilePipePrinterAccessMask.GENERIC_READ | FilePipePrinterAccessMask.GENERIC_WRITE, FileAttributes.FILE_ATTRIBUTE_NORMAL, ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE, CreateDisposition.FILE_OVERWRITE_IF, CreateOptions.FILE_NON_DIRECTORY_FILE, ) return file_open
def cleanup(self): """ Cleans up any old services or payloads that may have been left behind on a previous failure. This will search C:\\Windows for any files starting with PAExec-*.exe and delete them. It will also stop and remove any services that start with PAExec-* if they exist. Before calling this function, the connect() function must have already been called. """ scmr = self._service._scmr services = scmr.enum_services_status_w( self._service._scmr_handle, ServiceType.SERVICE_WIN32_OWN_PROCESS, EnumServiceState.SERVICE_STATE_ALL) for service in services: if service['service_name'].lower().startswith("paexec"): svc = Service(service['service_name'], self.session) svc.open() svc.delete() smb_tree = TreeConnect(self.session, r"\\%s\ADMIN$" % self.connection.server_name) smb_tree.connect() share = Open(smb_tree, "") query_msgs = [ share.create(ImpersonationLevel.Impersonation, DirectoryAccessMask.FILE_READ_ATTRIBUTES | DirectoryAccessMask.SYNCHRONIZE | DirectoryAccessMask.FILE_LIST_DIRECTORY, FileAttributes.FILE_ATTRIBUTE_DIRECTORY, ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE | ShareAccess.FILE_SHARE_DELETE, CreateDisposition.FILE_OPEN, CreateOptions.FILE_DIRECTORY_FILE, send=False), share.query_directory("PAExec-*.exe", FileInformationClass.FILE_NAMES_INFORMATION, send=False), share.close(False, send=False) ] query_reqs = self.connection.send_compound([x[0] for x in query_msgs], self.session.session_id, smb_tree.tree_connect_id, related=True) # receive response for open and close query_msgs[0][1](query_reqs[0]) query_msgs[2][1](query_reqs[2]) try: # receive the response for query_directory files = query_msgs[1][1](query_reqs[1]) except SMBResponseException as exc: if exc.status != NtStatus.STATUS_NO_SUCH_FILE: raise exc files = [] for file in files: file_name = file['file_name'].get_value().decode('utf-16-le') self._delete_file(smb_tree, file_name)
def test_change_notify_on_a_file(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3]) connection.connect() session = Session(connection, smb_real[0], smb_real[1]) tree = TreeConnect(session, smb_real[4]) open = Open(tree, "file-watch.txt") try: session.connect() tree.connect() open.create( ImpersonationLevel.Impersonation, FilePipePrinterAccessMask.MAXIMUM_ALLOWED, FileAttributes.FILE_ATTRIBUTE_NORMAL, ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE | ShareAccess.FILE_SHARE_DELETE, CreateDisposition.FILE_OPEN_IF, CreateOptions.FILE_NON_DIRECTORY_FILE) watcher = FileSystemWatcher(open) watcher.start(CompletionFilter.FILE_NOTIFY_CHANGE_FILE_NAME) with pytest.raises(InvalidParameter): watcher.wait() finally: connection.disconnect(True)
def test_change_notify_on_a_file(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3]) connection.connect() session = Session(connection, smb_real[0], smb_real[1]) tree = TreeConnect(session, smb_real[4]) open = Open(tree, "file-watch.txt") try: session.connect() tree.connect() open.create( ImpersonationLevel.Impersonation, FilePipePrinterAccessMask.MAXIMUM_ALLOWED, FileAttributes.FILE_ATTRIBUTE_NORMAL, ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE | ShareAccess.FILE_SHARE_DELETE, CreateDisposition.FILE_OPEN_IF, CreateOptions.FILE_NON_DIRECTORY_FILE) watcher = FileSystemWatcher(open) watcher.start(CompletionFilter.FILE_NOTIFY_CHANGE_FILE_NAME) expected = "Received unexpected status from the server: (3221225485) STATUS_INVALID_PARAMETER" with pytest.raises(SMBResponseException, match=re.escape(expected)): watcher.wait() finally: connection.disconnect(True)
def open_directory(tree, name, create=False): # ensure directory is created dir_open = Open(tree, name) if create: dir_open.create( ImpersonationLevel.Impersonation, DirectoryAccessMask.GENERIC_READ | DirectoryAccessMask.GENERIC_WRITE, FileAttributes.FILE_ATTRIBUTE_DIRECTORY, ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE, CreateDisposition.FILE_OPEN_IF, CreateOptions.FILE_DIRECTORY_FILE ) return dir_open
def create_service(self): # check if the service exists and delete it log.debug("Ensuring service is deleted before starting") self._service.delete() # copy across the PAExec payload to C:\Windows\ smb_tree = TreeConnect(self.session, r"\\%s\ADMIN$" % self.connection.server_name) log.info("Connecting to SMB Tree %s" % smb_tree.share_name) smb_tree.connect() paexec_file = Open(smb_tree, self._exe_file) log.debug("Creating open to PAExec file") paexec_file.open(ImpersonationLevel.Impersonation, FilePipePrinterAccessMask.FILE_WRITE_DATA, FileAttributes.FILE_ATTRIBUTE_NORMAL, ShareAccess.FILE_SHARE_READ, CreateDisposition.FILE_OVERWRITE_IF, CreateOptions.FILE_NON_DIRECTORY_FILE) log.info("Creating PAExec executable at %s\\%s" % (smb_tree.share_name, self._exe_file)) for (data, o) in paexec_out_stream(self.connection.max_write_size): paexec_file.write(data, o) log.debug("Closing open to PAExec file") paexec_file.close(False) log.info("Disconnecting from SMB Tree %s" % smb_tree.share_name) smb_tree.disconnect() # create the PAExec service service_path = r'"%SystemRoot%\{0}" -service'.format(self._exe_file) log.info("Creating PAExec service %s" % self.service_name) self._service.create(service_path)
def _get_paexec_files_and_services(self, client): server = os.environ['PYPSEXEC_SERVER'] username = os.environ.get('PYPSEXEC_USERNAME', None) password = os.environ.get('PYPSEXEC_PASSWORD', None) paexec_services = [] # need to close and reopen the connection to ensure deletes are # processed client.disconnect() client = Client(server, username=username, password=password) client.connect() scmr = client._service._scmr scmr_handle = client._service._scmr_handle services = scmr.enum_services_status_w(scmr_handle, ServiceType. SERVICE_WIN32_OWN_PROCESS, EnumServiceState. SERVICE_STATE_ALL) for service in services: if service['service_name'].lower().startswith("paexec"): paexec_services.append(service['service_name']) smb_tree = TreeConnect(client.session, r"\\%s\ADMIN$" % client.connection.server_name) smb_tree.connect() share = Open(smb_tree, "") share.create(ImpersonationLevel.Impersonation, DirectoryAccessMask.FILE_READ_ATTRIBUTES | DirectoryAccessMask.SYNCHRONIZE | DirectoryAccessMask.FILE_LIST_DIRECTORY, FileAttributes.FILE_ATTRIBUTE_DIRECTORY, ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE | ShareAccess.FILE_SHARE_DELETE, CreateDisposition.FILE_OPEN, CreateOptions.FILE_DIRECTORY_FILE) try: paexec_files = share.query_directory("PAExec-*.exe", FileInformationClass. FILE_NAMES_INFORMATION) except SMBResponseException as exc: if exc.status != NtStatus.STATUS_NO_SUCH_FILE: raise exc paexec_files = [] return client, paexec_services, paexec_files
def _resolve_dfs(raw_io): """ Resolves a DFS path for a failed Open request. :param raw_io: The SMBRawIO to resolve the DFS path for. :return: A new Open for each DFS target that was resolved. """ if not raw_io.fd.tree_connect.is_dfs_share: return # Path is on a DFS root that is linked to another server. client_config = ClientConfig() raw_path = raw_io.name referral = dfs_request(raw_io.fd.tree_connect, raw_path[1:]) client_config.cache_referral(referral) info = client_config.lookup_referral( [p for p in raw_path.split("\\") if p]) connection_kwargs = getattr(raw_io, '_%s__kwargs' % type(raw_io).__name__, {}) for target in info: new_path = raw_path.replace(info.dfs_path, target.target_path, 1) try: tree, fd_path = get_smb_tree(new_path, **connection_kwargs) except SMBResponseException as link_exc: log.warning("Failed to connect to DFS link target %s: %s" % (str(target), link_exc)) continue # Record the target that worked for future reference. info.target_hint = target yield Open(tree, fd_path)
def open_pipe(tree, name, access_mask, fsctl_wait=False): """ Opened the requested pipe with the access mask specified. Will attempt to connect 3 times before failing in case the pipe's don't exist at the time. :param tree: The SMB TreeConnect of IPC$ to connect to :param name: The name of the pipe to connect to :param access_mask: The access mask to apply to the Open :param fsctl_wait: Runs the FSCTL_PIPE_WAIT command over an SMB2IOCTLRequest :return: A connected Open() object of the pipe """ log.info("Creating SMB Open for pipe: %s" % name) pipe = Open(tree, name) if fsctl_wait: wait_pipe = SMB2IOCTLRequest() wait_pipe['ctl_code'] = CtlCode.FSCTL_PIPE_WAIT wait_pipe['file_id'] = b"\xff" * 16 wait_pipe['flags'] = IOCTLFlags.SMB2_0_IOCTL_IS_FSCTL fsctl_data = FSCTLPipeWait() fsctl_data['name'] = name.encode('utf-16-le') wait_pipe['buffer'] = fsctl_data log.info("Sending FSCTL_PIPE_WAIT for pipe %s" % name) log.debug(str(fsctl_data)) request = tree.session.connection.send( wait_pipe, sid=tree.session.session_id, tid=tree.tree_connect_id ) log.info("Receiving FSCTL_PIPE_WAIT response for pipe: %s" % name) tree.session.connection.receive(request) pipe.create(ImpersonationLevel.Impersonation, access_mask, FileAttributes.FILE_ATTRIBUTE_NORMAL, 0, CreateDisposition.FILE_OPEN, CreateOptions.FILE_NON_DIRECTORY_FILE | CreateOptions.FILE_SYNCHRONOUS_IO_NONALERT) return pipe
def __init__(self, path, mode='r', share_access=None, desired_access=None, file_attributes=None, create_options=0, buffer_size=MAX_PAYLOAD_SIZE, **kwargs): tree, fd_path = get_smb_tree(path, **kwargs) self.share_access = share_access self.fd = Open(tree, fd_path) self._mode = mode self._name = path self._offset = 0 self._flush = False self._buffer_size = buffer_size self.__kwargs = kwargs # Used in open for DFS referrals if desired_access is None: desired_access = 0 # While we can open a directory, the values for FilePipePrinterAccessMask also apply to Dirs so just use # the same enum to simplify code. if 'r' in self.mode or '+' in self.mode: desired_access |= FilePipePrinterAccessMask.FILE_READ_DATA | \ FilePipePrinterAccessMask.FILE_READ_ATTRIBUTES | \ FilePipePrinterAccessMask.FILE_READ_EA if 'w' in self.mode or 'x' in self.mode or 'a' in self.mode or '+' in self.mode: desired_access |= FilePipePrinterAccessMask.FILE_WRITE_DATA | \ FilePipePrinterAccessMask.FILE_WRITE_ATTRIBUTES | \ FilePipePrinterAccessMask.FILE_WRITE_EA self._desired_access = desired_access if file_attributes is None: file_attributes = FileAttributes.FILE_ATTRIBUTE_DIRECTORY if self.FILE_TYPE == 'dir' \ else FileAttributes.FILE_ATTRIBUTE_NORMAL self._file_attributes = file_attributes self._create_options = create_options self._create_options |= { 'dir': CreateOptions.FILE_DIRECTORY_FILE, 'file': CreateOptions.FILE_NON_DIRECTORY_FILE, }.get(self.FILE_TYPE, 0) super(SMBRawIO, self).__init__()
def test_change_notify_cancel(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3]) connection.connect() session = Session(connection, smb_real[0], smb_real[1], require_encryption=False) tree = TreeConnect(session, smb_real[4]) open = Open(tree, "directory-watch") try: session.connect() tree.connect() open.create( ImpersonationLevel.Impersonation, DirectoryAccessMask.MAXIMUM_ALLOWED, FileAttributes.FILE_ATTRIBUTE_DIRECTORY, ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE | ShareAccess.FILE_SHARE_DELETE, CreateDisposition.FILE_OPEN_IF, CreateOptions.FILE_DIRECTORY_FILE) watcher = FileSystemWatcher(open) watcher.start(CompletionFilter.FILE_NOTIFY_CHANGE_FILE_NAME) assert watcher.result is None assert watcher.response_event.is_set() is False # Makes sure that we cancel after the async response has been returned from the server. while watcher._request.async_id is None: pass assert watcher.result is None watcher.cancel() watcher.wait() assert watcher.cancelled is True assert watcher.result is None # Make sure it doesn't cause any weird errors when calling it again watcher.cancel() finally: connection.disconnect(True)
def _delete_file(self, tree, name): file_open = Open(tree, name) msgs = [ file_open.create(ImpersonationLevel.Impersonation, FilePipePrinterAccessMask.DELETE, FileAttributes.FILE_ATTRIBUTE_NORMAL, 0, CreateDisposition.FILE_OPEN_IF, CreateOptions.FILE_NON_DIRECTORY_FILE | CreateOptions.FILE_DELETE_ON_CLOSE, send=False), file_open.close(get_attributes=False, send=False) ] reqs = self.connection.send_compound([x[0] for x in msgs], sid=self.session.session_id, tid=tree.tree_connect_id, related=True) # remove the responses from the SMB outstanding requests msgs[0][1](reqs[0]) msgs[1][1](reqs[1])
def test_change_notify_underlying_close(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3]) connection.connect() session = Session(connection, smb_real[0], smb_real[1]) tree = TreeConnect(session, smb_real[4]) open = Open(tree, "directory-watch") try: session.connect() tree.connect() open.create( ImpersonationLevel.Impersonation, DirectoryAccessMask.MAXIMUM_ALLOWED, FileAttributes.FILE_ATTRIBUTE_DIRECTORY, ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE | ShareAccess.FILE_SHARE_DELETE, CreateDisposition.FILE_OPEN_IF, CreateOptions.FILE_DIRECTORY_FILE) watcher = FileSystemWatcher(open) watcher.start(CompletionFilter.FILE_NOTIFY_CHANGE_FILE_NAME) assert watcher.result is None assert watcher.response_event.is_set() is False open.close() expected = "Received unexpected status from the server: (267) STATUS_NOTIFY_CLEANUP" with pytest.raises(SMBResponseException, match=re.escape(expected)): watcher.wait() finally: connection.disconnect(True)
def _delete_file(self, tree, name): file_open = Open(tree, name) file_open.open( ImpersonationLevel.Impersonation, FilePipePrinterAccessMask.DELETE, FileAttributes.FILE_ATTRIBUTE_NORMAL, 0, CreateDisposition.FILE_OPEN_IF, CreateOptions.FILE_NON_DIRECTORY_FILE | CreateOptions.FILE_DELETE_ON_CLOSE) file_open.close(get_attributes=False)
def _remove_file(self, tree, name): file_open = Open(tree, name) file_open.create( ImpersonationLevel.Impersonation, FilePipePrinterAccessMask.DELETE, FileAttributes.FILE_ATTRIBUTE_NORMAL, ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE | ShareAccess.FILE_SHARE_DELETE, CreateDisposition.FILE_OPEN_IF, CreateOptions.FILE_NON_DIRECTORY_FILE | CreateOptions.FILE_DELETE_ON_CLOSE), file_open.close()
def test_change_notify_no_data(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3]) connection.connect() session = Session(connection, smb_real[0], smb_real[1]) tree = TreeConnect(session, smb_real[4]) open = Open(tree, "directory-watch") try: session.connect() tree.connect() open.create( ImpersonationLevel.Impersonation, DirectoryAccessMask.MAXIMUM_ALLOWED, FileAttributes.FILE_ATTRIBUTE_DIRECTORY, ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE | ShareAccess.FILE_SHARE_DELETE, CreateDisposition.FILE_OPEN_IF, CreateOptions.FILE_DIRECTORY_FILE) self._remove_file(tree, "directory-watch\\created file") watcher = FileSystemWatcher(open) watcher.start(CompletionFilter.FILE_NOTIFY_CHANGE_FILE_NAME, output_buffer_length=0) assert watcher.result is None assert watcher.response_event.is_set() is False # Run the wait in a separate thread so we can create the dir def watcher_wait(): watcher.wait() watcher_wait_thread = threading.Thread(target=watcher_wait) watcher_wait_thread.daemon = True watcher_wait_thread.start() def watcher_event(): watcher.response_event.wait() watcher_event_thread = threading.Thread(target=watcher_event) watcher_event_thread.daemon = True watcher_event_thread.start() # Create the new file file_open = Open(tree, "directory-watch\\created file") file_open.create( ImpersonationLevel.Impersonation, FilePipePrinterAccessMask.MAXIMUM_ALLOWED, FileAttributes.FILE_ATTRIBUTE_NORMAL, ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE | ShareAccess.FILE_SHARE_DELETE, CreateDisposition.FILE_OPEN_IF, CreateOptions.FILE_NON_DIRECTORY_FILE) file_open.close() watcher_wait_thread.join(timeout=2) watcher_event_thread.join(timeout=2) assert watcher_wait_thread.is_alive() is False assert watcher_event_thread.is_alive() is False assert watcher.response_event.is_set() assert watcher.result == [] open.close() finally: connection.disconnect(True)
def test_change_notify_on_dir_compound(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3]) connection.connect() # Cannot use encryption as Samba has a bug where the transform response has the wrong Session Id. Also there's # a special edge case of testing the Session Id of the plaintext response with signatures so don't use # encryption. # https://bugzilla.samba.org/show_bug.cgi?id=14189 session = Session(connection, smb_real[0], smb_real[1], require_encryption=False) tree = TreeConnect(session, smb_real[4]) open = Open(tree, "directory-watch") try: session.connect() tree.connect() # Ensure the dir is clean of files. open.create( ImpersonationLevel.Impersonation, DirectoryAccessMask.MAXIMUM_ALLOWED, FileAttributes.FILE_ATTRIBUTE_DIRECTORY, ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE | ShareAccess.FILE_SHARE_DELETE, CreateDisposition.FILE_OPEN_IF, CreateOptions.FILE_DIRECTORY_FILE) self._remove_file(tree, "directory-watch\\created file") open.close() watcher = FileSystemWatcher(open) messages = [ open.create(ImpersonationLevel.Impersonation, DirectoryAccessMask.MAXIMUM_ALLOWED, FileAttributes.FILE_ATTRIBUTE_DIRECTORY, ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE | ShareAccess.FILE_SHARE_DELETE, CreateDisposition.FILE_OPEN_IF, CreateOptions.FILE_DIRECTORY_FILE, send=False), watcher.start(CompletionFilter.FILE_NOTIFY_CHANGE_FILE_NAME, send=False) ] assert watcher.result is None assert watcher.response_event.is_set() is False requests = connection.send_compound([m[0] for m in messages], sid=session.session_id, tid=tree.tree_connect_id, related=True) [messages[i][1](req) for i, req in enumerate(requests)] # Run the wait in a separate thread so we can create the dir def watcher_wait(): watcher.wait() watcher_wait_thread = threading.Thread(target=watcher_wait) watcher_wait_thread.daemon = True watcher_wait_thread.start() def watcher_event(): watcher.response_event.wait() watcher_event_thread = threading.Thread(target=watcher_event) watcher_event_thread.daemon = True watcher_event_thread.start() # Create the new file file_open = Open(tree, "directory-watch\\created file") file_open.create( ImpersonationLevel.Impersonation, FilePipePrinterAccessMask.MAXIMUM_ALLOWED, FileAttributes.FILE_ATTRIBUTE_NORMAL, ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE | ShareAccess.FILE_SHARE_DELETE, CreateDisposition.FILE_OPEN_IF, CreateOptions.FILE_NON_DIRECTORY_FILE) file_open.close() watcher_wait_thread.join(timeout=2) watcher_event_thread.join(timeout=2) assert watcher_wait_thread.is_alive() is False assert watcher_event_thread.is_alive() is False assert watcher.response_event.is_set() assert len(watcher.result) == 1 assert watcher.result[0]['file_name'].get_value( ) == u"created file" assert watcher.result[0]['action'].get_value( ) == FileAction.FILE_ACTION_ADDED open.close() finally: connection.disconnect(True)
ace['mask'] = AccessMask.GENERIC_ALL ace['sid'] = everyone_sid acl = AclPacket() acl['aces'] = [ace] sec_desc = SMB2CreateSDBuffer() sec_desc['control'].set_flag(SDControl.SELF_RELATIVE) sec_desc.set_dacl(acl) sd_buffer = SMB2CreateContextRequest() sd_buffer['buffer_name'] = CreateContextName.SMB2_CREATE_SD_BUFFER sd_buffer['buffer_data'] = sec_desc create_contexts = [max_req, sd_buffer] file_open = Open(tree, file_name) open_info = file_open.create( ImpersonationLevel.Impersonation, FilePipePrinterAccessMask.GENERIC_READ | FilePipePrinterAccessMask.GENERIC_WRITE, FileAttributes.FILE_ATTRIBUTE_NORMAL, ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE, CreateDisposition.FILE_OVERWRITE_IF, CreateOptions.FILE_NON_DIRECTORY_FILE, create_contexts) # as the raw structure 'maximal_access' is an IntField, we create our own # flag field, set the value and get the human readble string max_access = FlagField(size=4, flag_type=FilePipePrinterAccessMask, flag_strict=False) max_access.set_value(open_info[0]['maximal_access'].get_value())
class SMBRawIO(io.RawIOBase): FILE_TYPE = None # 'file', 'dir', or None (for unknown) _INVALID_MODE = '' def __init__(self, path, mode='r', share_access=None, desired_access=None, file_attributes=None, create_options=0, **kwargs): tree, fd_path = get_smb_tree(path, **kwargs) self.share_access = share_access self.fd = Open(tree, fd_path) self._mode = mode self._name = path self._offset = 0 self._flush = False self.__kwargs = kwargs # Used in open for DFS referrals if desired_access is None: desired_access = 0 # While we can open a directory, the values for FilePipePrinterAccessMask also apply to Dirs so just use # the same enum to simplify code. if 'r' in self.mode or '+' in self.mode: desired_access |= FilePipePrinterAccessMask.FILE_READ_DATA | \ FilePipePrinterAccessMask.FILE_READ_ATTRIBUTES | \ FilePipePrinterAccessMask.FILE_READ_EA if 'w' in self.mode or 'x' in self.mode or 'a' in self.mode or '+' in self.mode: desired_access |= FilePipePrinterAccessMask.FILE_WRITE_DATA | \ FilePipePrinterAccessMask.FILE_WRITE_ATTRIBUTES | \ FilePipePrinterAccessMask.FILE_WRITE_EA self._desired_access = desired_access if file_attributes is None: file_attributes = FileAttributes.FILE_ATTRIBUTE_DIRECTORY if self.FILE_TYPE == 'dir' \ else FileAttributes.FILE_ATTRIBUTE_NORMAL self._file_attributes = file_attributes self._create_options = create_options self._create_options |= { 'dir': CreateOptions.FILE_DIRECTORY_FILE, 'file': CreateOptions.FILE_NON_DIRECTORY_FILE, }.get(self.FILE_TYPE, 0) super(SMBRawIO, self).__init__() def __enter__(self): self.open() return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() @property def closed(self): return not self.fd.connected @property def mode(self): return self._mode @property def name(self): return self._name def close(self, transaction=None): if transaction: transaction += self.fd.close(send=False) else: self.fd.close() def flush(self): if self._flush and self.FILE_TYPE != 'pipe': self.fd.flush() def open(self, transaction=None): if not self.closed: return share_access = _parse_share_access(self.share_access, self.mode) create_disposition = _parse_mode(self.mode, invalid=self._INVALID_MODE) try: # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wpo/feeb3122-cfe0-4b34-821d-e31c036d763c # Impersonation on SMB has little meaning when opening files but is important if using RPC so set to a sane # default of Impersonation. open_result = self.fd.create( ImpersonationLevel.Impersonation, self._desired_access, self._file_attributes, share_access, create_disposition, self._create_options, send=(transaction is None), ) except (PathNotCovered, ObjectNameNotFound, ObjectPathNotFound) as exc: # The MS-DFSC docs status that STATUS_PATH_NOT_COVERED is used when encountering a link to a different # server but Samba seems to return the generic name or path not found. if not self.fd.tree_connect.is_dfs_share: raise SMBOSError(exc.status, self.name) # Path is on a DFS root that is linked to another server. client_config = ClientConfig() referral = dfs_request(self.fd.tree_connect, self.name[1:]) client_config.cache_referral(referral) info = client_config.lookup_referral([p for p in self.name.split("\\") if p]) for target in info: new_path = self.name.replace(info.dfs_path, target.target_path, 1) try: tree, fd_path = get_smb_tree(new_path, **self.__kwargs) self.fd = Open(tree, fd_path) self.open(transaction=transaction) except SMBResponseException as link_exc: log.warning("Failed to connect to DFS link target %s: %s" % (str(target), link_exc)) else: # Record the target that worked for future reference. info.target_hint = target break else: # None of the targets worked so raise the original error. raise SMBOSError(exc.status, self.name) return except SMBResponseException as exc: raise SMBOSError(exc.status, self.name) if transaction is not None: transaction += open_result elif 'a' in self.mode and self.FILE_TYPE != 'pipe': self._offset = self.fd.end_of_file def readable(self): """ True if file was opened in a read mode. """ return 'r' in self.mode or '+' in self.mode def seek(self, offset, whence=SEEK_SET): """ Move to new file position and return the file position. Argument offset is a byte count. Optional argument whence defaults to SEEK_SET or 0 (offset from start of file, offset should be >= 0); other values are SEEK_CUR or 1 (move relative to current position, positive or negative), and SEEK_END or 2 (move relative to end of file, usually negative, although many platforms allow seeking beyond the end of a file). Note that not all file objects are seekable. """ seek_offset = { SEEK_SET: 0, SEEK_CUR: self._offset, SEEK_END: self.fd.end_of_file, }[whence] self._offset = seek_offset + offset return self._offset def seekable(self): """ True if file supports random-access. """ return True def tell(self): """ Current file position. Can raise OSError for non seekable files. """ return self._offset def truncate(self, size): """ Truncate the file to at most size bytes and return the truncated size. Size defaults to the current file position, as returned by tell(). The current file position is changed to the value of size. """ with SMBFileTransaction(self) as transaction: eof_info = FileEndOfFileInformation() eof_info['end_of_file'] = size set_info(transaction, eof_info) self.fd.end_of_file = size self._flush = True return size def writable(self): """ True if file was opened in a write mode. """ return 'w' in self.mode or 'x' in self.mode or 'a' in self.mode or '+' in self.mode def readall(self): """ Read and return all the bytes from the stream until EOF, using multiple calls to the stream if necessary. :return: The byte string read from the SMB file. """ data = bytearray() while True: read_length = min( # We always want to be reading a minimum of 64KiB. max(self.fd.end_of_file - self._offset, MAX_PAYLOAD_SIZE), self.fd.connection.max_read_size # We can never read more than this. ) buffer = bytearray(b'\x00' * read_length) bytes_read = self.readinto(buffer) if not bytes_read: break data += buffer[:bytes_read] return bytes(data) def readinto(self, b): """ Read bytes into a pre-allocated, writable bytes-like object b, and return the number of bytes read. This may read less bytes than requested as it depends on the negotiated read size and SMB credits available. :param b: bytes-like object to read the data into. :return: The number of bytes read. """ if self._offset >= self.fd.end_of_file and self.FILE_TYPE != 'pipe': return 0 chunk_size, credit_request = _chunk_size(self.fd.connection, len(b), 'read') read_msg, recv_func = self.fd.read(self._offset, chunk_size, send=False) request = self.fd.connection.send( read_msg, sid=self.fd.tree_connect.session.session_id, tid=self.fd.tree_connect.tree_connect_id, credit_request=credit_request ) try: file_bytes = recv_func(request) except PipeBroken: # A pipe will block until it returns the data available or was closed/broken. file_bytes = b"" b[:len(file_bytes)] = file_bytes if self.FILE_TYPE != 'pipe': self._offset += len(file_bytes) return len(file_bytes) def write(self, b): """ Write buffer b to file, return number of bytes written. Only makes one system call, so not all of the data may be written. The number of bytes actually written is returned. This can be less than the length of b as it depends on the underlying connection. """ chunk_size, credit_request = _chunk_size(self.fd.connection, len(b), 'write') # Python 2 compat, can be removed and just use the else statement. if isinstance(b, memoryview): data = b[:chunk_size].tobytes() else: data = bytes(b[:chunk_size]) write_msg, recv_func = self.fd.write(data, offset=self._offset, send=False) request = self.fd.connection.send( write_msg, sid=self.fd.tree_connect.session.session_id, tid=self.fd.tree_connect.tree_connect_id, credit_request=credit_request ) bytes_written = recv_func(request) if self.FILE_TYPE != 'pipe': self._offset += bytes_written self.fd.end_of_file = max(self.fd.end_of_file, self._offset) self._flush = True return bytes_written
def open(self, transaction=None): if not self.closed: return share_access = _parse_share_access(self.share_access, self.mode) create_disposition = _parse_mode(self.mode, invalid=self._INVALID_MODE) try: # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wpo/feeb3122-cfe0-4b34-821d-e31c036d763c # Impersonation on SMB has little meaning when opening files but is important if using RPC so set to a sane # default of Impersonation. open_result = self.fd.create( ImpersonationLevel.Impersonation, self._desired_access, self._file_attributes, share_access, create_disposition, self._create_options, send=(transaction is None), ) except (PathNotCovered, ObjectNameNotFound, ObjectPathNotFound) as exc: # The MS-DFSC docs status that STATUS_PATH_NOT_COVERED is used when encountering a link to a different # server but Samba seems to return the generic name or path not found. if not self.fd.tree_connect.is_dfs_share: raise SMBOSError(exc.status, self.name) # Path is on a DFS root that is linked to another server. client_config = ClientConfig() referral = dfs_request(self.fd.tree_connect, self.name[1:]) client_config.cache_referral(referral) info = client_config.lookup_referral([p for p in self.name.split("\\") if p]) for target in info: new_path = self.name.replace(info.dfs_path, target.target_path, 1) try: tree, fd_path = get_smb_tree(new_path, **self.__kwargs) self.fd = Open(tree, fd_path) self.open(transaction=transaction) except SMBResponseException as link_exc: log.warning("Failed to connect to DFS link target %s: %s" % (str(target), link_exc)) else: # Record the target that worked for future reference. info.target_hint = target break else: # None of the targets worked so raise the original error. raise SMBOSError(exc.status, self.name) return except SMBResponseException as exc: raise SMBOSError(exc.status, self.name) if transaction is not None: transaction += open_result elif 'a' in self.mode and self.FILE_TYPE != 'pipe': self._offset = self.fd.end_of_file
def session(self): server = os.environ.get('PYPSEXEC_SERVER', None) username = os.environ.get('PYPSEXEC_USERNAME', None) password = os.environ.get('PYPSEXEC_PASSWORD', None) if server and username and password: connection = Connection(uuid.uuid4(), server, 445) session = Session(connection, username, password) tree = TreeConnect(session, r"\\%s\ADMIN$" % server) paexec_file = Open(tree, "PAExec.exe") connection.connect() try: session.connect() tree.connect() paexec_file.open(ImpersonationLevel.Impersonation, FilePipePrinterAccessMask.FILE_WRITE_DATA, FileAttributes.FILE_ATTRIBUTE_NORMAL, ShareAccess.FILE_SHARE_READ, CreateDisposition.FILE_OVERWRITE_IF, CreateOptions.FILE_NON_DIRECTORY_FILE) paexec_file.write(binascii.unhexlify(PAEXEC_DATA), 0) paexec_file.close(get_attributes=False) yield session finally: paexec_file.open(ImpersonationLevel.Impersonation, FilePipePrinterAccessMask.DELETE, FileAttributes.FILE_ATTRIBUTE_NORMAL, 0, CreateDisposition.FILE_OVERWRITE_IF, CreateOptions.FILE_DELETE_ON_CLOSE) paexec_file.close(get_attributes=False) connection.disconnect(True) else: pytest.skip("PYPSEXEC_SERVER, PYPSEXEC_USERNAME, PYPSEXEC_PASSWORD" " environment variables were not set. Integration " "tests will be skipped")
def __init__(self, smb_session): # connect to the IPC tree and open a handle at svcctl self.tree = TreeConnect( smb_session, r"\\%s\IPC$" % smb_session.connection.server_name) self.handle = Open(self.tree, "svcctl") self.call_id = 0
username = "******" password = "******" share = r"\\%s\share" % server dir_name = "directory" connection = Connection(uuid.uuid4(), server, port) connection.connect() try: session = Session(connection, username, password) session.connect() tree = TreeConnect(session, share) tree.connect() # ensure directory is created dir_open = Open(tree, dir_name) dir_open.create( ImpersonationLevel.Impersonation, DirectoryAccessMask.GENERIC_READ | DirectoryAccessMask.GENERIC_WRITE, FileAttributes.FILE_ATTRIBUTE_DIRECTORY, ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE, CreateDisposition.FILE_OPEN_IF, CreateOptions.FILE_DIRECTORY_FILE) # create some files in dir and query the contents as part of a compound # request directory_file = Open(tree, r"%s\file.txt" % dir_name) directory_file.create( ImpersonationLevel.Impersonation, FilePipePrinterAccessMask.GENERIC_WRITE | FilePipePrinterAccessMask.DELETE, FileAttributes.FILE_ATTRIBUTE_NORMAL, ShareAccess.FILE_SHARE_READ,
class SMBRawIO(io.RawIOBase): FILE_TYPE = None # 'file', 'dir', or None (for unknown) _INVALID_MODE = '' def __init__(self, path, mode='r', share_access=None, desired_access=None, file_attributes=None, create_options=0, buffer_size=MAX_PAYLOAD_SIZE, **kwargs): tree, fd_path = get_smb_tree(path, **kwargs) self.share_access = share_access self.fd = Open(tree, fd_path) self._mode = mode self._name = path self._offset = 0 self._flush = False self._buffer_size = buffer_size if desired_access is None: desired_access = 0 # While we can open a directory, the values for FilePipePrinterAccessMask also apply to Dirs so just use # the same enum to simplify code. if 'r' in self.mode or '+' in self.mode: desired_access |= FilePipePrinterAccessMask.FILE_READ_DATA | \ FilePipePrinterAccessMask.FILE_READ_ATTRIBUTES | \ FilePipePrinterAccessMask.FILE_READ_EA if 'w' in self.mode or 'x' in self.mode or 'a' in self.mode or '+' in self.mode: desired_access |= FilePipePrinterAccessMask.FILE_WRITE_DATA | \ FilePipePrinterAccessMask.FILE_WRITE_ATTRIBUTES | \ FilePipePrinterAccessMask.FILE_WRITE_EA self._desired_access = desired_access if file_attributes is None: file_attributes = FileAttributes.FILE_ATTRIBUTE_DIRECTORY if self.FILE_TYPE == 'dir' \ else FileAttributes.FILE_ATTRIBUTE_NORMAL self._file_attributes = file_attributes self._create_options = create_options self._create_options |= { 'dir': CreateOptions.FILE_DIRECTORY_FILE, 'file': CreateOptions.FILE_NON_DIRECTORY_FILE, }.get(self.FILE_TYPE, 0) super(SMBRawIO, self).__init__() def __enter__(self): self.open() return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() @property def closed(self): return not self.fd.connected @property def mode(self): return self._mode @property def name(self): return self._name def close(self, transaction=None): if transaction: transaction += self.fd.close(send=False) else: self.fd.close() def flush(self): if self._flush and self.FILE_TYPE != 'pipe': self.fd.flush() def open(self, transaction=None): if not self.closed: return share_access = _parse_share_access(self.share_access, self.mode) create_disposition = _parse_mode(self.mode, invalid=self._INVALID_MODE) try: # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wpo/feeb3122-cfe0-4b34-821d-e31c036d763c # Impersonation on SMB has little meaning when opening files but is important if using RPC so set to a sane # default of Impersonation. open_result = self.fd.create( ImpersonationLevel.Impersonation, self._desired_access, self._file_attributes, share_access, create_disposition, self._create_options, send=(transaction is None), ) except SMBResponseException as exc: raise SMBOSError(exc.status, self.name) if transaction is not None: transaction += open_result elif 'a' in self.mode and self.FILE_TYPE != 'pipe': self._offset = self.fd.end_of_file def readable(self): """ True if file was opened in a read mode. """ return 'r' in self.mode or '+' in self.mode def seek(self, offset, whence=SEEK_SET): """ Move to new file position and return the file position. Argument offset is a byte count. Optional argument whence defaults to SEEK_SET or 0 (offset from start of file, offset should be >= 0); other values are SEEK_CUR or 1 (move relative to current position, positive or negative), and SEEK_END or 2 (move relative to end of file, usually negative, although many platforms allow seeking beyond the end of a file). Note that not all file objects are seekable. """ seek_offset = { SEEK_SET: 0, SEEK_CUR: self._offset, SEEK_END: self.fd.end_of_file, }[whence] self._offset = seek_offset + offset return self._offset def seekable(self): """ True if file supports random-access. """ return True def tell(self): """ Current file position. Can raise OSError for non seekable files. """ return self._offset def truncate(self, size): """ Truncate the file to at most size bytes and return the truncated size. Size defaults to the current file position, as returned by tell(). The current file position is changed to the value of size. """ with SMBFileTransaction(self) as transaction: eof_info = FileEndOfFileInformation() eof_info['end_of_file'] = size set_info(transaction, eof_info) self.fd.end_of_file = size self._flush = True return size def writable(self): """ True if file was opened in a write mode. """ return 'w' in self.mode or 'x' in self.mode or 'a' in self.mode or '+' in self.mode def readall(self): """ Read and return all the bytes from the stream until EOF, using multiple calls to the stream if necessary. :return: The byte string read from the SMB file. """ data = b"" remaining_bytes = self.fd.end_of_file - self._offset while len(data) < remaining_bytes or self.FILE_TYPE == 'pipe': try: data += self.fd.read(self._offset, self._buffer_size) except SMBResponseException as exc: if exc.status == NtStatus.STATUS_PIPE_BROKEN: break raise if self.FILE_TYPE != 'pipe': self._offset += len(data) return data def readinto(self, b): """ Read bytes into a pre-allocated, writable bytes-like object b, and return the number of bytes read. :param b: bytes-like object to read the data into. :return: The number of bytes read. """ if self._offset >= self.fd.end_of_file and self.FILE_TYPE != 'pipe': return 0 try: file_bytes = self.fd.read(self._offset, len(b)) except SMBResponseException as exc: if exc.status == NtStatus.STATUS_PIPE_BROKEN: file_bytes = b"" else: raise b[:len(file_bytes)] = file_bytes if self.FILE_TYPE != 'pipe': self._offset += len(file_bytes) return len(file_bytes) def write(self, b): """ Write buffer b to file, return number of bytes written. Only makes one system call, so not all of the data may be written. The number of bytes actually written is returned. """ if isinstance(b, memoryview): b = b.tobytes() with SMBFileTransaction(self) as transaction: transaction += self.fd.write(b, offset=self._offset, send=False) # Send the request with an SMB2QueryInfoRequest for FileStandardInformation so we can update the end of # file stored internally. if self.FILE_TYPE != 'pipe': query_info(transaction, FileStandardInformation) bytes_written = transaction.results[0] if self.FILE_TYPE != 'pipe': self._offset += bytes_written self.fd.end_of_file = transaction.results[1][ 'end_of_file'].get_value() self._flush = True return bytes_written
class SCMRApi(object): def __init__(self, smb_session): # connect to the IPC tree and open a handle at svcctl self.tree = TreeConnect( smb_session, r"\\%s\IPC$" % smb_session.connection.server_name) self.handle = Open(self.tree, "svcctl") self.call_id = 0 def open(self): log.debug("Connecting to SMB Tree %s for SCMR" % self.tree.share_name) self.tree.connect() log.debug("Opening handle to svcctl pipe") self.handle.create( ImpersonationLevel.Impersonation, FilePipePrinterAccessMask.GENERIC_READ | FilePipePrinterAccessMask.GENERIC_WRITE, 0, ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE | ShareAccess.FILE_SHARE_DELETE, CreateDisposition.FILE_OPEN, CreateOptions.FILE_NON_DIRECTORY_FILE) # we need to bind svcctl to SCManagerW over DCE/RPC bind = BindPDU() bind['pfx_flags'].set_flag(PFlags.PFC_FIRST_FRAG) bind['pfx_flags'].set_flag(PFlags.PFC_LAST_FRAG) bind['packed_drep'] = DataRepresentationFormat() bind['call_id'] = self.call_id self.call_id += 1 context_ndr = ContextElement() context_ndr['context_id'] = 0 context_ndr['abstract_syntax'] = SyntaxIdElement() context_ndr['abstract_syntax']['uuid'] = \ uuid.UUID("367ABB81-9844-35F1-AD32-98F038001003") context_ndr['abstract_syntax']['version'] = 2 # https://msdn.microsoft.com/en-us/library/cc243843.aspx ndr_syntax = SyntaxIdElement() ndr_syntax['uuid'] = uuid.UUID("8a885d04-1ceb-11c9-9fe8-08002b104860") ndr_syntax['version'] = 2 context_ndr['transfer_syntaxes'] = [ndr_syntax] context_bind = ContextElement() context_bind['context_id'] = 1 context_bind['abstract_syntax'] = SyntaxIdElement() context_bind['abstract_syntax']['uuid'] = \ uuid.UUID("367ABB81-9844-35F1-AD32-98F038001003") context_bind['abstract_syntax']['version'] = 2 # https://msdn.microsoft.com/en-us/library/cc243715.aspx # uuid prefix = 6CB71C2C-9812-4540 # uuid prefix bytes = b'\x2c\x1c\xb7\x6c\x12\x98\x40\x45' # BindTimeFeatureNegotiateBitmask # https://msdn.microsoft.com/en-us/library/cc243884.aspx # SecurityContextMultiplexingSupported = 0x01 # KeepConnectionOnOrphanSupported = 0x02 # version number is 1 bind_syntax = SyntaxIdElement() bind_syntax['uuid'] = b'\x2c\x1c\xb7\x6c\x12\x98\x40\x45' \ b'\x03\x00\x00\x00\x00\x00\x00\x00' bind_syntax['version'] = 1 context_bind['transfer_syntaxes'] = [bind_syntax] bind['context_elems'] = [context_ndr, context_bind] bind_data = bind.pack() log.info("Sending bind request to svcctl") log.debug(str(bind)) self.handle.write(bind_data) log.info("Receiving bind result for svcctl") bind_data = self.handle.read(0, 1024) bind_result = parse_pdu(bind_data) log.debug(str(bind_result)) if not isinstance(bind_result, BindAckPDU): raise PDUException("Expecting BindAckPDU for initial bind result " "but got: %s" % str(bind_result)) def close(self): log.info("Closing bind to svcctl") self.handle.close(False) self.tree.disconnect() # SCMR Functions below def close_service_handle_w(self, handle): # https://msdn.microsoft.com/en-us/library/cc245920.aspx opnum = 0 res = self._invoke("RCloseServiceHandleW", opnum, handle) handle = res[:20] return_code = struct.unpack("<i", res[20:])[0] self._parse_error(return_code, "RCloseServiceHandleW") return handle def control_service(self, service_handle, control_code): # https://msdn.microsoft.com/en-us/library/cc245921.aspx opnum = 1 data = service_handle data += struct.pack("<i", control_code) res = self._invoke("RControlService", opnum, data) return_code = struct.unpack("<i", res[-4:])[0] self._parse_error(return_code, "RControlService") service_status = ServiceStatus() service_status.unpack(res[:-4]) return service_status def delete_service(self, service_handle): # https://msdn.microsoft.com/en-us/library/cc245926.aspx opnum = 2 res = self._invoke("RDeleteService", opnum, service_handle) return_code = struct.unpack("<i", res)[0] self._parse_error(return_code, "RDeleteService") def query_service_status(self, service_handle): # https://msdn.microsoft.com/en-us/library/cc245952.aspx opnum = 6 res = self._invoke("RQueryServiceStatus", opnum, service_handle) return_code = struct.unpack("<i", res[-4:])[0] self._parse_error(return_code, "RQueryServiceStatus") service_status = ServiceStatus() service_status.unpack(res[:-4]) return service_status def enum_services_status_w(self, server_handle, service_type, service_state): """ Enumerates the services based on the criteria selected :param server_handle: A handle to SCMR :param service_type: ServiceType flags to filter by service type :param service_state: EnumServiceState enum value :return: List dictionaries with the following entries service_name: The service name of the service display_name: The display name of the service service_status: ServiceStatus structure of the service """ # https://msdn.microsoft.com/en-us/library/cc245933.aspx opnum = 14 # sent 0 bytes on the buffer size for the 1st request to get the # buffer size that is required req_data = server_handle req_data += struct.pack("<i", service_type) req_data += struct.pack("<i", service_state) req_data += struct.pack("<i", 0) req_data += b"\x00\x00\x00\x00" res = self._invoke("REnumServicesStatusW", opnum, req_data) # now send another request with the total buffer size sent buffer_size = struct.unpack("<i", res[4:8])[0] req_data = server_handle req_data += struct.pack("<i", service_type) req_data += struct.pack("<i", service_state) req_data += res[4:8] req_data += b"\x00\x00\x00\x00" try: res = self._invoke("REnumServicesStatusW", opnum, req_data) data = res except SMBResponseException as exc: if exc.status != NtStatus.STATUS_BUFFER_OVERFLOW: raise exc ioctl_resp = SMB2IOCTLResponse() ioctl_resp.unpack(exc.header['data'].get_value()) pdu_resp = self._parse_pdu(ioctl_resp['buffer'].get_value(), opnum) read_data = self.handle.read(0, 3256) # 4280 - 1024 data = pdu_resp + read_data while len(data) < buffer_size: read_data = self.handle.read(0, 4280) data += self._parse_pdu(read_data, opnum) return_code = struct.unpack("<i", data[-4:])[0] self._parse_error(return_code, "REnumServicesStatusW") # now we have all the data, let's unpack it services = [] services_returned = struct.unpack("<i", data[-12:-8])[0] offset = 4 for i in range(0, services_returned): name_offset = struct.unpack("<i", data[offset:4 + offset])[0] disp_offset = struct.unpack("<i", data[4 + offset:8 + offset])[0] service_status = ServiceStatus() service_name = data[name_offset + 4:].split(b"\x00\x00")[0] display_name = data[disp_offset + 4:].split(b"\x00\x00")[0] service_status.unpack(data[offset + 8:]) service_info = { "display_name": (display_name + b"\x00").decode('utf-16-le'), "service_name": (service_name + b"\x00").decode('utf-16-le'), "service_status": service_status } services.append(service_info) offset += 8 + len(service_status) return services def open_sc_manager_w(self, machine_name, database_name, desired_access): # https://msdn.microsoft.com/en-us/library/cc245942.aspx opnum = 15 data = self._marshal_string(machine_name, unique=True) data += self._marshal_string(database_name) data += struct.pack("<i", desired_access) res = self._invoke("ROpenSCManagerW", opnum, data) server_handle = res[:20] return_code = struct.unpack("<i", res[20:])[0] self._parse_error(return_code, "ROpenSCManagerW") return server_handle def open_service_w(self, server_handle, service_name, desired_access): # https://msdn.microsoft.com/en-us/library/cc245944.aspx opnum = 16 data = server_handle data += self._marshal_string(service_name) data += struct.pack("<i", desired_access) res = self._invoke("ROpenServiceW", opnum, data) service_handle = res[:20] return_code = struct.unpack("<i", res[20:])[0] self._parse_error(return_code, "ROpenServiceW") return service_handle def start_service_w(self, service_handle, *args): opnum = 19 data = service_handle data += struct.pack("<i", len(args)) data += b"".join([self._marshal_string(arg) for arg in args]) data += b"\x00" * 4 # terminate arg list res = self._invoke("RStartServiceW", opnum, data) return_code = struct.unpack("<i", res)[0] self._parse_error(return_code, "RStartServiceW") def create_service_w(self, server_handle, service_name, display_name, desired_access, service_type, start_type, error_control, path, load_order_group, tag_id, dependencies, username, password): # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-scmr/6a8ca926-9477-4dd4-b766-692fab07227e opnum = 12 data = server_handle data += self._marshal_string(service_name) data += self._marshal_string(display_name, unique=True) data += struct.pack("<i", desired_access) data += struct.pack("<i", service_type) data += struct.pack("<i", start_type) data += struct.pack("<i", error_control) data += self._marshal_string(path) data += self._marshal_string(load_order_group) data += struct.pack("<i", tag_id) # TODO: convert list of string to a byte object dependencies_bytes = dependencies if dependencies else b"\x00" * 6 data += dependencies_bytes dependencies_length = len(dependencies) if dependencies else 0 data += struct.pack("<i", dependencies_length) data += self._marshal_string(username) pass_bytes = self._marshal_string(password) data += pass_bytes pass_len = len(pass_bytes) if password else 0 data += struct.pack("<i", pass_len) res = self._invoke("RCreateServiceW", opnum, data) tag_id = res[0:4] service_handle = res[4:24] return_code = struct.unpack("<i", res[24:])[0] self._parse_error(return_code, "RCreateServiceW") return tag_id, service_handle def _invoke(self, function_name, opnum, data): req = RequestPDU() req['pfx_flags'].set_flag(PFlags.PFC_FIRST_FRAG) req['pfx_flags'].set_flag(PFlags.PFC_LAST_FRAG) req['packed_drep'] = DataRepresentationFormat() req['call_id'] = self.call_id self.call_id += 1 req['opnum'] = opnum req['stub_data'] = data ioctl_request = SMB2IOCTLRequest() ioctl_request['ctl_code'] = CtlCode.FSCTL_PIPE_TRANSCEIVE ioctl_request['file_id'] = self.handle.file_id ioctl_request['max_output_response'] = 1024 ioctl_request['flags'] = IOCTLFlags.SMB2_0_IOCTL_IS_FSCTL ioctl_request['buffer'] = req session_id = self.tree.session.session_id tree_id = self.tree.tree_connect_id log.info("Sending svcctl RPC request for %s" % function_name) log.debug(str(req)) request = self.tree.session.connection.send(ioctl_request, sid=session_id, tid=tree_id) log.info("Receiving svcctl RPC response for %s" % function_name) resp = self.tree.session.connection.receive(request) ioctl_resp = SMB2IOCTLResponse() ioctl_resp.unpack(resp['data'].get_value()) log.debug(str(ioctl_resp)) pdu_resp = self._parse_pdu(ioctl_resp['buffer'].get_value(), opnum) return pdu_resp def _parse_pdu(self, data, opnum): pdu_resp = parse_pdu(data) if not isinstance(pdu_resp, ResponsePDU): raise PDUException("Expecting ResponsePDU for opnum %d response " "but got: %s" % (opnum, str(pdu_resp))) return pdu_resp['stub_data'].get_value() def _parse_error(self, return_code, function_name): error_string = "ERROR_UNKNOWN" for error_name, error_val in vars(ScmrReturnValues).items(): if isinstance(error_val, int) and error_val == return_code: error_string = error_name break if not error_string.startswith("ERROR_SUCCESS"): raise SCMRException(function_name, return_code, error_string) def _marshal_string(self, string, unique=False, max_count=None): """ Strings are encoding as a UTF-16-LE byte structure and are marshalled in a particular format to send over RPC. The format is as follows Referent ID (Int32): A unique ID for the string, we just set to 1 Max Count (Int32): If the server can return a value, this is the size that can be returned in the buffer otherwise just the numbers of chars in the input string Offset (Int32): The offset of the string, defaults to 0 Actual Count (Int32): The number of chars (not bytes) or the string itself including the NULL terminator Bytes (Bytes): The string encoded as a UTF-16-LE byte string with a NULL terminator Padding (Bytes): The value must align to a 4-byte boundary so this is some NULL bytes to pad the length :param string: The string to marshal :param unique: Whether the string is unique and requires an ID :return: A byte string of the marshaled string """ # return NULL Pointer for a null string if not string: return b"\x00" * 4 referent = b"\x00\x00\x00\x01" if unique else b"" unicode_string = string.encode("utf-16-le") + b"\x00\x00" unicode_count = int(len(unicode_string) / 2) count = struct.pack("<i", unicode_count) offset = b"\x00" * 4 bytes = referent + count + offset + count + unicode_string # each parameter needs to be aligned at a 4-byte boundary so get the # padding length if necessary mod = len(bytes) % 4 padding_len = 0 if mod == 0 else 4 - mod return bytes + (b"\x00" * padding_len)