def test_invalid_user(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3]) connection.connect() try: session = Session(connection, "fakeuser", "fakepass") with pytest.raises(SMBAuthenticationError) as exc: session.connect() assert "Failed to authenticate with server: " in str(exc.value) finally: connection.disconnect(True)
def test_connection_echo(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3]) connection.connect() session = Session(connection, smb_real[0], smb_real[1]) session.connect() try: actual = connection.echo(sid=session.session_id, credit_request=2) assert actual == 2 finally: connection.disconnect(True)
def test_require_encryption_not_supported(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3]) connection.connect(Dialects.SMB_2_1_0) try: session = Session(connection, smb_real[0], smb_real[1]) with pytest.raises(SMBException) as exc: session.connect() assert str(exc.value) == "SMB encryption is required but the " \ "connection does not support it" finally: connection.disconnect(True)
def test_dialect_2_encrypted_share(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3]) connection.connect(Dialects.SMB_2_1_0) session = Session(connection, smb_real[0], smb_real[1], False) tree = TreeConnect(session, smb_real[5]) try: session.connect() with pytest.raises(AccessDenied) as exc: tree.connect() finally: connection.disconnect(True)
def test_requested_credits_greater_than_available(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3], True) connection.connect() try: msg = SMB2IOCTLRequest() msg['max_output_response'] = 65538 # results in 2 credits required with pytest.raises(SMBException) as exc: connection._generate_packet_header(msg, None, None, 0) assert str(exc.value) == "Request requires 2 credits but only 1 " \ "credits are available" finally: connection.disconnect()
def test_verify_message_skip(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3], True) connection.connect() try: header = SMB2HeaderResponse() header['message_id'] = 0xFFFFFFFFFFFFFFFF expected = header.pack() connection._verify(header, 0) actual = header.pack() assert actual == expected finally: connection.disconnect()
def test_verify_fail_no_session(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3], True) connection.connect() try: header = SMB2HeaderResponse() header['message_id'] = 1 header['flags'].set_flag(Smb2Flags.SMB2_FLAGS_SIGNED) with pytest.raises(SMBException) as exc: connection._verify(header, 100) assert str(exc.value) == "Failed to find session 100 for " \ "message verification" finally: connection.disconnect()
def test_dialect_2_encrypted_share(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3]) connection.connect(Dialects.SMB_2_1_0) session = Session(connection, smb_real[0], smb_real[1], False) tree = TreeConnect(session, smb_real[5]) try: session.connect() with pytest.raises(SMBResponseException) as exc: tree.connect() assert str(exc.value) == "Received unexpected status from the " \ "server: (3221225506) " \ "STATUS_ACCESS_DENIED: 0xc0000022" finally: connection.disconnect(True)
def test_send_invalid_tree_id(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3]) session = Session(connection, smb_real[0], smb_real[1]) connection.connect() try: session.connect() msg = SMB2IOCTLRequest() msg['file_id'] = b"\xff" * 16 with pytest.raises(SMBException) as exc: connection.send(msg, session.session_id, 10) assert str(exc.value) == "Cannot find Tree with the ID 10 in " \ "the session tree table" finally: connection.disconnect()
def test_secure_negotiation_verification_failed(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3]) connection.connect(Dialects.SMB_3_0_2) session = Session(connection, smb_real[0], smb_real[1]) connection.dialect = Dialects.SMB_3_0_0 tree = TreeConnect(session, smb_real[4]) try: session.connect() with pytest.raises(SMBException) as exc: tree.connect() assert "Secure negotiate failed to verify server dialect, " \ "Actual: 770, Expected: 768" in str(exc.value) finally: connection.disconnect(True)
def test_dialect_3_encrypted_share(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3]) connection.connect(Dialects.SMB_3_1_1) session = Session(connection, smb_real[0], smb_real[1]) tree = TreeConnect(session, smb_real[5]) try: session.connect() tree.connect() assert tree.encrypt_data assert not tree.is_ca_share assert not tree.is_dfs_share assert not tree.is_scaleout_share assert isinstance(tree.tree_connect_id, int) finally: connection.disconnect(True)
def test_connect_fail(self, smb_real, monkeypatch, mocker): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3]) connection.connect() try: monkeypatch.setattr( pyspnego, 'client', mocker.MagicMock( side_effect=pyspnego.exceptions.NoCredentialError())) session = Session(connection, smb_real[0], smb_real[1]) with pytest.raises(SMBAuthenticationError, match="Failed to authenticate with server"): session.connect() finally: connection.disconnect(True)
def test_decrypt_invalid_flag(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3], True) session = Session(connection, smb_real[0], smb_real[1]) connection.connect() try: session.connect() # just get some random message header = connection.preauth_integrity_hash_value[-1] enc_header = connection._encrypt(header.pack(), session) assert isinstance(enc_header, SMB2TransformHeader) enc_header['flags'] = 5 with pytest.raises(SMBException) as exc: connection._decrypt(enc_header) assert str(exc.value) == "Expecting flag of 0x0001 but got 5 in " \ "the SMB Transform Header Response" finally: connection.disconnect(True)
def test_decrypt_invalid_session_id(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3], True) session = Session(connection, smb_real[0], smb_real[1]) connection.connect() try: session.connect() # just get some random message header = connection.preauth_integrity_hash_value[-1] enc_header = connection._encrypt(header.pack(), session) assert isinstance(enc_header, SMB2TransformHeader) enc_header['session_id'] = 100 with pytest.raises(SMBException) as exc: connection._decrypt(enc_header) assert str(exc.value) == "Failed to find valid session 100 for " \ "message decryption" finally: connection.disconnect(True)
def test_secure_ignore_negotiation_verification_failed(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3]) connection.connect(Dialects.SMB_3_0_2) session = Session(connection, smb_real[0], smb_real[1]) connection.dialect = Dialects.SMB_3_0_0 tree = TreeConnect(session, smb_real[4]) try: session.connect() tree.connect(False) assert not tree.encrypt_data assert not tree.is_ca_share assert not tree.is_dfs_share assert not tree.is_scaleout_share assert isinstance(tree.tree_connect_id, int) finally: connection.disconnect(True) tree.disconnect() # test that disconnect can be run mutliple times
def test_broken_message_worker_closed_socket(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3], True) connection.connect() try: test_msg = SMB2NegotiateRequest() test_req = Request(test_msg, type(test_msg), connection) connection.outstanding_requests[666] = test_req # Close the connection manually connection.transport.close() with pytest.raises(SMBConnectionClosed): connection.receive(test_req) with pytest.raises(SMBConnectionClosed): connection.send(SMB2NegotiateRequest()) finally: connection.disconnect()
def test_verify_mistmatch(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3], True) session = Session(connection, smb_real[0], smb_real[1]) connection.connect() try: session.connect() header = connection.preauth_integrity_hash_value[-2] # just set some random values for verifiation failure header['flags'].set_flag(Smb2Flags.SMB2_FLAGS_SIGNED) header['signature'] = b"\xff" * 16 with pytest.raises(SMBException) as exc: connection._verify(header, header['session_id'].get_value(), verify_session=True) assert "Server message signature could not be verified:" in \ str(exc.value) finally: connection.disconnect(True)
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 test_dialect_2_1_0(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3]) connection.connect(Dialects.SMB_2_1_0) session = Session(connection, smb_real[0], smb_real[1], require_encryption=False) try: session.connect() assert len(session.application_key) == 16 assert session.decryption_key is None assert not session.encrypt_data assert session.encryption_key is None assert len(session.preauth_integrity_hash_value) == 5 assert not session.require_encryption assert session.session_id is not None assert session.session_key == session.application_key assert session.signing_key == session.signing_key assert session.signing_required finally: connection.disconnect(True)
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: 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.create(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(pkgutil.get_data('pypsexec', 'paexec.exe'), 0) paexec_file.close(get_attributes=False) yield session finally: paexec_file.create(ImpersonationLevel.Impersonation, FilePipePrinterAccessMask.DELETE, FileAttributes.FILE_ATTRIBUTE_NORMAL, ShareAccess.FILE_SHARE_DELETE, 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 test_broken_message_worker(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3], True) connection.connect() try: test_msg = SMB2NegotiateRequest() test_req = Request(test_msg, connection) connection.outstanding_requests[666] = test_req # Put a bad message in the incoming queue to break the worker in a bad way connection.transport._recv_queue.put(b"\x01\x02\x03\x04") while connection._t_exc is None: pass with pytest.raises(Exception): connection.send(SMB2NegotiateRequest()) # Verify that all outstanding request events have been set on a failure assert test_req.response_event.is_set() finally: connection.disconnect()
def test_dialect_2_1_0(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3]) connection.connect(Dialects.SMB_2_1_0) try: assert connection.dialect == Dialects.SMB_2_1_0 assert connection.negotiated_dialects == [Dialects.SMB_2_1_0] assert connection.gss_negotiate_token is not None assert len(connection.preauth_integrity_hash_value) == 2 assert len(connection.salt) == 32 assert connection.sequence_window['low'] == 1 assert connection.sequence_window['high'] == 2 assert connection.client_security_mode == \ SecurityMode.SMB2_NEGOTIATE_SIGNING_REQUIRED # server settings override the require signing assert connection.server_security_mode is None assert not connection.supports_encryption assert connection.require_signing finally: connection.disconnect()
def test_verify_mistmatch(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3], True) session = Session(connection, smb_real[0], smb_real[1]) connection.connect() try: session.connect() b_header = connection.preauth_integrity_hash_value[-2] header = SMB2HeaderResponse() header.unpack(b_header) # just set some random values for verification failure header['flags'].set_flag(Smb2Flags.SMB2_FLAGS_SIGNED) header['signature'] = b"\xff" * 16 with pytest.raises(SMBException) as exc: connection.verify_signature( header, list(connection.session_table.keys())[0], force=True) assert "Server message signature could not be verified:" in str( exc.value) finally: connection.disconnect(True)
def test_setup_session_with_ms_gss_token(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3]) connection.connect() connection.gss_negotiate_token = b"\x60\x76\x06\x06\x2b\x06\x01\x05" \ b"\x05\x02\xa0\x6c\x30\x6a\xa0\x3c" \ b"\x30\x3a\x06\x0a\x2b\x06\x01\x04" \ b"\x01\x82\x37\x02\x02\x1e\x06\x09" \ b"\x2a\x86\x48\x82\xf7\x12\x01\x02" \ b"\x02\x06\x09\x2a\x86\x48\x86\xf7" \ b"\x12\x01\x02\x02\x06\x0a\x2a\x86" \ b"\x48\x86\xf7\x12\x01\x02\x02\x03" \ b"\x06\x0a\x2b\x06\x01\x04\x01\x82" \ b"\x37\x02\x02\x0a\xa3\x2a\x30\x28" \ b"\xa0\x26\x1b\x24\x6e\x6f\x74\x5f" \ b"\x64\x65\x66\x69\x6e\x65\x64\x5f" \ b"\x69\x6e\x5f\x52\x46\x43\x34\x31" \ b"\x37\x38\x40\x70\x6c\x65\x61\x73" \ b"\x65\x5f\x69\x67\x6e\x6f\x72\x65" session = Session(connection, smb_real[0], smb_real[1], False) try: session.connect() assert len(session.application_key) == 16 assert session.application_key != session.session_key assert len(session.decryption_key) == 16 assert session.decryption_key != session.session_key assert not session.encrypt_data assert len(session.encryption_key) == 16 assert session.encryption_key != session.session_key assert len(session.connection.preauth_integrity_hash_value) == 2 assert len(session.preauth_integrity_hash_value) == 3 assert not session.require_encryption assert session.session_id is not None assert len(session.session_key) == 16 assert len(session.signing_key) == 16 assert session.signing_key != session.session_key assert session.signing_required finally: connection.disconnect(True)
def test_dialect_3_1_1_not_require_signing(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3], False) connection.connect(Dialects.SMB_3_1_1) try: assert connection.dialect == Dialects.SMB_3_1_1 assert connection.negotiated_dialects == [Dialects.SMB_3_1_1] assert connection.gss_negotiate_token is not None assert len(connection.preauth_integrity_hash_value) == 2 assert len(connection.salt) == 32 assert connection.sequence_window['low'] == 1 assert connection.sequence_window['high'] == 2 assert connection.client_security_mode == \ SecurityMode.SMB2_NEGOTIATE_SIGNING_ENABLED # server settings override the require signing assert connection.server_security_mode & \ SecurityMode.SMB2_NEGOTIATE_SIGNING_REQUIRED == \ SecurityMode.SMB2_NEGOTIATE_SIGNING_REQUIRED assert connection.supports_encryption # for tests we set that server requires signing so that overrides # the client setting assert connection.require_signing finally: connection.disconnect()
def test_dialect_3_1_1(self, smb_real): connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3]) connection.connect(Dialects.SMB_3_1_1) session = Session(connection, smb_real[0], smb_real[1]) try: session.connect() assert len(session.application_key) == 16 assert session.application_key != session.session_key assert len(session.decryption_key) == 16 assert session.decryption_key != session.session_key assert session.encrypt_data assert len(session.encryption_key) == 16 assert session.encryption_key != session.session_key assert len(session.preauth_integrity_hash_value) == 5 assert session.require_encryption assert session.session_id is not None assert len(session.session_key) == 16 assert len(session.signing_key) == 16 assert session.signing_key != session.session_key assert not session.signing_required finally: connection.disconnect(True) # test that disconnect can be run multiple times session.disconnect()
responses.append(response) dir_files = [] for dir_file in responses[1]: dir_files.append(dir_file['file_name'].get_value().decode('utf-16-le')) print("Directory '%s\\%s' contains the files: '%s'" % (share, dir_name, "', '".join(dir_files))) # delete a directory (note the dir needs to be empty to delete on close) dir_open = Open(tree, dir_name) 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 = connection.send_compound([x[0] for x in delete_msgs], sid=session.session_id, tid=tree.tree_connect_id, related=True) for i, request in enumerate(delete_reqs): response = delete_msgs[i][1](request) finally: connection.disconnect(True)
class Client(object): def __init__(self, server, username=None, password=None, port=445, encrypt=True, obscure=True, sharename="ADMIN$"): self.server = server self.port = port self.pid = os.getpid() self.current_host = socket.gethostname() self.connection = Connection(uuid.uuid4(), server, port) self.session = Session(self.connection, username, password, require_encryption=encrypt) self.service_name = "" #added if obscure: #added self.service_name = Client.obscure_service_name() #added else: #added self.service_name = "PAExec" # This doesn't need a new name everytime the service is created; makes a mess in the directories # OLD = "PAExec-%d-%s" % (self.pid, self.current_host) log.info("Creating PyPsexec Client with unique name: %s" % self.service_name) self._exe_file = "%s.exe" % self.service_name self._stdout_pipe_name = "PaExecOut%s%d" % (self.current_host, self.pid) self._stderr_pipe_name = "PaExecErr%s%d" % (self.current_host, self.pid) self._stdin_pipe_name = "PaExecIn%s%d" % (self.current_host, self.pid) self._unique_id = get_unique_id(self.pid, self.current_host) log.info("Generated unique ID for PyPsexec Client: %d" % self._unique_id) self._service = Service(self.service_name, self.session) self._share = sharename @staticmethod #added----------------------------------------------------------------------- def obscure_service_name(): selection = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0987654321_- " stringbuilder = [] for iter in range(0, random.randint(4, 16)): stringbuilder.append(random.choice(selection)) return "".join(stringbuilder) def connect(self, timeout=60): log.info("Setting up SMB Connection to %s:%d" % (self.server, self.port)) self.connection.connect(timeout=timeout) log.info("Authenticating SMB Session") self.session.connect() log.info("Opening handle to SCMR and PAExec service") self._service.open() def disconnect(self): log.info("Closing handle of PAExec service and SCMR") self._service.close() log.info("Closing SMB Connection") self.connection.disconnect(close=False) 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\%s" % (self.connection.server_name, self._share)) 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.create(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 # added --------------------------------------------------------------- path_root = r"%SystemRoot%" path_root = dir_map.get(self._share.strip("$\n\r\t"), r"%SystemRoot%") # added --------------------------------------------------------------- service_path = r'"{}\{}" -service'.format(path_root, self._exe_file) # added log.info("Creating PAExec service %s" % self.service_name) self._service.create(service_path) def remove_service(self): """ Removes the PAExec service and executable that was created as part of the create_service function. This does not remove any older executables or services from previous runs, use cleanup() instead for that purpose. """ # Stops/remove the PAExec service and removes the executable log.debug("Deleting PAExec service at the end of the process") self._service.delete() # delete the PAExec executable smb_tree = TreeConnect( self.session, r"\\%s\%s" % (self.connection.server_name, self._share)) log.info("Connecting to SMB Tree %s" % smb_tree.share_name) smb_tree.connect() log.info("Creating open to PAExec file with delete on close flags") self._delete_file(smb_tree, self._exe_file) log.info("Disconnecting from SMB Tree %s" % smb_tree.share_name) smb_tree.disconnect() 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\%s" % (self.connection.server_name, self._share)) 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 run_executable(self, executable, arguments=None, processors=None, asynchronous=False, load_profile=True, interactive_session=0, interactive=False, run_elevated=False, run_limited=False, username=None, password=None, use_system_account=False, working_dir=None, show_ui_on_win_logon=False, priority=ProcessPriority.NORMAL_PRIORITY_CLASS, remote_log_path=None, timeout_seconds=0, stdout=OutputPipeBytes, stderr=OutputPipeBytes, stdin=None): """ Runs a command over the PAExec/PSExec interface based on the options provided. At a minimum the executable argument is required and the rest can stay as the defaults. The default configuration for a process (with no changes) is; User: The user that authenticated the SMB Session Elevation: Highest possible Working Dir: %SYSTEM_ROOT%\System32 Interactive: False Priority: Normal :param executable: (String) The executable to be run :param arguments: (String) Arguments to run with the executable :param processors: (List<Int>) The processors that the process can run on, default is all the processors :param asynchronous: (Bool) Whether to run the process and not wait for the output, it will continue to run in the background. The stdout and stderr return value will be None and the rc is not reflective of the running process :param load_profile: (Bool) Whether to load the user profile, default is True :param interactive_session: (Int) The session id that an interactive process will run on, use with interactive=True to run a process on an existing session :param interactive: (Bool) Whether to run on an interative session or not, default is False. The stdout and stderr will be None :param run_elevated: (Bool) Whether to run as an elevated process or not, default is False (This only applies when username is supplied) :param run_limited: (Bool) Whether to run as a limited user context, admin rights are removed, or not, default is False (This only applied when username is applied) :param username: (String) The username to run the process as, if not set then either the SMB Session account is used or NT AUTHORITY\SYSTEM (use_system_account=True) is used :param password: (String) The password for the username account :param use_system_account: (Bool) Whether to use the NT AUTHORITY\SYSTEM account isn't of a normal user :param working_dir: (String) The working directory that is used when spawning the process :param show_ui_on_win_logon: (Bool) Whether to display the UI on the Winlogon secure desktop (use_system_account=True only), default is False :param priority: (paexec.ProcessPriority) The process priority level, default is NORMAL_PRIORITY_CLASS :param remote_log_path: (String) A path on the remote host to output log files for the PAExec service process (for debugging purposes) :param timeout_seconds: (Int) A timeout that will force the PAExec process to stop once reached, default is 0 (no timeout) :param stdout: (pipe.OutputPipe) An class that implements of pipe.OutputPipe that handles the Named Pipe stdout output. The default is pipe.OutputPipeBytes which returns a byte string of the stdout :param stderr: (pipe.OutputPipe) An class that implements of pipe.OutputPipe that handles the Named Pipe stderr output. The default is pipe.OutputPipeBytes which returns a byte string of the stderr :param stdin: Either a byte string of generator that yields multiple byte strings to send over the stdin pipe. :return: Tuple(stdout, stderr, rc) stdout: (Bytes) The stdout.get_bytes() return result stderr: (Bytes) The stderr.get_bytes() return result rc: (Int) The return code of the process (The pid of the async process when async=True) """ if run_elevated and run_limited: raise PypsexecException("Both run_elevated and run_limited are " "set, only 1 of these can be true") if stdin is not None and (asynchronous or interactive): raise PypsexecException("Cannot send stdin data on an interactive " "or asynchronous process") log.debug("Making sure PAExec service is running") self._service.start() smb_tree = TreeConnect(self.session, r"\\%s\IPC$" % self.connection.server_name) log.info("Connecting to SMB Tree %s" % smb_tree.share_name) smb_tree.connect() settings = PAExecSettingsBuffer() settings['processors'] = processors if processors else [] settings['asynchronous'] = asynchronous settings['dont_load_profile'] = not load_profile settings['interactive_session'] = interactive_session settings['interactive'] = interactive settings['run_elevated'] = run_elevated settings['run_limited'] = run_limited settings['username'] = self._encode_string(username) settings['password'] = self._encode_string(password) settings['use_system_account'] = use_system_account settings['working_dir'] = self._encode_string(working_dir) settings['show_ui_on_win_logon'] = show_ui_on_win_logon settings['priority'] = priority settings['executable'] = self._encode_string(executable) settings['arguments'] = self._encode_string(arguments) settings['remote_log_path'] = self._encode_string(remote_log_path) settings['timeout_seconds'] = timeout_seconds input_data = PAExecSettingsMsg() input_data['unique_id'] = self._unique_id input_data['buffer'] = settings # write the settings to the main PAExec pipe pipe_access_mask = FilePipePrinterAccessMask.GENERIC_READ | \ FilePipePrinterAccessMask.GENERIC_WRITE | \ FilePipePrinterAccessMask.FILE_APPEND_DATA | \ FilePipePrinterAccessMask.READ_CONTROL | \ FilePipePrinterAccessMask.SYNCHRONIZE for i in range(0, 3): try: main_pipe = open_pipe(smb_tree, self._exe_file, pipe_access_mask) except SMBResponseException as exc: if exc.status != NtStatus.STATUS_OBJECT_NAME_NOT_FOUND: raise exc elif i == 2: raise PypsexecException("Failed to open main PAExec pipe " "%s, no more attempts remaining" % self._exe_file) log.warning("Main pipe %s does not exist yet on attempt %d. " "Trying again in 5 seconds" % (self._exe_file, i + 1)) time.sleep(5) else: break log.info("Writing PAExecSettingsMsg to the main PAExec pipe") log.info(str(input_data)) main_pipe.write(input_data.pack(), 0) log.info("Reading PAExecMsg from the PAExec pipe") settings_resp_raw = main_pipe.read(0, 1024) settings_resp = PAExecMsg() settings_resp.unpack(settings_resp_raw) log.debug(str(settings_resp)) settings_resp.check_resp() # start the process now start_msg = PAExecMsg() start_msg['msg_id'] = PAExecMsgId.MSGID_START_APP start_msg['unique_id'] = self._unique_id start_msg['buffer'] = PAExecStartBuffer() start_buffer = PAExecStartBuffer() start_buffer['process_id'] = self.pid start_buffer['comp_name'] = self.current_host.encode('utf-16-le') start_msg['buffer'] = start_buffer log.info("Writing PAExecMsg with PAExecStartBuffer to start the " "remote process") log.debug(str(start_msg)) main_pipe.write(start_msg.pack(), 0) if not interactive and not asynchronous: # create a pipe for stdout, stderr, and stdin and run in a separate # thread log.info("Connecting to remote pipes to retrieve output") stdout_pipe = stdout(smb_tree, self._stdout_pipe_name) stdout_pipe.start() stderr_pipe = stderr(smb_tree, self._stderr_pipe_name) stderr_pipe.start() stdin_pipe = InputPipe(smb_tree, self._stdin_pipe_name) # wait until the stdout and stderr pipes have sent their first # response log.debug("Waiting for stdout pipe to send first request") while not stdout_pipe.sent_first: pass log.debug("Waiting for stderr pipe to send first request") while not stderr_pipe.sent_first: pass # send any input if there was any try: if stdin and isinstance(stdin, bytes): log.info("Sending stdin bytes over stdin pipe: %s" % self._stdin_pipe_name) stdin_pipe.write(stdin) elif stdin: log.info("Sending stdin generator bytes over stdin pipe: " "%s" % self._stdin_pipe_name) for stdin_data in stdin(): stdin_pipe.write(stdin_data) except SMBResponseException as exc: # if it fails with a STATUS_PIPE_BROKEN exception, continue as # the actual error will be in the response (process failed) if exc.status != NtStatus.STATUS_PIPE_BROKEN: raise exc log.warning("Failed to send data through stdin: %s" % str(exc)) # read the final response from the process log.info("Reading result of PAExec process") exe_result_raw = main_pipe.read(0, 1024) log.info("Results read of PAExec process") if not interactive and not asynchronous: log.info("Closing PAExec std* pipes") stdout_pipe.close() stderr_pipe.close() stdin_pipe.close() log.info("Gettings stdout and stderr from pipe buffer queue") stdout_out = stdout_pipe.get_output() stderr_bytes = stderr_pipe.get_output() else: stdout_out = None stderr_bytes = None log.info("Closing main PAExec pipe") main_pipe.close() log.info("Disconnecting from SMB Tree %s" % smb_tree.share_name) smb_tree.disconnect() log.info("Unpacking PAExecMsg data from process result") exe_result = PAExecMsg() exe_result.unpack(exe_result_raw) log.debug(str(exe_result)) exe_result.check_resp() log.debug("Unpacking PAExecReturnBuffer from main PAExecMsg") rc = PAExecReturnBuffer() rc.unpack(exe_result['buffer'].get_value()) log.debug(str(rc)) return_code = rc['return_code'].get_value() log.info("Process finished with exit code: %d" % return_code) log.debug("RC: %d" % return_code) return stdout_out, stderr_bytes, return_code def _encode_string(self, string): return string.encode('utf-16-le') if string else None def _empty_queue(self, queue): data = b"" while True: try: data += queue.get(block=False) except Empty: break return data 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_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)
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)