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 TestFramework(unittest.TestCase): def __init__(self, name='TestFramework'): super(TestFramework, self).__init__(name) self.framework_utils = FrameworkUtils() self.protobuf_target = ProtobufTarget() def test_01_convert_filepath_to_module_path(self): module_import_path = \ self.framework_utils.convert_filepath_to_module_path(file_path='/framework/models/pb2/api/') self.assertEqual(module_import_path, 'framework.models.pb2.api.') def test_02_encode_message(self): with open( os.path.abspath(os.path.join(os.path.dirname(__file__))) + '/msg_encode_assert') as file_data: data = file_data.read().encode() file_data.close() msg = self.protobuf_target._construct_message(data, query_pb2) msg_encoded = self.framework_utils.encode_message(msg) self.assertEqual(msg_encoded, msg_encoded) def test_03_decode_message(self): result = self.framework_utils.decode_message(msg_encoded, query_pb2, 'Request') msg = json_format.MessageToDict(result[0]) with open( os.path.join(os.path.dirname(__file__)) + '/msg_decoded_assert') as file_data: msg_decoded_assert = json.load(file_data) self.assertEqual(msg, msg_decoded_assert) def test_04_create_result_dir_if_not_exists(self): is_exits = False self.framework_utils.results_dir() if os.path.exists(os.getcwd() + '/results/'): is_exits = True self.assertTrue(is_exits, True) #shutil.rmtree(os.getcwd() + '/results/', ignore_errors=True) def test_05_rename_log_file(self): self._uuid = GenerateUUID.generate_uuid() self.framework_utils.rename_log_file() file_name = glob.glob(os.getcwd() + '/kittylogs/' + '*.log')[0].split('/')[-1:] expected_file_name = str( self._uuid) + '-kitty_' + time.strftime("%Y%m%d-%H%M%S") + '.log' shutil.rmtree(os.getcwd() + '/kittylogs/', ignore_errors=True) self.assertAlmostEqual(file_name[0], expected_file_name) def test_06_rename_log_file_file_not_found(self): shutil.rmtree(os.getcwd() + '/kittylogs/', ignore_errors=True) with self.assertRaises(FileNotFoundError): self.framework_utils.rename_log_file() def test_07_rename_log_file_could_not_read_from_file(self): os.makedirs(os.getcwd() + '/kittylogs/') with open(os.getcwd() + '/kittylogs/foo.gld', 'w') as file: file.write('dummy') file.close() with self.assertLogs('fuzzer', level='WARNING') as cm: self.framework_utils.rename_log_file() self.assertIn( 'WARNING:fuzzer:Could not read from file ' + os.getcwd() + '/kittylogs/', cm.output) shutil.rmtree(os.getcwd() + '/kittylogs/', ignore_errors=True) def test_08_archive_locally(self): dir_exists = False self.framework_utils.archive_locally() self._uuid = GenerateUUID.generate_uuid() dir_ = str(self._uuid) + '-log' if os.path.exists(os.getcwd() + '/' + dir_): dir_exists = True self.assertTrue(dir_exists, True) def test_09_extract_values(self): input_ = { "a": 1, "b": 2, "c": { "d": 3, "e": 4, "f": [{ "g": 5 }], "h": { "i": 6 } } } key_ = 'i' self.assertEqual(self.framework_utils.extract_values(input_, key_), [6]) def test_10_update_nested_object(self): context_ = [("a", 1), ("c", 2), ("h", 1), ("j", 1)] appnd_ = {"m": 9} key_ = "j" input_ = { "a": 1, "b": 2, "c": { "d": 3, "e": 4, "f": [{ "g": 5 }], "h": { "i": 6, "j": [{ "k": 7, "l": 8 }] } } } result = self.framework_utils.update_nested(input_, key_, appnd_, context_) expected = { 'a': 1, 'b': 2, 'c': { 'd': 3, 'e': 4, 'f': [{ 'g': 5 }], 'h': { 'i': 6, 'j': [{ 'k': 7, 'l': 8, 'm': 9 }] } } } self.assertEqual(result, expected) def test_11_update_nested_object_same_name(self): context_ = [("a", 1), ("c", 2), ("h", 1)] appnd_ = {"h": 9} key_ = "h" input_ = { "a": 1, "b": 2, "c": { "d": 3, "e": 4, "f": { "g": 5 }, "h": { "i": 6, "j": [{ "k": 7, "l": 8 }] } } } result = self.framework_utils.update_nested(input_, key_, appnd_, context_) expected = { 'a': 1, 'b': 2, 'c': { 'd': 3, 'e': 4, 'f': { 'g': 5 }, 'h': { 'i': 6, 'j': [{ 'k': 7, 'l': 8 }], 'h': 9 } } } self.assertEqual(result, expected) def test_12_update_nested_object_same_name_nested(self): context_ = [("a", 1), ("q", 2), ("q", 1)] appnd_ = {"q": 9} key_ = "q" input_ = { "a": 1, "b": 2, "q": { "q": { "x": 1 } }, "c": { "d": 3, "e": 4, "f": { "g": 5 }, "h": { "i": 6, "j": [{ "k": 7, "l": 8 }] } } } result = self.framework_utils.update_nested(input_, key_, appnd_, context_) expected = { 'a': 1, 'b': 2, 'q': { 'q': { 'x': 1, 'q': 9 } }, 'c': { 'd': 3, 'e': 4, 'f': { 'g': 5 }, 'h': { 'i': 6, 'j': [{ 'k': 7, 'l': 8 }] } } } self.assertEqual(result, expected) def test_13_field_randomizer_double(self): # field_randomizer(self, field_type, default_value=None, enum_list=None) is_close = False result = self.framework_utils.field_randomizer(1, default_value=None, enum_list=None) if 1.0 <= result <= 1.9: is_close = True self.assertTrue(is_close, True) def test_13_field_randomizer_double_default_value(self): # field_randomizer(self, field_type, default_value=None, enum_list=None) is_close = False result = self.framework_utils.field_randomizer( 1, default_value=1.12345679, enum_list=None) if 1.0 <= result <= 1.9: is_close = True self.assertTrue(is_close, True) def test_14_field_randomizer_float(self): # field_randomizer(self, field_type, default_value=None, enum_list=None) is_close = False result = self.framework_utils.field_randomizer(2, default_value=None, enum_list=None) if 1.0 <= result <= 1.9: is_close = True self.assertTrue(is_close, True) def test_15_field_randomizer_float_default(self): # field_randomizer(self, field_type, default_value=None, enum_list=None) is_close = False result = self.framework_utils.field_randomizer( 2, default_value=1.123456789, enum_list=None) if 1.0 <= result <= 1.9: is_close = True self.assertTrue(is_close, True) def test_14_field_randomizer_int64(self): # field_randomizer(self, field_type, default_value=None, enum_list=None) is_close = False result = self.framework_utils.field_randomizer(3, default_value=None, enum_list=None) if -9223372036854775808 <= result <= 9223372036854775808: is_close = True self.assertTrue(is_close, True) def test_15_field_randomizer_int64_default(self): # field_randomizer(self, field_type, default_value=None, enum_list=None) is_close = False result = self.framework_utils.field_randomizer( 3, default_value=1234567899999999, enum_list=None) if -9223372036854775808 <= result <= 9223372036854775808: is_close = True self.assertTrue(is_close, True) def test_16_field_randomizer_uint64(self): # field_randomizer(self, field_type, default_value=None, enum_list=None) is_close = False result = self.framework_utils.field_randomizer(4, default_value=None, enum_list=None) if -0 <= result <= 18446744073709551615: is_close = True self.assertTrue(is_close, True) def test_17_field_randomizer_uint64_default(self): # field_randomizer(self, field_type, default_value=None, enum_list=None) is_close = False result = self.framework_utils.field_randomizer( 4, default_value=1844674407370955161, enum_list=None) if -0 <= result <= 18446744073709551615: is_close = True self.assertTrue(is_close, True) def test_18_field_randomizer_int32(self): # field_randomizer(self, field_type, default_value=None, enum_list=None) is_close = False result = self.framework_utils.field_randomizer(5, default_value=None, enum_list=None) if -2147483648 <= result <= 2147483648: is_close = True self.assertTrue(is_close, True) def test_19_field_randomizer_int32_default(self): # field_randomizer(self, field_type, default_value=None, enum_list=None) is_close = False result = self.framework_utils.field_randomizer(5, default_value=1234567, enum_list=None) if -2147483648 <= result <= 2147483648: is_close = True self.assertTrue(is_close, True) def test_20_field_randomizer_fixed64(self): # field_randomizer(self, field_type, default_value=None, enum_list=None) is_close = False result = self.framework_utils.field_randomizer(6, default_value=None, enum_list=None) if -9223372036854775808 <= result <= 9223372036854775808: is_close = True self.assertTrue(is_close, True) def test_21_field_randomizer_fixed64_default(self): # field_randomizer(self, field_type, default_value=None, enum_list=None) is_close = False result = self.framework_utils.field_randomizer( 6, default_value=92233720368547758, enum_list=None) if -9223372036854775808 <= result <= 9223372036854775808: is_close = True self.assertTrue(is_close, True) def test_22_field_randomizer_fixed32(self): # field_randomizer(self, field_type, default_value=None, enum_list=None) is_close = False result = self.framework_utils.field_randomizer(7, default_value=None, enum_list=None) if -2147483648 <= result <= 2147483648: is_close = True self.assertTrue(is_close, True) def test_23_field_randomizer_fixed32_default(self): # field_randomizer(self, field_type, default_value=None, enum_list=None) is_close = False result = self.framework_utils.field_randomizer(7, default_value=214748364, enum_list=None) if -2147483648 <= result <= 2147483648: is_close = True self.assertTrue(is_close, True) def test_22_field_randomizer_bool(self): # field_randomizer(self, field_type, default_value=None, enum_list=None) is_close = False result = self.framework_utils.field_randomizer(8, default_value=None, enum_list=None) if result or not result: is_close = True self.assertTrue(is_close, True) def test_23_field_randomizer_bool_default(self): # field_randomizer(self, field_type, default_value=None, enum_list=None) is_close = False result = self.framework_utils.field_randomizer(8, default_value=True, enum_list=None) if result or not result: is_close = True self.assertTrue(is_close, True) def test_24_field_randomizer_string(self): # field_randomizer(self, field_type, default_value=None, enum_list=None) is_close = False regex = re.compile('[a-zA-Z0-9]', re.I) result = self.framework_utils.field_randomizer(9, default_value=None, enum_list=None) res = bool(regex.match(str(result))) if isinstance(result, str) and res: is_close = True self.assertTrue(is_close, True) def test_25_field_randomizer_string_default(self): # field_randomizer(self, field_type, default_value=None, enum_list=None) is_close = False regex = re.compile('[a-zA-Z0-9]', re.I) result = self.framework_utils.field_randomizer( 9, default_value='abcdefghijk', enum_list=None) res = bool(regex.match(str(result))) if isinstance(result, str) and res: is_close = True self.assertTrue(is_close, True) def test_26_field_randomizer_group(self): # field_randomizer(self, field_type, default_value=None, enum_list=None) with self.assertRaises(NotImplementedError): self.framework_utils.field_randomizer(10, default_value=None, enum_list=None) def test_27_field_randomizer_bytes(self): #TODO finish self.framework_utils.field_randomizer(12, default_value=None, enum_list=None) def test_28_field_randomizer_bytes_default(self): #TODO self.framework_utils.field_randomizer(12, default_value=0x23, enum_list=None) def test_29_field_randomizer_uint32(self): # field_randomizer(self, field_type, default_value=None, enum_list=None) is_close = False result = self.framework_utils.field_randomizer(13, default_value=None, enum_list=None) if 0 <= result <= 4294967295: is_close = True self.assertTrue(is_close, True) def test_30_field_randomizer_uint32_default(self): # field_randomizer(self, field_type, default_value=None, enum_list=None) is_close = False result = self.framework_utils.field_randomizer(13, default_value=429496729, enum_list=None) if 0 <= result <= 4294967295: is_close = True self.assertTrue(is_close, True) def test_31_field_randomizer_enum(self): # field_randomizer(self, field_type, default_value=None, enum_list=None) is_close = False enum_list = ['foo', 'bar', 'baz'] result = self.framework_utils.field_randomizer(14, default_value=None, enum_list=enum_list) if result.split('+')[0] in enum_list: is_close = True self.assertTrue(is_close, True) def test_32_field_randomizer_enum_default(self): # field_randomizer(self, field_type, default_value=None, enum_list=None) is_close = False enum_list = ['foo', 'bar', 'baz'] result = self.framework_utils.field_randomizer(14, default_value='bar', enum_list=enum_list) if result.split('+')[0] in enum_list: is_close = True self.assertTrue(is_close, True) def test_33_field_randomizer_sfixed32(self): # field_randomizer(self, field_type, default_value=None, enum_list=None) with self.assertRaises(NotImplementedError): self.framework_utils.field_randomizer(15, default_value=None, enum_list=None) def test_34_field_randomizer_sfixed64(self): # field_randomizer(self, field_type, default_value=None, enum_list=None) with self.assertRaises(NotImplementedError): self.framework_utils.field_randomizer(16, default_value=None, enum_list=None) def test_35_field_randomizer_sint32(self): # field_randomizer(self, field_type, default_value=None, enum_list=None) with self.assertRaises(NotImplementedError): self.framework_utils.field_randomizer(17, default_value=None, enum_list=None) def test_36_field_randomizer_sint64(self): # field_randomizer(self, field_type, default_value=None, enum_list=None) with self.assertRaises(NotImplementedError): self.framework_utils.field_randomizer(18, default_value=None, enum_list=None) def test_37_str_to_bool_true(self): result = self.framework_utils.str_to_bool('true') self.assertTrue(result, True) def test_38_str_to_bool_false(self): result = self.framework_utils.str_to_bool('false') self.assertFalse(result, False) def test_39_dir_files_to_archive(self): result = self.framework_utils.dir_files_to_archive() self.assertEqual(len(result), 1) def test_40_md5_checksum_for_proto_files(self): pass