def __init__(self, name=str, host=str, port=int, max_retries=10, timeout=None, logger=None, pb2_module=None) -> None: """ Class creates the protobuf payload with the marriage of Fuzzed data. :param name: name of the class :param host: target host :param port: target port :param max_retries: maximum retries of connection :param timeout: timeout :param logger: kitty logger """ super(ProtobufTarget, self).__init__(name, logger) self.host = host self.port = port if (host is None) or (port is None): raise ValueError('Host and port may not be None') self.timeout = timeout self.socket = None self.max_retries = max_retries self.pb2_module = pb2_module self.set_expect_response(False) self.config = ConfigParser() self.verbosity = self.config.get_generic_verbosity() self.logger.setLevel(self.verbosity) self.module_path = self.config.get_module_path() self._uuid = GenerateUUID.generate_uuid() self.frmwrk_utils = FrameworkUtils()
def __init__(self, name, host, port, timeout=None, logger=None): """ :param name: name of the target :param host: host ip (to send data to) currently unused :param port: port to send to :param timeout: socket timeout (default: None) :param logger: logger for the object (default: None) """ super(DnsTarget, self).__init__(name, logger) self.host = host self.port = port if (host is None) or (port is None): raise ValueError('Host and port may not be None!') self.timeout = timeout self.socket = None self.bind_host = None self.bind_port = None self.expect_response = False self.config = ConfigParser() self.dns_a_record_status = self.config.get_dns_a_record_status() self.dns_ns_record_status = self.config.get_dns_ns_record_status() self.dns_txt_record_status = self.config.get_dns_txt_record_status() self.verbosity = self.config.get_generic_verbosity() self.logger.setLevel(self.verbosity) self._uuid = GenerateUUID.generate_uuid() self.question_length = None
def __init__(self, pb2_api, name='ProtobufRunner', logger=None): super(ProtobufRunner, self).__init__(name, logger) self.pb2_api = pb2_api self.config = ConfigParser() self.target_host = self.config.get_target_host_name() self.target_port = self.config.get_target_port() self.frmu = FrameworkUtils()
def __init__(self, name, host, port, max_retries=10, timeout=None, logger=None) -> object: """ :param name: name of the target :param host: host ip (to send data to) currently unused :param port: port to send to :param max_retries: maximum connection retries (default: 10) :param timeout: socket timeout (default: None) :param logger: logger for the object (default: None) """ super(HttpTarget, self).__init__(name, logger) self.host = host self.port = port if (host is None) or (port is None): raise ValueError('host and port may not be None') self.timeout = timeout self.socket = None self.max_retries = max_retries self.config = ConfigParser() self.use_tls = self.config.get_tls() self.target_host = self.config.get_target_host_name() self.report = Report('report') self._uuid = GenerateUUID.generate_uuid()
def __init__(self, name='DnsRunner', logger=None): super(DnsRunner, self).__init__(name, logger) self.config = ConfigParser() self.target_host = self.config.get_target_host_name() self.target_port = self.config.get_target_port() self.timeout = self.config.get_dns_timout() self.tld = self.config.get_dns_tld() self.default_labels = self.config.get_dns_default_labels()
def __init__(self, name='Runner', logger=None): super(Runner, self).__init__(name, logger) self.config = ConfigParser() self.framework_utils = FrameworkUtils() self.put_archive_to_s3 = PutArchiveToS3() self.proto_path = self.config.get_generic_proto_path() self.pb2_path = self.config.get_generic_pb2_path() self.verbosity = self.config.get_generic_verbosity() self.module_path = self.config.get_module_path() self.proto_modules = self.config.get_protobuf_modules() self.proto_classes = self.config.get_protobuf_classes_to_send() self.protocol_proto = self.config.get_protocol_protobuf() self.protocol_http = self.config.get_protocol_http() self.protocol_dns = self.config.get_protocol_dns() self.set_verbosity(self.verbosity) self.use_s3_to_archive = self.config.get_archive_to_s3()
class ProtobufRunner(FuzzObject): def __init__(self, pb2_api, name='ProtobufRunner', logger=None): super(ProtobufRunner, self).__init__(name, logger) self.pb2_api = pb2_api self.config = ConfigParser() self.target_host = self.config.get_target_host_name() self.target_port = self.config.get_target_port() self.frmu = FrameworkUtils() def run_proto(self) -> None: """ kitty low level field model https://kitty.readthedocs.io/en/latest/kitty.model.low_level.field.html """ js = ext_json.dict_to_JsonObject(dict(self.pb2_api[0]['Messages']), 'api') template_a = Template(name='Api', fields=js) self.logger.info(f"[{time.strftime('%H:%M:%S')}] Prepare ProtobufTarget ") target = ProtobufTarget('ProtobufTarget', host=self.target_host, port=self.target_port, max_retries=10, timeout=None, pb2_module=self.pb2_api[1]) self.logger.info(f"[{time.strftime('%H:%M:%S')}] Prepare ProtobufController ") controller = ProtobufController('ProtobufController', host=self.target_host, port=self.target_port) target.set_controller(controller) #target.set_expect_response('true') self.logger.info(f"[{time.strftime('%H:%M:%S')}] Defining GraphModel") model = GraphModel() model.connect(template_a) self.logger.info(f"[{time.strftime('%H:%M:%S')}] Prepare Server Fuzzer ") fuzzer = ServerFuzzer() fuzzer.set_interface(WebInterface(port=26001)) fuzzer.set_model(model) fuzzer.set_target(target) fuzzer.start() self.logger.info(f"[{time.strftime('%H:%M:%S')}] Start Fuzzer") self.logger.info(f"[Further info are in the related Kitty log output!]") six.moves.input('press enter to exit') self.logger.info(f"[{time.strftime('%H:%M:%S')}] End Fuzzer Session") fuzzer.stop()
def _get_message_by_config(self, pb_obj: object) -> list: """ A compare and section creation depending on the provided class list in ['protobuf']['classes'] config object; If it is empty we rely on the protobuf descriptor information and we process every top level messages, if list is provided then the logic seeking for section amid messages and config provided messages, obviously logic process only the section of the two aggregation. For comparing the two lists the logic uses the string representation of messages via _message_types_by_names. The method will returns the list of message_types_by_items method which is a list of tuples including the string representation of the message and the object as well. :param pb_obj: protobuf object :return: list of top level messages :rtype: list """ configuration = ConfigParser() messages_by_config: list = configuration.get_protobuf_classes_to_send() messages_by_name: list = self._message_types_by_name_keys(pb_obj) messages_by_item: list = self._message_types_by_name_items(pb_obj) if not messages_by_config: return list(messages_by_item) intersection = list(set(messages_by_name) & set(messages_by_config)) if len(messages_by_config) == len(intersection) and \ len(list(messages_by_name)) == len(intersection): self.logger.info( f"PPROT MSG - Messages provided by configuration: {messages_by_config} " f"messages by pb object: {list(messages_by_name)} - section: {intersection} - OK!" ) return list(messages_by_item) if len(intersection) < len(list(messages_by_name)): for inter_item in intersection: for msg_str_item, _ in list(messages_by_item): if inter_item not in msg_str_item: # remove tuple from list return list( filter( lambda x: str(x[0]) not in str(msg_str_item), list(messages_by_item))) else: self.logger.info( f"PPROT MSG - Config message {len(messages_by_name)} and section " f"{intersection} mismatch!" f"Maybe something wrong in the config ['protobuf']['classes'] field!" ) raise MessageCompareException return []
def _get_case_description(self): get_desc = ConfigParser() if get_desc.get_protocol_dns() is True: return get_desc.get_dns_case_desc() elif get_desc.get_protocol_http() is True: return get_desc.get_http_case_desc() elif get_desc.get_protocol_protobuf is True: return get_desc.get_protobuf_case_desc()
def __init__(self, name='HttpRunner', logger=None) -> object: """ HttpRunner constructor :param name: the name of the class :param logger: logger from the framework """ super(HttpRunner, self).__init__(name, logger) self.config = ConfigParser() self.target_host = self.config.get_target_host_name() self.target_port = self.config.get_target_port() self.http_get = self.config.get_http_get_method() self.http_post_put = self.config.get_http_post_put_method() self.http_post_update = self.config.get_http_post_update_method() self.http_delete = self.config.get_http_delete_method() self.http_fuzz_protocol = self.config.get_http_fuzz_protocol() self.http_path = self.config.get_http_path() self.http_content_type = self.config.get_http_content_type() self.http_payload = self.config.get_http_payload() self.gen_uuid = GenerateUUID.generate_uuid()
class ProtobufTarget(ServerTarget): """ ProtobufTarget will create files with the fuzzed payloads """ def __init__(self, name=str, host=str, port=int, max_retries=10, timeout=None, logger=None, pb2_module=None) -> None: """ Class creates the protobuf payload with the marriage of Fuzzed data. :param name: name of the class :param host: target host :param port: target port :param max_retries: maximum retries of connection :param timeout: timeout :param logger: kitty logger """ super(ProtobufTarget, self).__init__(name, logger) self.host = host self.port = port if (host is None) or (port is None): raise ValueError('Host and port may not be None') self.timeout = timeout self.socket = None self.max_retries = max_retries self.pb2_module = pb2_module self.set_expect_response(False) self.config = ConfigParser() self.verbosity = self.config.get_generic_verbosity() self.logger.setLevel(self.verbosity) self.module_path = self.config.get_module_path() self._uuid = GenerateUUID.generate_uuid() self.frmwrk_utils = FrameworkUtils() def pre_test(self, test_num=int) -> None: """ This is only checks whether the target is available or not. :param test_num: The number of the test case. """ super(ProtobufTarget, self).pre_test(test_num) retry_count = 0 while self.socket is None and retry_count < self.max_retries: sock = self._get_socket() if self.timeout is not None: sock.settimeout(self.timeout) try: retry_count += 1 sock.connect((self.host, self.port)) self.socket = sock except Exception: sock.close() self.logger.error( f"PBTARGET - TCP Error: {traceback.format_exc()}") self.logger.error( f"PBTARGET - TCP Failed to connect to target server, retrying..." ) time.sleep(1) if self.socket is None: raise (KittyException( 'PBTARGET - TCPTarget: (pre_test) cannot connect to server (retries = %d' % retry_count)) def _get_socket(self) -> socket: """ Get a socket object """ return socket.socket(socket.AF_INET, socket.SOCK_STREAM) def post_test(self, test_num=int): """ Called after a test is completed, perform cleanup etc. """ super(ProtobufTarget, self).post_test(test_num) if self.socket is not None: self.socket.close() self.socket = None if self.report.get('status') != Report.PASSED: if not os.path.exists(os.getcwd() + '/results/'): os.makedirs(os.getcwd() + '/results/') test_num = self.report.get('test_number') with open(os.getcwd() + '/results/{}-result-protobuf-{}.json'.format( test_num, str(self._uuid)), 'w', encoding='utf-8') as file: json.dump(self.report.to_dict(), file, ensure_ascii=False, indent=4) def _construct_message(self, data, pb2_module): msg = None to_dict = json.loads(data.decode()) counter = 0 for key, val in to_dict.items(): if counter == 0: module_to_put_into = getattr(pb2_module, key) msg = json_format.ParseDict(val, module_to_put_into()) else: # TODO Should implement a mechanism to send together # TODO top level messages. # TODO Currently sending only one top level for.ie.: Request # TODO and all its nested messages. raise NotImplementedError counter += 1 return msg def _send_to_target(self, data): """ HTTP POST with protobuf binary payload: POST / HTTP/1.1 Host: localhost:8000 Content-Type: : 'application/octet-stream' "b'|\x00\x00\x00\n\x05fdbcd\x10{\x1a\x05fdbcd"\t\n\x05fdbcd\x10\x01:_\n\x05fdbcd\x10{\x18\x01""\n\x05fdbcd\x10{\x1a\x05fdbcd"\x10\n\x05fdbcd\x10{\x1a\x05fdbcd*\x10\n\x05fdbcd\x10{\x1a\x05fdbcd2\x10\n\x05fdbcd\x10{\x1a\x05fdbcdB\n\n\x06%u0000\x10{'" https://stackoverflow.com/questions/28670835/python-socket-client-post-parameters :param data: :return: """ proto_payload = self._construct_message(data, self.pb2_module) host = None port = None headers = """\ POST / HTTP/1.1\r \r\n\ Content-Type: {content_type}\r Content-Length: {content_length}\r \r\n""" body_bytes = self.frmwrk_utils.encode_message(proto_payload) # Evaluation of the message. #self.frmwrk_utils.decode_message(body_bytes, lookup_pb2, 'Request') header_bytes = headers.format(content_type="application/octet-stream", content_length=len(body_bytes), host=str(host) + ":" + str(port)).encode('iso-8859-1') payload = header_bytes + body_bytes self.socket.send(payload) def _receive_from_target(self): return self.socket.recv(10000) def transmit(self, payload): """ This is the original transmit method from ServerTarget overwritten with special cases such as 40X or 50X according to the aim of the test. Accordin to https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500 500 Internal Server Error 501 Not Implemented 502 Bad Gateway 503 Service Unavailable 504 Gateway Timeout 505 HTTP Version Not Supported 506 Variant Also Negotiates 507 Insufficient Storage 508 Loop Detected 510 Not Extended 511 Network Authentication Required Original method docstring: Transmit single payload, and receive response, if expected. The actual implementation of the send/receive should be in ``_send_to_target`` and ``_receive_from_target``. :type payload: str :param payload: payload to send :rtype: str :return: the response (if received) """ SERVER_50x_CODES = [ '500 Internal Server Error', '501 Not Implemented', '502 Bad Gateway', '503 Service Unavailable', '504 Gateway Timeout', '505 HTTP Version Not Supported', '506 Variant Also Negotiates', '507 Insufficient Storage', '508 Loop Detected', '510 Not Extended', '511 Network Authentication Required' ] SERVER_40xCODES = [ '400 Bad Request', '401 Unauthorized', '402 Payment Required', '403 Forbidden', '404 Not Found', '405 Method Not Allowed', '406 Not Acceptable', '407 Proxy Authentication Required', '408 Request Timeout', '409 Conflict', '410 Gone', '411 Length Required', '412 Precondition Failed', '413 Payload Too Large', '414 URI Too Long', '415 Unsupported Media Type', '416 Range Not Satisfiable', '417 Expectation Failed', '422 Unprocessable Entity', '425 Too Early', '426 Upgrade Required', '428 Precondition Required', '429 Too Many Requests', '431 Request Header Fields Too Large', '451 Unavailable For Legal Reasons' ] response = None trans_report_name = 'transmission_0x%04x' % self.transmission_count trans_report = Report(trans_report_name) self.transmission_report = trans_report self.report.add(trans_report_name, trans_report) try: trans_report.add('request (hex)', hexlify(payload).decode()) trans_report.add('request (raw)', '%s' % payload) trans_report.add('request length', len(payload)) trans_report.add('request time', time.time()) request = hexlify(payload).decode() request = request if len(request) < 100 else (request[:100] + ' ...') self.logger.info(f"request({len(payload)}): {request}") self.logger.debug(f"payload {payload}") self._send_to_target(payload) trans_report.success() if self.expect_response: try: response = self._receive_from_target() trans_report.add('response time', time.time()) trans_report.add('response (hex)', hexlify(response).decode()) trans_report.add('response (raw)', '%s' % response) trans_report.add('response length', len(response)) trans_report.add('Session ID', str(self._uuid)) printed_response = hexlify(response).decode() printed_response = printed_response if len( printed_response) < 100 else (printed_response[:100] + ' ...') self.logger.info( f"response({len(response)}): {printed_response}") string_response = response.decode('utf-8') response_code_string = string_response.splitlines()[0] response_code = response_code_string.replace( 'HTTP/1.1 ', '') if response_code in SERVER_40xCODES or response_code in SERVER_50x_CODES: self.logger.info( f"response failure {response.decode('utf-8')}") trans_report.failed('Failure in HTTP-PROTO response.') trans_report.add('Response', response.decode('utf-8')) self.report.set_status('failed') self.receive_failure = True except Exception as ex2: trans_report.failed('failed to receive response: %s' % ex2) trans_report.add('traceback', traceback.format_exc()) self.logger.error( f"target.transmit - failure in receive (exception: {ex2})" ) self.logger.error(traceback.format_exc()) self.receive_failure = True else: response = '' except Exception as ex1: #trans_report.failed('failed to send payload: %s' % ex1) #trans_report.add('traceback', traceback.format_exc()) self.logger.error( f"target.transmit - failure in send (exception: {ex1})") self.logger.error(traceback.format_exc()) #self.send_failure = True self.transmission_count += 1 return response
class Runner(FuzzObject): """ This is the main runner class of the Fuzzer Framework. It integrates the kitty/katnip components and the framework components. See documentation for the usage. """ def __init__(self, name='Runner', logger=None): super(Runner, self).__init__(name, logger) self.config = ConfigParser() self.framework_utils = FrameworkUtils() self.put_archive_to_s3 = PutArchiveToS3() self.proto_path = self.config.get_generic_proto_path() self.pb2_path = self.config.get_generic_pb2_path() self.verbosity = self.config.get_generic_verbosity() self.module_path = self.config.get_module_path() self.proto_modules = self.config.get_protobuf_modules() self.proto_classes = self.config.get_protobuf_classes_to_send() self.protocol_proto = self.config.get_protocol_protobuf() self.protocol_http = self.config.get_protocol_http() self.protocol_dns = self.config.get_protocol_dns() self.set_verbosity(self.verbosity) self.use_s3_to_archive = self.config.get_archive_to_s3() def search_files(self) -> tuple: """ Creates lists of raw and compiled proto files paired into tuple; :return: raw and compiled proto file lists in tuple; """ search_raw_proto = SearchFile(file_type='.proto', file_path=self.proto_path) raw_proto_files = search_raw_proto.search_file self.logger.debug(f'RAW proto files {raw_proto_files}') search_api = SearchFile(file_type='_pb2.py', file_path=self.pb2_path) compiled_proto_files = search_api.search_file self.logger.debug(f'Compiled proto files {compiled_proto_files}') return raw_proto_files, compiled_proto_files def checksum_files(self, raw_proto: list) -> list: # TODO for further usage, function not in use. checksum_files = self.framework_utils.md5_checksum_for_proto_files(raw_proto) self.logger.debug(f'Checksum {checksum_files}') return checksum_files @property def process_pb2(self) -> object: """ This function parses the compiled protobuf API and produces a Dictionary representation of it. :return: object """ compiled_proto_files = self.search_files()[1] for item in compiled_proto_files: if len(item.split('_')) > 2: raise FileNameError # raise if file name contains more then 2 underline. elif item.split('_')[0] in self.config.get_protobuf_modules(): parse_file = ParseProtoApi(pb2_api_file=item, module_path=self.module_path) return parse_file.execute_api_parse() else: self.logger.info(f'File {item} was not part of the inspection.') def tear_down(self): """ The common tear down sequence of a fuzzing session :return: None """ self.framework_utils.results_dir() self.framework_utils.rename_log_file() self.framework_utils.archive_locally() self.report() self.framework_utils.archive_xml() if self.use_s3_to_archive: self.put_archive_to_s3.do_upload(self.framework_utils.dir_files_to_archive()) def report(self): _report = CreateReport() _report.run_report() def run(self): if self.config.get_banner(): file1 = open(os.getcwd() + '/banner', 'r') Lines = file1.readlines() for line in Lines: print(f' {line.strip()}') for i in range(5, 0, -1): sys.stdout.write(str(i) + '.....') sys.stdout.flush() time.sleep(1) if self.protocol_proto: self.logger.info(f"[{time.strftime('%H:%M:%S')}] Starting PROTOBUF Fuzzer session") try: api = self.process_pb2 do_proto = ProtobufRunner(pb2_api=api) do_proto.run_proto() except SystemError as e: self.logger.error(f"Interpreter found an internal error: {e}") except EnvironmentError as e: self.logger(f"Error occur outside the Python environment: {e}") finally: self.logger.info(f"[{time.strftime('%H:%M:%S')}] Flush cache...") self.logger.info(f"[{time.strftime('%H:%M:%S')}] Rename kitty related log files...") self.tear_down() elif self.protocol_dns: self.logger.info(f"[{time.strftime('%H:%M:%S')}] Starting DNS Fuzzer session") try: # query record type do_dns = DnsRunner() do_dns.run_dns() self.logger.info(f"[{time.strftime('%H:%M:%S')}] DNS runner started") except SystemError as e: self.logger.error(f"Interpreter finds an internal problem {e}") except EnvironmentError as e: self.logger(f"Error occur outside the Python environment: {e}") finally: self.logger.info(f"[{time.strftime('%H:%M:%S')}] Rename kitty related log files...") self.tear_down() elif self.protocol_http: self.logger.info(f"[{time.strftime('%H:%M:%S')}] Starting HTTP Fuzzer session") try: do_http = HttpRunner() do_http.run_http() self.logger.info(f"[{time.strftime('%H:%M:%S')}] HTTP runner started") except SystemError as e: self.logger.error(f"Interpreter finds an internal problem {e}") except EnvironmentError as e: self.logger(f"Error occur outside the Python environment: {e}") finally: self.logger.info(f"[{time.strftime('%H:%M:%S')}] Rename kitty related log files...") self.tear_down()
class HttpRunner(FuzzObject): """ HttpRunner class created according to the Kitty/Katnip API documentations. The class is a part of the Fuzzer Framework. """ def __init__(self, name='HttpRunner', logger=None) -> object: """ HttpRunner constructor :param name: the name of the class :param logger: logger from the framework """ super(HttpRunner, self).__init__(name, logger) self.config = ConfigParser() self.target_host = self.config.get_target_host_name() self.target_port = self.config.get_target_port() self.http_get = self.config.get_http_get_method() self.http_post_put = self.config.get_http_post_put_method() self.http_post_update = self.config.get_http_post_update_method() self.http_delete = self.config.get_http_delete_method() self.http_fuzz_protocol = self.config.get_http_fuzz_protocol() self.http_path = self.config.get_http_path() self.http_content_type = self.config.get_http_content_type() self.http_payload = self.config.get_http_payload() self.gen_uuid = GenerateUUID.generate_uuid() def run_http(self) -> None: """ This method provides the HTTP GET, POST, ... , templating for the HTTP header as fields, data provided by the config, explained in the User Documentation. kitty low level field model https://kitty.readthedocs.io/en/latest/kitty.model.low_level.field.html :returns: None :rtype: None """ http_template = None # HTTP GET TEMPLATE self.logger.info( f"[{time.strftime('%H:%M:%S')}] Initiate template for HTTP GET ..." ) if self.http_get: http_template = Template( name='HTTP_GET', fields=[ # GET / HTTP/1.1 String('GET', name='method', fuzzable=False), Delimiter(' ', name='delimiter-1', fuzzable=False), String(self.http_path, name='path'), Delimiter(' ', name='delimiter-2', fuzzable=self.http_fuzz_protocol), String('HTTP', name='protocol name', fuzzable=self.http_fuzz_protocol), Delimiter('/', name='fws-1', fuzzable=self.http_fuzz_protocol), Dword(1, name='major version', encoder=ENC_INT_DEC, fuzzable=self.http_fuzz_protocol), Delimiter('.', name='dot-1', fuzzable=self.http_fuzz_protocol), Dword(1, name='minor version', encoder=ENC_INT_DEC, fuzzable=self.http_fuzz_protocol), Static('\r\n', name='EOL-1'), # User agent String('User-Agent:', name='user_agent_field', fuzzable=self.http_fuzz_protocol), Delimiter(' ', name='delimiter-3', fuzzable=self.http_fuzz_protocol), String('Fuzzer', name='user-agent_name', fuzzable=self.http_fuzz_protocol), Static('\r\n', name='EOL-2'), # Token generated by framework to support following the session if necessary. String('Fuzzer-Token:', name='fuzzer_token', fuzzable=self.http_fuzz_protocol), Delimiter(' ', name='delimiter-4', fuzzable=self.http_fuzz_protocol), String(str(self.gen_uuid), name='fuzzer_token_type', fuzzable=False), # do not fuzz token Static('\r\n', name='EOL-3'), # Accept String('Accept:', name='accept', fuzzable=self.http_fuzz_protocol), Delimiter(' ', name='delimiter-5', fuzzable=self.http_fuzz_protocol), String('*/*', name='accept_type_', fuzzable=self.http_fuzz_protocol), Static('\r\n', name='EOL-4'), # Cache-control no-cache by default String('Cache-Control:', name='cache-control', fuzzable=self.http_fuzz_protocol), Delimiter(' ', name='delimiter-6', fuzzable=self.http_fuzz_protocol), String('no-cache', name='cache_control_type', fuzzable=self.http_fuzz_protocol), Static('\r\n', name='EOL-5'), # Host, the target host String('Host:', name='host_name', fuzzable=self.http_fuzz_protocol), Delimiter(' ', name='delimiter-7', fuzzable=self.http_fuzz_protocol), String(self.target_host, name='target_host', fuzzable=False), # do not fuzz target host address! Static('\r\n', name='EOL-6'), # Connection close, do not use keep-alive it results only one mutation, than the # fuzzer will hang. String('Connection:', name='accept_encoding', fuzzable=self.http_fuzz_protocol), Delimiter(' ', name='delimiter-8', fuzzable=self.http_fuzz_protocol), String('close', name='accept_encoding_types', fuzzable=False), # do not fuzz this field! Static('\r\n', name='EOM-7'), # Content-type from config. String('Content-Type:', name='Content-Type', fuzzable=self.http_fuzz_protocol), Delimiter(' ', name='delimiter-9', fuzzable=self.http_fuzz_protocol), String(self.http_content_type, name='content_type_', fuzzable=self.http_fuzz_protocol), Static('\r\n\r\n', name='EOM-8') ]) if self.http_post_put: self.logger.info( f"[{time.strftime('%H:%M:%S')}] Initiate template for HTTP POST ..." ) http_template = Template( name='HTTP_POST', fields=[ # POST / HTTP/1.1 String('POST', name='method', fuzzable=False), Delimiter(' ', name='delimiter-1', fuzzable=False), String(self.http_path, name='path'), Delimiter(' ', name='delimiter-2', fuzzable=self.http_fuzz_protocol), String('HTTP', name='protocol name', fuzzable=self.http_fuzz_protocol), Delimiter('/', name='fws-1', fuzzable=self.http_fuzz_protocol), Dword(1, name='major version', encoder=ENC_INT_DEC, fuzzable=self.http_fuzz_protocol), Delimiter('.', name='dot-1', fuzzable=self.http_fuzz_protocol), Dword(1, name='minor version', encoder=ENC_INT_DEC, fuzzable=self.http_fuzz_protocol), Static('\r\n', name='EOL-1'), # User agent String('User-Agent:', name='user_agent_field', fuzzable=self.http_fuzz_protocol), Delimiter(' ', name='delimiter-3', fuzzable=self.http_fuzz_protocol), String('Fuzzer', name='user-agent_name', fuzzable=self.http_fuzz_protocol), Static('\r\n', name='EOL-2'), # Token generated by framework to support following the session if necessary. String('Fuzzer-Token:', name='fuzzer_token', fuzzable=self.http_fuzz_protocol), Delimiter(' ', name='delimiter-4', fuzzable=self.http_fuzz_protocol), String(str(self.gen_uuid), name='fuzzer_token_type', fuzzable=self.http_fuzz_protocol), Static('\r\n', name='EOL-3'), # Accept String('Accept:', name='accept', fuzzable=self.http_fuzz_protocol), Delimiter(' ', name='delimiter-5', fuzzable=self.http_fuzz_protocol), String('*/*', name='accept_type_', fuzzable=self.http_fuzz_protocol), Static('\r\n', name='EOL-4'), # Cache-control no-cache by default String('Cache-Control:', name='cache-control', fuzzable=self.http_fuzz_protocol), Delimiter(' ', name='delimiter-6', fuzzable=self.http_fuzz_protocol), String('no-cache', name='cache_control_type', fuzzable=self.http_fuzz_protocol), Static('\r\n', name='EOL-5'), # Host, the target host String('Host:', name='host_name', fuzzable=self.http_fuzz_protocol), Delimiter(' ', name='delimiter-7', fuzzable=self.http_fuzz_protocol), String(self.target_host, name='target_host', fuzzable=False), # do not fuzz target host address! Static('\r\n', name='EOL-6'), # Content length: obvious payload lenght. String('Content-Length:', name='content_length', fuzzable=self.http_fuzz_protocol), Delimiter(' ', name='delimiter-9', fuzzable=self.http_fuzz_protocol), String(str(len(self.http_payload)), name='content_length_len', fuzzable=False), Static('\r\n', name='EOM-8'), # Connection close, do not use keep-alive it results only one mutation, than the # fuzzer will hang. String('Connection:', name='accept_encoding', fuzzable=self.http_fuzz_protocol), Delimiter(' ', name='delimiter-8', fuzzable=self.http_fuzz_protocol), String('close', name='accept_encoding_types', fuzzable=False), # do not fuzz this field! Static('\r\n', name='EOM-7'), # Content type String('Content-Type:', name='Content-Type', fuzzable=self.http_fuzz_protocol), Delimiter(' ', name='delimiter-10', fuzzable=self.http_fuzz_protocol), String(self.http_content_type, name='content_type_', fuzzable=self.http_fuzz_protocol), Static('\n\r\n', name='EOM-9'), # Payload String(self.http_payload, name='payload'), Static('\r\n\r\n', name='EOM-10') ]) self.logger.info( f"[{time.strftime('%H:%M:%S')}] Prepare HttpTarget ...") target = HttpTarget(name='HttpTarget', host=self.target_host, port=self.target_port, max_retries=10, timeout=None) target.set_expect_response('true') self.logger.info( f"[{time.strftime('%H:%M:%S')}] Prepare HttpController ...") controller = HttpGetController('HttpGetController', host=self.target_host, port=self.target_port) target.set_controller(controller) self.logger.info( f"[{time.strftime('%H:%M:%S')}] Defining GraphModel...") model = GraphModel() model.connect(http_template) fuzzer = ServerFuzzer() fuzzer.set_interface(WebInterface(port=26001)) fuzzer.set_model(model) fuzzer.set_target(target) fuzzer.set_delay_between_tests(1) self.logger.info(f"[{time.strftime('%H:%M:%S')}] Start Fuzzer...") self.logger.info( f"[Further info are in the related Kitty log output!]") fuzzer.start() self.logger.info(f"[{time.strftime('%H:%M:%S')}] End Fuzzer Session") fuzzer.stop()
class HttpTarget(ServerTarget): """ HttpTarget is implementation of a TCP target for the ServerFuzzer """ def __init__(self, name, host, port, max_retries=10, timeout=None, logger=None) -> object: """ :param name: name of the target :param host: host ip (to send data to) currently unused :param port: port to send to :param max_retries: maximum connection retries (default: 10) :param timeout: socket timeout (default: None) :param logger: logger for the object (default: None) """ super(HttpTarget, self).__init__(name, logger) self.host = host self.port = port if (host is None) or (port is None): raise ValueError('host and port may not be None') self.timeout = timeout self.socket = None self.max_retries = max_retries self.config = ConfigParser() self.use_tls = self.config.get_tls() self.target_host = self.config.get_target_host_name() self.report = Report('report') self._uuid = GenerateUUID.generate_uuid() def pre_test(self, test_num=str) -> None: """Katnip original method.""" super(HttpTarget, self).pre_test(test_num) retry_count = 0 while self.socket is None and retry_count < self.max_retries: sock = self._get_socket() if self.timeout is not None: sock.settimeout(self.timeout) try: retry_count += 1 sock.connect((self.host, self.port)) self.socket = sock except Exception: sock.close() self.logger.error(f"Error: {traceback.format_exc()}") self.logger.error( f"Failed to connect to target server, retrying...") time.sleep(1) if self.socket is None: raise (KittyException( 'TCPTarget: (pre_test) cannot connect to server (retries = %d' % retry_count)) def _get_socket(self) -> socket: """ Katnip original method. Get a Socket object. Extended with Python3.x TLS socket wrapper """ if self.use_tls: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(10) socket_handler = socket.socket(socket.AF_INET, socket.SOCK_STREAM) socket_wraped = ssl.create_default_context().wrap_socket( socket_handler, server_hostname=self.target_host) return socket_wraped elif not self.use_tls: return socket.socket(socket.AF_INET, socket.SOCK_STREAM) def post_test(self, test_num=str) -> None: """Katnip original method.""" super(HttpTarget, self).post_test(test_num) if self.socket is not None: self.socket.close() self.socket = None if self.report.get('status') != Report.PASSED: if not os.path.exists(os.getcwd() + '/results/'): os.makedirs(os.getcwd() + '/results/') test_num = self.report.get('test_number') with open(os.getcwd() + '/results/{}-result-http-{}.json'.format( test_num, str(self._uuid)), 'w', encoding='utf-8') as file: json.dump(self.report.to_dict(), file, ensure_ascii=False, indent=4) file.close() def _raw_data(self, data=bytes) -> None: """Convert bytes data to UTF-8""" try: self.logger.info(f"Data sent: {data.decode('utf-8')}") except UnicodeDecodeError as err: """ UnicodeDecodeError: 'utf-8' codec can't decode byte 0xfe in position 14: invalid start byte Fuzzer transform data so I have to log the bytes format. """ self.logger.info(f"Data sent: {data}, error {err}") def _send_to_target(self, data=bytes) -> None: self._raw_data(data) self.socket.send(data) def _receive_from_target(self) -> bytes: return self.socket.recv(10000) def transmit(self, payload): """ This is the original transmit method from ServerTarget overwritten with special cases such as 40X or 50X according to the aim of the test. According to https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500 500 Internal Server Error 501 Not Implemented 502 Bad Gateway 503 Service Unavailable 504 Gateway Timeout 505 HTTP Version Not Supported 506 Variant Also Negotiates 507 Insufficient Storage 508 Loop Detected 510 Not Extended 511 Network Authentication Required Original method docstring: Transmit single payload, and receive response, if expected. The actual implementation of the send/receive should be in ``_send_to_target`` and ``_receive_from_target``. :type payload: str :param payload: payload to send :rtype: str :return: the response (if received) """ SERVER_50x_CODES = [ '500 Internal Server Error', '501 Not Implemented', '502 Bad Gateway', '503 Service Unavailable', '504 Gateway Timeout', '505 HTTP Version Not Supported', '506 Variant Also Negotiates', '507 Insufficient Storage', '508 Loop Detected', '510 Not Extended', '511 Network Authentication Required' ] SERVER_40xCODES = [ '400 Bad Request', '401 Unauthorized', '402 Payment Required', '403 Forbidden', '404 Not Found', '405 Method Not Allowed', '406 Not Acceptable', '407 Proxy Authentication Required', '408 Request Timeout', '409 Conflict', '410 Gone', '411 Length Required', '412 Precondition Failed', '413 Payload Too Large', '414 URI Too Long', '415 Unsupported Media Type', '416 Range Not Satisfiable', '417 Expectation Failed', '422 Unprocessable Entity', '425 Too Early', '426 Upgrade Required', '428 Precondition Required', '429 Too Many Requests', '431 Request Header Fields Too Large', '451 Unavailable For Legal Reasons' ] response = None trans_report_name = 'transmission_0x%04x' % self.transmission_count trans_report = Report(trans_report_name) self.transmission_report = trans_report self.report.add(trans_report_name, trans_report) try: trans_report.add('request (hex)', hexlify(payload).decode()) trans_report.add('request (raw)', '%s' % payload) trans_report.add('request length', len(payload)) trans_report.add('request time', time.time()) request = hexlify(payload).decode() request = request if len(request) < 100 else (request[:100] + ' ...') self.logger.info('request(%d): %s' % (len(payload), request)) self._send_to_target(payload) trans_report.success() if self.expect_response: try: response = self._receive_from_target() trans_report.add('response time', time.time()) trans_report.add('response (hex)', hexlify(response).decode()) trans_report.add('response (raw)', '%s' % response) trans_report.add('response length', len(response)) trans_report.add('Session ID', str(self._uuid)) printed_response = hexlify(response).decode() printed_response = printed_response if len( printed_response) < 100 else (printed_response[:100] + ' ...') self.logger.info( f"response{len(response)}: {printed_response}") self.logger.debug(response.decode('utf-8')) string_response = response.decode('utf-8') response_code_string = string_response.splitlines()[0] response_code = response_code_string.replace( 'HTTP/1.1 ', '') if response_code in SERVER_40xCODES or response_code in SERVER_50x_CODES: self.logger.info( f"response failure {response.decode('utf-8')}") trans_report.failed('Failure in HTTP response.') trans_report.add('Response', response.decode('utf-8')) self.report.set_status('failed') self.receive_failure = True except Exception as ex2: trans_report.failed('failed to receive response: %s' % ex2) trans_report.add('traceback', traceback.format_exc()) self.logger.error( f"target.transmit - failure in receive (exception: {ex2})" ) self.logger.error(traceback.format_exc()) self.receive_failure = True else: response = '' except Exception as ex1: trans_report.failed(f"failed to send payload: {ex1}") trans_report.add('traceback', traceback.format_exc()) self.logger.error( f"target.transmit - failure in send (exception: {ex1})") self.logger.error(traceback.format_exc()) self.send_failure = True self.transmission_count += 1 return response
class DnsRunner(FuzzObject): def __init__(self, name='DnsRunner', logger=None): super(DnsRunner, self).__init__(name, logger) self.config = ConfigParser() self.target_host = self.config.get_target_host_name() self.target_port = self.config.get_target_port() self.timeout = self.config.get_dns_timout() self.tld = self.config.get_dns_tld() self.default_labels = self.config.get_dns_default_labels() def run_dns(self): """ kitty low level field model https://kitty.readthedocs.io/en/latest/kitty.model.low_level.field.html """ fields = [] counter = 0 dns_label_length = len(self.default_labels.split('.')) dns_label_list = self.default_labels.split('.') self.logger.info( f"[{time.strftime('%H:%M:%S')}] Initiate template for DNS ...") while counter < dns_label_length: fields.append( String(dns_label_list[counter], name='sub_domain_' + str(counter), max_size=10)) fields.append(Delimiter('.', name='delimiter_' + str(counter))) counter += 1 fields.append(String(self.tld, name='tld', fuzzable=False)) dns_query = Template(name='DNS_QUERY', fields=fields) """ dns_query = Template(name='DNS_QUERY', fields=[ String('r', name='sub_domain', max_size=10), Delimiter('.', name='space1"), String('rf', name='sub_domain2', max_size=10), Delimiter('.', name='space2"), String(self.tld, name='tld', fuzzable=False), ]) """ # define target, in this case this is SslTarget because of HTTPS self.logger.info( f"[{time.strftime('%H:%M:%S')}] Prepare DnsTarget ...") target = DnsTarget(name='DnsTarget', host=self.target_host, port=self.target_port, timeout=self.timeout) target.set_expect_response('true') self.logger.info( f"[{time.strftime('%H:%M:%S')}] Prepare DnsController ...") controller = DnsController('DnsController', host=self.target_host, port=self.target_port) target.set_controller(controller) # Define model self.logger.info( f"[{time.strftime('%H:%M:%S')}] Defining GraphModel...") model = GraphModel() model.connect(dns_query) self.logger.info( f"[{time.strftime('%H:%M:%S')}] Prepare Server Fuzzer ...") fuzzer = ServerFuzzer() fuzzer.set_interface(WebInterface(port=26001)) fuzzer.set_model(model) fuzzer.set_target(target) fuzzer.set_delay_between_tests(1) self.logger.info(f"[{time.strftime('%H:%M:%S')}] Start Fuzzer...") self.logger.info( f"[Further info are in the related Kitty log output!]") fuzzer.start() self.logger.info(f"[{time.strftime('%H:%M:%S')}] End Fuzzer Session") fuzzer.stop()
class DnsTarget(ServerTarget): """ DnsTarget is implementation of a DNS target, which is inherited from ServerTarget and uses socket DGRAM to send DNS query over UDP. """ def __init__(self, name, host, port, timeout=None, logger=None): """ :param name: name of the target :param host: host ip (to send data to) currently unused :param port: port to send to :param timeout: socket timeout (default: None) :param logger: logger for the object (default: None) """ super(DnsTarget, self).__init__(name, logger) self.host = host self.port = port if (host is None) or (port is None): raise ValueError('Host and port may not be None!') self.timeout = timeout self.socket = None self.bind_host = None self.bind_port = None self.expect_response = False self.config = ConfigParser() self.dns_a_record_status = self.config.get_dns_a_record_status() self.dns_ns_record_status = self.config.get_dns_ns_record_status() self.dns_txt_record_status = self.config.get_dns_txt_record_status() self.verbosity = self.config.get_generic_verbosity() self.logger.setLevel(self.verbosity) self._uuid = GenerateUUID.generate_uuid() self.question_length = None def set_binding(self, host=str, port=int, expect_response=False) -> None: """ Enable binding of socket to given ip/address Katnip original method. """ self.bind_host = host self.bind_port = port self.expect_response = expect_response self._do_bind() def _do_bind(self) -> None: """Katnip original method.""" self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) self.socket.bind((self.bind_host, self.bind_port)) def _prepare_socket(self) -> None: """Katnip original method.""" if self.bind_host is None: self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) else: self._do_bind() def pre_test(self, test_num=str) -> None: """Katnip original method.""" super(DnsTarget, self).pre_test(test_num) if self.socket is None: self._prepare_socket() if self.timeout is not None: self.socket.settimeout(self.timeout) def post_test(self, test_num=str) -> None: """ Katnip original method. Extended with report export. """ super(DnsTarget, self).post_test(test_num) if self.socket is not None: self.socket.close() self.socket = None if self.report.get('status') != Report.PASSED: if not os.path.exists(os.getcwd() + '/results/'): os.makedirs(os.getcwd() + '/results/') test_num = self.report.get('test_number') with open(os.getcwd() + '/results/{}-result-dns-{}.json'.format(test_num, str(self._uuid)), 'w', encoding='utf-8') as file: json.dump(self.report.to_dict(), file, ensure_ascii=False, indent=4) file.close() def _construct_dns_query(self, data=bytes) -> bytes: """ According to https://routley.io/posts/hand-writing-dns-messages/ and based on https://github.com/tigerlyb/DNS-Lookup-Tool-in-Python/blob/master/dnslookup.py :param data: the fuzzed data, in this case the DNS query foo.baz.bar.TLD :type data: bytes :return: the processed DNS query we will have been putting on wire. :rtype: bytes """ global qtype domain = None try: domain = data.decode('utf-8') except: """UnicodeDecodeError: 'utf-8' codec can't decode byte 0xfe in position 1: invalid start byte""" self.logger.error(f"UnicodeDecodeError: {data.decode('iso-8859-1')}") domain = data.decode('iso-8859-1') qname = b'' for label in domain.split('.'): # QNAME a domain name represented as a sequence of labels which consists # the length octet followed by the number of octets. # example.com consists two sections example and com # to construct the URL encode the section and producing series of bytes using struct in this case. # Section terminator is zero byte (00) qname = qname + struct.pack(b'!b' + str(len(label)).encode('utf-8') + b's', len(label), label.encode('utf-8')) """ Header: 0 1 2 3 4 5 6 7 8 9 A B C D E F +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | ID | header_layer_1 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ |QR| Opcode |AA|TC|RD|RA| Z | RCODE | header_layer_2 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | QDCOUNT | header_layer_3 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | ANCOUNT | header_layer_4 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | NSCOUNT | header_layer_5 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | ARCOUNT | header_layer_6 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ """ header_layer_1 = b'\x41\x41' # ID AA this is an ID assigned by the program that generates the query header_layer_2 = b'\x01\x00' # QR Query parameters, QR: 1 bit flag query or response, we set this flag to 0. # Opcode A 4 bit field # 0: inverse query, # 1: standard query, # 2: server status request # 3: reserved for further usage # TC: 1 bit flag to truncat message # RD: 1 bit flag if recursion is desired # QDCOUNT: 16 bit integer to specifying the number of entries in the question section. # 01 00 hexadecimal is 0000 0001 0000 0000 from QR to RCODE represents a standard # DNS query. Fields are not mentioned sets to 0. header_layer_3 = b'\x00\x01' # Number of questions, we ask one question per query header_layer_4 = b'\x00\x00' # Number of answers header_layer_5 = b'\x00\x00' # Number of authority records header_layer_6 = b'\x00\x00' # Number of additional records header = header_layer_1 + header_layer_2 + header_layer_3 + header_layer_4 + header_layer_5 + header_layer_6 # print(header) # print(type(header)) """ 0 1 2 3 4 5 6 7 8 9 A B C D E F +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | | / QNAME / / / +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | QTYPE | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | QCLASS | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ """ # QNAME holds by domain_labels variable, explained above. # QTYPE the DNS record type we looking for # QTYPE A: 1 NS: 2 TXT: 16 # QCLASS the class we are looking up, we using IN which has a value of 1, internet. # here again terminate x00\x00 is the zero byte terminator. terminate = b'\x00\x00' if self.dns_a_record_status: qtype = b'\x01\x00' # 1 ask A record elif self.dns_ns_record_status: qtype = b'\x02\x00' # 2 ask NS elif self.dns_txt_record_status: qtype = b'\x10\x00' # 16 ask TXT record qclass = b'\x01' question = qname + terminate + qtype + qclass message = header + question self.question_length = len(question) # USE DNS LIB TO HELP DEBUGGING MALFORMED HEADER # debug_question_header = DNSBuffer(binascii.unhexlify(header)) # print('Debugg header: ', DNSHeader.parse(debug_question_header)) return message def _send_to_target(self, data): message = self._construct_dns_query(data) self.logger.debug(f"Sending data to host: {self.host}, {self.port}") try: self.logger.info(f"Sending data {data.decode('utf-8')}") except: self.logger.info(f"Sending data {data.decode('iso-8859-1')}") self.socket.sendto(message, (self.host, self.port)) def _receive_from_target(self) -> bytes: """ This function is in charge to process the DNS reply back on the road. According to the response: 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | | / / / NAME / | | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | TYPE | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | CLASS | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | TTL | | | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | RDLENGTH | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ / RDATA / / / +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ :return: the response in bytes :rtype: bytes """ response, addr = self.socket.recvfrom(1024) return response def transmit(self, payload): """ This is the original transmit method from ServerTarget overwritten with DNS response code specific reporting Accordin to: https://support.umbrella.com/hc/en-us/articles/232254248-Common-DNS-return-codes-for-any-DNS-service-and-Umbrella- NOERROR RCODE:0 DNS Query completed successfully FORMERR RCODE:1 DNS Query Format Error SERVFAIL RCODE:2 Server failed to complete the DNS request NXDOMAIN RCODE:3 Domain name does not exist. NOTIMP RCODE:4 Function not implemented REFUSED RCODE:5 The server refused to answer for the query YXDOMAIN RCODE:6 Name that should not exist, does exist XRRSET RCODE:7 RRset that should not exist, does exist NOTAUTH RCODE:8 Server not authoritative for the zone NOTZONE RCODE:9 Name not in zone Original method docstring: Transmit single payload, and receive response, if expected. The actual implementation of the send/receive should be in ``_send_to_target`` and ``_receive_from_target``. :type payload: str :param payload: payload to send :rtype: str :return: the response (if received) """ DNS_RETRUN_CODES = [ 'NOERROR', 'FORMERR', 'SERVFAIL', 'NXDOMAIN', 'NOTIMP', 'REFUSED', 'YXDOMAIN', 'XRRSET', 'NOTAUTH', 'NOTZONE' ] DNS_EXCLUDE = [0, 3, 5] response = None trans_report_name = 'transmission_0x%04x' % self.transmission_count trans_report = Report(trans_report_name) self.transmission_report = trans_report self.report.add(trans_report_name, trans_report) try: trans_report.add('request (hex)', hexlify(payload).decode()) trans_report.add('request (raw)', '%s' % payload) trans_report.add('request length', len(payload)) trans_report.add('request time', time.time()) request = hexlify(payload).decode() request = request if len(request) < 100 else (request[:100] + ' ...') self.logger.info(f"request({len(payload)}): {request}") self._send_to_target(payload) trans_report.success() if self.expect_response: try: response = self._receive_from_target() trans_report.add('response time', time.time()) trans_report.add('response (hex)', hexlify(response).decode()) trans_report.add('response (raw)', '%s' % response) trans_report.add('response length', len(response)) trans_report.add('Session ID', str(self._uuid)) printed_response = hexlify(response).decode() printed_response = printed_response if len(printed_response) < 100 else (printed_response[:100] + ' ...') self.logger.info(f"response({len(response)}): {printed_response}") server_message = response.split(b',', 0) reply = codecs.encode(server_message[0], 'hex') # USE DNS LIB TO HELP DEBUGGING MALFORMED HEADER debug_response_header = DNSBufferExt(unhexlify(reply)) parsed_dns_response_header = DNSHeader.parse(debug_response_header) dns_response_message = DNSRecordExt.parse(server_message[0]) if int(parsed_dns_response_header.rcode) not in DNS_EXCLUDE: self.logger.error(f"DNS response code: " f"{DNS_RETRUN_CODES[int(parsed_dns_response_header.rcode)]}") self.logger.error(f"Debug header: {str(parsed_dns_response_header)}") trans_report.failed(f"Failure in response, code: " f"{DNS_RETRUN_CODES[int(parsed_dns_response_header.rcode)]}") trans_report.add('Response', str(dns_response_message)) trans_report.add('traceback', traceback.format_exc()) self.receive_failure = True self.report.set_status('failed') except Exception as ex2: trans_report.failed('failed to receive response: %s' % ex2) trans_report.add('traceback', traceback.format_exc()) self.logger.error(f"target.transmit - failure in receive (exception: {ex2})") self.logger.error(traceback.format_exc()) self.receive_failure = True else: response = '' except Exception as ex1: trans_report.failed('failed to send payload: %s' % ex1) trans_report.add('traceback', traceback.format_exc()) self.logger.error(f"target.transmit - failure in send (exception: {ex1})") self.logger.error(traceback.format_exc()) self.send_failure = True self.transmission_count += 1 return response