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 _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 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, "") share.open( 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: files = share.query_directory( "PAExec-*.exe", FileInformationClass.FILE_NAMES_INFORMATION) 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 _get_paexec_files_and_services(self, client): server = os.environ['PYPSEXEC_SERVER'] username = os.environ['PYPSEXEC_USERNAME'] password = os.environ['PYPSEXEC_PASSWORD'] 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.open( 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 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.open( 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 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")
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.open( 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_wow64_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://msdn.microsoft.com/en-us/library/cc245925.aspx opnum = 45 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("RCreateServiceWOW64W", 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, "RCreateServiceWOW64W") 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)