def list_test_opts(): return [ cfg.FloatOpt("length_diff_percent", default=1000.0, help=_( "Percentage difference between initial request " "and test request body length to trigger a signal")), cfg.FloatOpt("time_diff_percent", default=1000.0, help=_( "Percentage difference between initial response " "time and test response time to trigger a signal")), cfg.IntOpt("max_time", default=10, help=_("Maximum absolute time (in seconds) to wait for a " "response before triggering a timeout signal")), cfg.IntOpt( "max_length", default=500, help=_("Maximum length (in characters) of the response text")), cfg.ListOpt("failure_keys", default="[`syntax error`]", help=_("Comma seperated list of keys for which the test " "would fail.")) ]
def test_case(self): self.init_signals.register(https_check(self)) if "HTTP_LINKS_PRESENT" in self.init_signals: self.register_issue( defect_type=_("SSL_ERROR"), severity=syntribos.MEDIUM, confidence=syntribos.HIGH, description=(_("Make sure that all the returned endpoint URIs" " use 'https://' and not 'http://'")))
def list_logger_opts(): # TODO(unrahul): Add log formating and verbosity options return [ cfg.BoolOpt("http_request_compression", default=True, help=_("Request content compression to compress fuzz " "strings present in the http request content.")), cfg.StrOpt("log_dir", default="", sample_default="~/.syntribos/logs", help=_("Where to save debug log files for a syntribos run")) ]
def replace_one_variable(cls, var_obj): """Evaluate a VariableObject according to its type A meta variable's type is optional. If a type is given, the parser will interpret the variable in one of 3 ways according to its type, and returns that value. * Type config: The parser will attempt to read the config value specified by the "val" attribute and returns that value. * Type function: The parser will call the function named in the "val" attribute with arguments given in the "args" attribute, and returns the value from calling the function. This value is cached, and will be returned on subsequent calls. * Type generator: works the same way as the function type, but its results are not cached and the function will be called every time. Otherwise, the parser will interpret the variable as a static variable, and will return whatever is in the "val" attribute. :param var_obj: A :class:`syntribos.clients.http.parser.VariableObject` :returns: The evaluated value according to its meta variable type """ if var_obj.var_type == 'config': try: return reduce(getattr, var_obj.val.split("."), CONF) except AttributeError: msg = _("Meta json file contains reference to the config " "option %s, which does not appear to" "exist.") % var_obj.val raise TemplateParseException(msg) elif var_obj.var_type == 'function': if var_obj.function_return_value: return var_obj.function_return_value if not var_obj.val: msg = _("The type of variable %s is function, but there is no " "reference to the function.") % var_obj.name raise TemplateParseException(msg) else: var_obj.function_return_value = cls.call_one_external_function( var_obj.val, var_obj.args) return var_obj.function_return_value elif var_obj.var_type == 'generator': if not var_obj.val: msg = _("The type of variable %s is generator, but there is no" " reference to the function.") % var_obj.name raise TemplateParseException(msg) return cls.call_one_external_function(var_obj.val, var_obj.args) else: return str(var_obj.val)
def list_logger_opts(): # TODO(unrahul): Add log formating and verbosity options return [ cfg.BoolOpt("http_request_compression", default=True, help=_( "Request content compression to compress fuzz " "strings present in the http request content.")), cfg.StrOpt("log_dir", default="", sample_default="~/.syntribos/logs", help=_( "Where to save debug log files for a syntribos run" )) ]
def call_one_external_function(cls, string, args): """Calls one function read in from templates and returns the result.""" if not isinstance(string, six.string_types): return string match = re.search(cls.FUNC_NO_ARGS, string) func_string_has_args = False if not match: match = re.search(cls.FUNC_WITH_ARGS, string) func_string_has_args = True if match: try: dot_path = match.group(1) func_name = match.group(2) mod = importlib.import_module(dot_path) func = getattr(mod, func_name) if func_string_has_args and not args: arg_list = match.group(3) args = json.loads(arg_list) val = func(*args) except Exception: msg = _("The reference to the function %s failed to parse " "correctly, please check the documentation to ensure " "your function import string adheres to the proper " "format") % string raise TemplateParseException(msg) else: try: func_lst = string.split(":") if len(func_lst) == 2: args = func_lst[1] func_str = func_lst[0] dot_path = ".".join(func_str.split(".")[:-1]) func_name = func_str.split(".")[-1] mod = importlib.import_module(dot_path) func = getattr(mod, func_name) val = func(*args) except Exception: msg = _("The reference to the function %s failed to parse " "correctly, please check the documentation to ensure " "your function import string adheres to the proper " "format") % string raise TemplateParseException(msg) if isinstance(val, types.GeneratorType): return str(six.next(val)) else: return str(val)
def list_cli_opts(): return [ cfg.SubCommandOpt(name="sub_command", handler=sub_commands, help=_("Available commands"), title="syntribos Commands"), cfg.MultiStrOpt("test-types", dest="test_types", short="t", default=[""], sample_default=["SQL", "XSS"], help=_( "Test types to run against the target API")), cfg.MultiStrOpt("excluded-types", dest="excluded_types", short="e", default=[""], sample_default=["SQL", "XSS"], help=_("Test types to be excluded from " "current run against the target API")), cfg.BoolOpt("colorize", dest="colorize", short="cl", default=False, help=_("Enable color in syntribos terminal output")), cfg.StrOpt("outfile", short="o", sample_default="out.json", help=_("File to print " "output to")), cfg.StrOpt("format", dest="output_format", short="f", default="json", choices=["json"], ignore_case=True, help=_("The format for outputting results")), cfg.StrOpt("min-severity", dest="min_severity", short="S", default="LOW", choices=syntribos.RANKING, help=_("Select a minimum severity for reported " "defects")), cfg.StrOpt("min-confidence", dest="min_confidence", short="C", default="LOW", choices=syntribos.RANKING, help=_("Select a minimum confidence for reported " "defects")) ]
def safe_makedirs(path, force=False): if not os.path.exists(path): try: os.makedirs(path) except (OSError, IOError): LOG.exception(_("Error creating folder (%s).") % path) elif os.path.exists(path) and force: try: shutil.rmtree(path) os.makedirs(path) except (OSError, IOError): LOG.exception(_("Error overwriting existing folder (%s).") % path) else: LOG.warning(_LW("Folder was already found (%s). Skipping.") % path)
def sub_commands(sub_parser): init_parser = sub_parser.add_parser( "init", help=_("Initialize syntribos environment after " "installation. Should be run before any other " "commands.")) init_parser.add_argument( "--force", dest="force", action="store_true", help=_( "Skip prompts for configurable options, force initialization " "even if syntribos believes it has already been initialized. If " "--custom_install_root isn't specified, we will use the default " "options. WARNING: This is potentially destructive! Use with " "caution.")) init_parser.add_argument( "--custom_install_root", dest="custom_install_root", help=_("Skip prompts for configurable options, and initialize " "syntribos in the specified directory. Can be combined " "with --force to overwrite existing files.")) init_parser.add_argument( "--no_downloads", dest="no_downloads", action="store_true", help=_("Disable the downloading of payload files as part of the " "initialization process")) download_parser = sub_parser.add_parser( "download", help=_("Download payload and template files. This command is " "configurable according to the remote section of your " "config file")) download_parser.add_argument("--templates", dest="templates", action="store_true", help=_("Download templates")) download_parser.add_argument("--payloads", dest="payloads", action="store_true", help=_("Download payloads")) sub_parser.add_parser("list_tests", help=_("List all available tests")) sub_parser.add_parser("run", help=_("Run syntribos with given config" "options")) sub_parser.add_parser("dry_run", help=_("Dry run syntribos with given config" "options"))
def list_tests(cls): """Print out the list of available tests types that can be run.""" print(_("List of available tests...:\n")) print("{:<50}{}\n".format(_("[Test Name]"), _("[Description]"))) testdict = {name: clss.__doc__ for name, clss in cls.get_tests()} for test in sorted(testdict): if testdict[test] is None: raise Exception( _("No test description provided" " as doc string for the test: %s") % test) else: test_description = testdict[test].split(".")[0] print("{test:<50}{desc}\r".format(test=test, desc=test_description)) print("\n")
def test_case(self): self.run_default_checks() self.test_signals.register(has_string(self)) if "FAILURE_KEYS_PRESENT" in self.test_signals: failed_strings = self.test_signals.find( slugs="FAILURE_KEYS_PRESENT")[0].data["failed_strings"] self.register_issue( defect_type="command_injection", severity=syntribos.HIGH, confidence=syntribos.MEDIUM, description=("A string known to be commonly returned after a " "successful command injection attack was " "included in the response. This could indicate " "a vulnerability to command injection " "attacks.").format(failed_strings)) self.diff_signals.register(time_diff(self)) if "TIME_DIFF_OVER" in self.diff_signals: self.register_issue( defect_type="command_injection", severity=syntribos.HIGH, confidence=syntribos.MEDIUM, description=(_("The time elapsed between the sending of " "the request and the arrival of the res" "ponse exceeds the expected amount of time, " "suggesting a vulnerability to command " "injection attacks.")))
def test_case(self): self.run_default_checks() self.test_signals.register(has_string(self)) if "FAILURE_KEYS_PRESENT" in self.test_signals: failed_strings = self.test_signals.find( slugs="FAILURE_KEYS_PRESENT")[0].data["failed_strings"] self.register_issue( defect_type="sql_strings", severity=syntribos.MEDIUM, confidence=syntribos.LOW, description=("The string(s): '{0}', known to be commonly " "returned after a successful SQL injection attack" ", have been found in the response. This could " "indicate a vulnerability to SQL injection " "attacks.").format(failed_strings)) self.diff_signals.register(time_diff(self)) if "TIME_DIFF_OVER" in self.diff_signals: self.register_issue( defect_type="sql_timing", severity=syntribos.MEDIUM, confidence=syntribos.LOW, description=(_("A response to one of our payload requests has " "taken too long compared to the baseline " "request. This could indicate a vulnerability " "to time-based SQL injection attacks")))
def _parse_url_line(cls, line, endpoint): """Split first line of an HTTP request into its components :param str line: the first line of the HTTP request :param str endpoint: the full URL of the endpoint to test :rtype: tuple :returns: HTTP method, URL, request parameters, HTTP version """ valid_methods = [ "GET", "POST", "HEAD", "OPTIONS", "PUT", "DELETE", "TRACE", "CONNECT", "PATCH" ] params = {} method, url, version = line.split() url = url.split("?", 1) if len(url) == 2: for param in url[1].split("&"): param = param.split("=", 1) if len(param) > 1: params[param[0]] = param[1] else: params[param[0]] = "" url = url[0] url = urlparse.urljoin(endpoint, url) if method not in valid_methods: raise ValueError(_("Invalid HTTP method: %s") % method) return (method, cls._replace_str_variables(url), cls._replace_dict_variables(params), version)
def handle_config_exception(exc): msg = "" if not any(LOG.handlers): logging.basicConfig(level=logging.DEBUG) if isinstance(exc, cfg.RequiredOptError): msg = "Missing option '{opt}'".format(opt=exc.opt_name) if exc.group: msg += " in group '{}'".format(exc.group) CONF.print_help() elif isinstance(exc, cfg.ConfigFilesNotFoundError): if CONF._args[0] == "init": return msg = (_("Configuration file specified ('%s') wasn't " "found or was unreadable.") % ",".join( CONF.config_file)) if msg: LOG.warning(msg) print(syntribos.SEP) sys.exit(0) else: LOG.exception(exc)
def _parse_data(cls, lines): """Parse the body of the HTTP request (e.g. POST variables) :param list lines: lines of the HTTP body :returns: object representation of body data (JSON or XML) """ postdat_regex = r"([\w%]+=[\w%]+&?)+" data = "\n".join(lines).strip() if not data: return "" try: data = json.loads(data) # TODO(cneill): Make this less hacky if isinstance(data, list): data = json.dumps(data) if isinstance(data, dict): return cls._replace_dict_variables(data) else: return cls._replace_str_variables(data) except TemplateParseException: raise except (TypeError, ValueError): try: data = ElementTree.fromstring(data) except Exception: if not re.match(postdat_regex, data): raise TypeError(_("Unknown data format")) except Exception: raise return data
def test_case(self): self.run_default_checks() self.test_signals.register(has_string(self)) if "FAILURE_KEYS_PRESENT" in self.test_signals: failed_strings = self.test_signals.find( slugs="FAILURE_KEYS_PRESENT")[0].data["failed_strings"] self.register_issue( defect_type="json_depth_limit_strings", severity=syntribos.MEDIUM, confidence=syntribos.HIGH, description=( "The string(s): '{0}', is known to be commonly " "returned after a successful overflow of the json" " parsers depth limit. This could possibly " "result in a dos vulnerability.").format(failed_strings)) self.diff_signals.register(time_diff(self)) if "TIME_DIFF_OVER" in self.diff_signals: self.register_issue( defect_type="json_depth_timing", severity=syntribos.MEDIUM, confidence=syntribos.LOW, description=(_("The time it took to resolve a request " "was too long compared to the " "baseline request. This could indicate a " "vulnerability to denial of service attacks.")))
def __init__(self, name, var_type="", args=[], val="", fuzz=True, fuzz_types=[], min_length=0, max_length=sys.maxsize, url_encode=False, **kwargs): if var_type and var_type.lower() not in self.VAR_TYPES: msg = _("The meta variable %(name)s has a type of %(var)s which " "syntribos does not" "recognize") % { 'name': name, 'var': var_type } raise TemplateParseException(msg) self.name = name self.var_type = var_type.lower() self.val = val self.args = args self.fuzz_types = fuzz_types self.fuzz = fuzz self.min_length = min_length self.max_length = max_length self.url_encode = url_encode self.function_return_value = None
def test_case(self): self.run_default_checks() self.test_signals.register(has_string(self)) if "FAILURE_KEYS_PRESENT" in self.test_signals: failed_strings = self.test_signals.find( slugs="FAILURE_KEYS_PRESENT")[0].data["failed_strings"] self.register_issue( defect_type="bof_strings", severity=syntribos.MEDIUM, confidence=syntribos.MEDIUM, description=("The string(s): '{0}', known to be commonly " "returned after a successful buffer overflow " "attack, have been found in the response. This " "could indicate a vulnerability to buffer " "overflow attacks.").format(failed_strings)) self.diff_signals.register(time_diff(self)) if "TIME_DIFF_OVER" in self.diff_signals: self.register_issue( defect_type="bof_timing", severity=syntribos.MEDIUM, confidence=syntribos.LOW, description=(_("The time it took to resolve a request with a " "long string was too long compared to the " "baseline request. This could indicate a " "vulnerability to buffer overflow attacks")))
def test_case(self): self.run_default_checks() self.test_signals.register(has_string(self)) if "FAILURE_KEYS_PRESENT" in self.test_signals: failed_strings = self.test_signals.find( slugs="FAILURE_KEYS_PRESENT")[0].data["failed_strings"] self.register_issue( defect_type="user_defined_strings", severity=syntribos.MEDIUM, confidence=syntribos.LOW, description=("The string(s): '{0}', is in the list of " "possible vulnerable keys. This may " "indicate a vulnerability to this form of " "user defined attack.").format(failed_strings)) self.diff_signals.register(time_diff(self)) if "TIME_DIFF_OVER" in self.diff_signals: self.register_issue( defect_type="user_defined_string_timing", severity=syntribos.MEDIUM, confidence=syntribos.LOW, description=(_("A response to one of the payload requests has " "taken too long compared to the baseline " "request. This could indicate a vulnerability " "to time-based injection attacks using the user" " provided strings.")))
def _replace_dict_variables(cls, dic): """Recursively evaluates all meta variables in a given dict.""" for (key, value) in dic.items(): # Keys dont get fuzzed, so can handle them here match = re.search(cls.METAVAR, key) if match: replaced_key = match.group(0).strip("|") key_obj = cls._create_var_obj(replaced_key) replaced_key = cls.replace_one_variable(key_obj) new_key = re.sub(cls.METAVAR, replaced_key, key) del dic[key] dic[new_key] = value # Vals are fuzzed so they need to be passed to datagen as an object if isinstance(value, six.string_types): match = re.search(cls.METAVAR, value) if match: var_str = match.group(0).strip("|") if var_str != value.strip("|%s" % string.whitespace): msg = _("Meta-variable references cannot come in the " "middle of the value %s") % value raise TemplateParseException(msg) val_obj = cls._create_var_obj(var_str) if key in dic: dic[key] = val_obj elif new_key in dic: dic[new_key] = val_obj elif isinstance(value, dict): cls._replace_dict_variables(value) return dic
def get(uri, cache_dir=None): """Entry method for download method :param str uri: A formatted remote URL of a file :param str: Absolute path to the downloaded content :param str cache_dir: path to save downloaded files """ user_base_dir = cache_dir or CONF.remote.cache_dir if user_base_dir: try: temp = tempfile.TemporaryFile(dir=os.path.abspath(user_base_dir)) temp.close() except OSError: LOG.error("Failed to write remote files to: %s", os.path.abspath(user_base_dir)) exit(1) abs_path = download(uri, os.path.abspath(user_base_dir)) else: abs_path = download(uri) try: return extract_tar(abs_path) except (tarfile.TarError, Exception): msg = _("Not a gz file, returning abs_path") LOG.debug(msg) return abs_path
def test_case(self): self.run_default_checks() self.test_signals.register(has_string(self)) if "FAILURE_KEYS_PRESENT" in self.test_signals: failed_strings = self.test_signals.find( slugs="FAILURE_KEYS_PRESENT")[0].data["failed_strings"] self.register_issue( defect_type="user_defined_strings", severity=syntribos.MEDIUM, confidence=syntribos.LOW, description=("The string(s): '{0}', is in the list of " "possible vulnerable keys. This may " "indicate a vulnerability to this form of " "user defined attack.").format(failed_strings)) self.diff_signals.register(time_diff(self)) if "TIME_DIFF_OVER" in self.diff_signals: self.register_issue( defect_type="user_defined_string_timing", severity=syntribos.MEDIUM, confidence=syntribos.MEDIUM, description=(_("A response to one of the payload requests has " "taken too long compared to the baseline " "request. This could indicate a vulnerability " "to time-based injection attacks using the user" " provided strings.")))
def handle_config_exception(exc): msg = "" if not any(LOG.handlers): logging.basicConfig(level=logging.DEBUG) if isinstance(exc, cfg.RequiredOptError): msg = "Missing option '{opt}'".format(opt=exc.opt_name) if exc.group: msg += " in group '{}'".format(exc.group) CONF.print_help() elif isinstance(exc, cfg.ConfigFilesNotFoundError): if CONF._args[0] == "init": return msg = (_("Configuration file specified ('%s') wasn't " "found or was unreadable.") % ",".join(CONF.config_file)) if msg: LOG.warning(msg) print(syntribos.SEP) sys.exit(0) else: LOG.exception(exc)
def test_case(self): self.run_default_checks() self.test_signals.register(has_string(self)) if "FAILURE_KEYS_PRESENT" in self.test_signals: failed_strings = self.test_signals.find( slugs="FAILURE_KEYS_PRESENT")[0].data["failed_strings"] self.register_issue( defect_type="sql_strings", severity=syntribos.MEDIUM, confidence=syntribos.LOW, description=("The string(s): '{0}', known to be commonly " "returned after a successful SQL injection attack" ", have been found in the response. This could " "indicate a vulnerability to SQL injection " "attacks.").format(failed_strings)) self.diff_signals.register(time_diff(self)) if "TIME_DIFF_OVER" in self.diff_signals: self.register_issue( defect_type="sql_timing", severity=syntribos.MEDIUM, confidence=syntribos.MEDIUM, description=(_("A response to one of our payload requests has " "taken too long compared to the baseline " "request. This could indicate a vulnerability " "to time-based SQL injection attacks")))
def test_case(self): self.run_default_checks() self.test_signals.register(has_string(self)) if "FAILURE_KEYS_PRESENT" in self.test_signals: failed_strings = self.test_signals.find( slugs="FAILURE_KEYS_PRESENT")[0].data["failed_strings"] self.register_issue( defect_type="bof_strings", severity=syntribos.MEDIUM, confidence=syntribos.LOW, description=("The string(s): '{0}', known to be commonly " "returned after a successful buffer overflow " "attack, have been found in the response. This " "could indicate a vulnerability to buffer " "overflow attacks.").format(failed_strings)) self.diff_signals.register(time_diff(self)) if "TIME_DIFF_OVER" in self.diff_signals: self.register_issue( defect_type="bof_timing", severity=syntribos.MEDIUM, confidence=syntribos.MEDIUM, description=(_("The time it took to resolve a request with a " "long string was too long compared to the " "baseline request. This could indicate a " "vulnerability to buffer overflow attacks")))
def list_tests(cls): """Print out the list of available tests types that can be run.""" print(_("List of available tests...:\n")) print("{:<50}{}\n".format(_("[Test Name]"), _("[Description]"))) testdict = {name: clss.__doc__ for name, clss in cls.get_tests()} for test in sorted(testdict): if testdict[test] is None: raise Exception( _("No test description provided" " as doc string for the test: %s") % test) else: test_description = testdict[test].split(".")[0] print("{test:<50}{desc}\r".format( test=test, desc=test_description)) print("\n")
def safe_makedirs(path, force=False): if not os.path.exists(path): try: os.makedirs(path) except (OSError, IOError): LOG.exception(_("Error creating folder (%s).") % path) elif os.path.exists(path) and force: try: shutil.rmtree(path) os.makedirs(path) except (OSError, IOError): LOG.exception( _("Error overwriting existing folder (%s).") % path) else: LOG.warning( _LW("Folder was already found (%s). Skipping.") % path)
def get(uri, cache_dir=None): """Entry method for download method :param str uri: A formatted remote URL of a file :param str: Absolute path to the downloaded content :param str cache_dir: path to save downloaded files """ user_base_dir = cache_dir or CONF.remote.cache_dir if user_base_dir: try: temp = tempfile.TemporaryFile(dir=os.path.abspath(user_base_dir)) temp.close() except OSError: LOG.error(_LE("Failed to write remote files to: %s") % os.path.abspath(user_base_dir)) exit(1) abs_path = download(uri, os.path.abspath(user_base_dir)) else: abs_path = download(uri) try: return extract_tar(abs_path) except (tarfile.TarError, Exception): msg = _("Not a gz file, returning abs_path") LOG.debug(msg) return abs_path
def _parse_url_line(cls, line, endpoint): """Split first line of an HTTP request into its components :param str line: the first line of the HTTP request :param str endpoint: the full URL of the endpoint to test :rtype: tuple :returns: HTTP method, URL, request parameters, HTTP version """ valid_methods = ["GET", "POST", "HEAD", "OPTIONS", "PUT", "DELETE", "TRACE", "CONNECT", "PATCH"] params = {} method, url, version = line.split() url = url.split("?", 1) if len(url) == 2: for param in url[1].split("&"): param = param.split("=", 1) if len(param) > 1: params[param[0]] = param[1] else: params[param[0]] = "" url = url[0] url = urlparse.urljoin(endpoint, url) if method not in valid_methods: raise ValueError(_("Invalid HTTP method: %s") % method) return (method, cls._replace_str_variables(url), cls._replace_dict_variables(params), version)
def _parse_data(cls, lines, content_type=""): """Parse the body of the HTTP request (e.g. POST variables) :param list lines: lines of the HTTP body :param content_type: Content-type header in template if any :returns: object representation of body data (JSON or XML) """ postdat_regex = r"([\w%]+=[\w%]+&?)+" data = "\n".join(lines).strip() data_type = "text" if not data: return '', None try: data = json.loads(data) # TODO(cneill): Make this less hacky if isinstance(data, list): data = json.dumps(data) if isinstance(data, dict): return cls._replace_dict_variables(data), 'json' else: return cls._replace_str_variables(data), 'str' except TemplateParseException: raise except (TypeError, ValueError): if 'json' in content_type: msg = ("The Content-Type header in this template is %s but " "syntribos cannot parse the request body as json" % content_type) raise TemplateParseException(msg) try: data = ElementTree.fromstring(data) data_type = 'xml' except Exception: if 'xml' in content_type: msg = ( "The Content-Type header in this template is %s " "but syntribos cannot parse the request body as xml" % content_type) raise TemplateParseException(msg) try: data = yaml.safe_load(data) data_type = 'yaml' except yaml.YAMLError: if 'yaml' in content_type: msg = ("The Content-Type header in this template is %s" "but syntribos cannot parse the request body as" "yaml" % content_type) raise TemplateParseException(msg) if not re.match(postdat_regex, data): raise TypeError( _("Make sure that your request body is" "valid JSON, XML, or YAML data - be " "sure to check for typos.")) except Exception: raise return data, data_type
def list_syntribos_opts(): def wrap_try_except(func): def wrap(*args): try: func(*args) except IOError: msg = _( "\nCan't open a file or directory specified in the " "config file under the section `[syntribos]`; verify " "if the path exists.\nFor more information please refer " "the debug logs.") print(msg) exit(1) return wrap return [ cfg.StrOpt("endpoint", default="", sample_default="http://localhost/app", help=_("The target host to be tested")), cfg.Opt("templates", type=ContentType("r", 0), default="", sample_default="~/.syntribos/templates", help=_("A directory of template files, or a single " "template file, to test on the target API")), cfg.StrOpt("payloads", default="", sample_default="~/.syntribos/data", help=_("The location where we can find syntribos'" "payloads")), cfg.MultiStrOpt("exclude_results", default=[""], sample_default=["500_errors", "length_diff"], help=_("Defect types to exclude from the " "results output")), cfg.Opt("custom_root", type=wrap_try_except(ExistingDirType()), short="c", sample_default="/your/custom/root", help=_("The root directory where the subfolders that make up" " syntribos' environment (logs, templates, payloads, " "configuration files, etc.)")), ]
def sub_commands(sub_parser): init_parser = sub_parser.add_parser( "init", help=_("Initialize syntribos environment after " "installation. Should be run before any other " "commands.")) init_parser.add_argument( "--force", dest="force", action="store_true", help=_( "Skip prompts for configurable options, force initialization " "even if syntribos believes it has already been initialized. If " "--custom_install_root isn't specified, we will use the default " "options. WARNING: This is potentially destructive! Use with " "caution.")) init_parser.add_argument( "--custom_install_root", dest="custom_install_root", help=_("Skip prompts for configurable options, and initialize " "syntribos in the specified directory. Can be combined " "with --force to overwrite existing files.")) init_parser.add_argument( "--no_downloads", dest="no_downloads", action="store_true", help=_("Disable the downloading of payload files as part of the " "initialization process")) download_parser = sub_parser.add_parser( "download", help=_( "Download payload and template files. This command is " "configurable according to the remote section of your " "config file")) download_parser.add_argument( "--templates", dest="templates", action="store_true", help=_("Download templates")) download_parser.add_argument( "--payloads", dest="payloads", action="store_true", help=_("Download payloads")) sub_parser.add_parser("list_tests", help=_("List all available tests")) sub_parser.add_parser("run", help=_("Run syntribos with given config" "options")) sub_parser.add_parser("dry_run", help=_("Dry run syntribos with given config" "options"))
def _parse_data(cls, lines, content_type=""): """Parse the body of the HTTP request (e.g. POST variables) :param list lines: lines of the HTTP body :param content_type: Content-type header in template if any :returns: object representation of body data (JSON or XML) """ postdat_regex = r"([\w%]+=[\w%]+&?)+" data = "\n".join(lines).strip() data_type = "text" if not data: return '', None try: data = json.loads(data) # TODO(cneill): Make this less hacky if isinstance(data, list): data = json.dumps(data) if isinstance(data, dict): return cls._replace_dict_variables(data), 'json' else: return cls._replace_str_variables(data), 'str' except TemplateParseException: raise except (TypeError, ValueError): if 'json' in content_type: msg = ("The Content-Type header in this template is %s but " "syntribos cannot parse the request body as json" % content_type) raise TemplateParseException(msg) try: data = ElementTree.fromstring(data) data_type = 'xml' except Exception: if 'xml' in content_type: msg = ("The Content-Type header in this template is %s " "but syntribos cannot parse the request body as xml" % content_type) raise TemplateParseException(msg) try: data = yaml.safe_load(data) data_type = 'yaml' except yaml.YAMLError: if 'yaml' in content_type: msg = ("The Content-Type header in this template is %s" "but syntribos cannot parse the request body as" "yaml" % content_type) raise TemplateParseException(msg) if not re.match(postdat_regex, data): raise TypeError(_("Make sure that your request body is" "valid JSON, XML, or YAML data - be " "sure to check for typos.")) except Exception: raise return data, data_type
def dry_run_report(cls, output): """Reports the dry run through a formatter.""" formatter_types = {"json": JSONFormatter(result)} formatter = formatter_types[CONF.output_format] formatter.report(output) test_log = cls.log_path print(syntribos.SEP) print(_("LOG PATH...: {path}").format(path=test_log)) print(syntribos.SEP)
def list_cli_opts(): return [ cfg.SubCommandOpt(name="sub_command", handler=sub_commands, help=_("Available commands"), title="syntribos Commands"), cfg.MultiStrOpt("test-types", dest="test_types", short="t", default=[""], sample_default=["SQL", "XSS"], help=_( "Test types to run against the target API")), cfg.MultiStrOpt("excluded-types", dest="excluded_types", short="e", default=[""], sample_default=["SQL", "XSS"], help=_("Test types to be excluded from " "current run against the target API")), cfg.BoolOpt("colorize", dest="colorize", short="cl", default=True, help=_("Enable color in syntribos terminal output")), cfg.StrOpt("outfile", short="o", sample_default="out.json", help=_("File to print " "output to")), cfg.StrOpt("format", dest="output_format", short="f", default="json", choices=["json"], ignore_case=True, help=_("The format for outputting results")), cfg.StrOpt("min-severity", dest="min_severity", short="S", default="LOW", choices=syntribos.RANKING, help=_("Select a minimum severity for reported " "defects")), cfg.StrOpt("min-confidence", dest="min_confidence", short="C", default="LOW", choices=syntribos.RANKING, help=_("Select a minimum confidence for reported " "defects")), cfg.BoolOpt("stacktrace", dest="stacktrace", default=True, help=_("Select if Syntribos outputs a stacktrace " " if an exception is raised")), cfg.StrOpt( "custom_root", dest="custom_root", help=_("Filesystem location for syntribos root directory, " "containing logs, templates, payloads, config files. " "Creates directories and skips interactive prompts when " "used with 'syntribos init'"), deprecated_group="init", deprecated_name="custom_install_root") ]
def list_syntribos_opts(): def wrap_try_except(func): def wrap(*args): try: func(*args) except IOError: msg = _( "\nCan't open a file or directory specified in the " "config file under the section `[syntribos]`; verify " "if the path exists.\nFor more information please refer " "the debug logs.") print(msg) exit(1) return wrap return [ cfg.StrOpt("endpoint", default="", sample_default="http://localhost/app", help=_("The target host to be tested")), cfg.Opt("templates", type=ContentType("r", 0), default="", sample_default="~/.syntribos/templates", help=_("A directory of template files, or a single " "template file, to test on the target API")), cfg.StrOpt("payloads", default="", sample_default="~/.syntribos/data", help=_( "The location where we can find syntribos'" "payloads")), cfg.MultiStrOpt("exclude_results", default=[""], sample_default=["500_errors", "length_diff"], help=_( "Defect types to exclude from the " "results output")), cfg.Opt("custom_root", type=wrap_try_except(ExistingDirType()), short="c", sample_default="/your/custom/root", help=_( "The root directory where the subfolders that make up" " syntribos' environment (logs, templates, payloads, " "configuration files, etc.)")), ]
def test_case(self): self.diff_signals.register(time_diff(self)) if "TIME_DIFF_OVER" in self.diff_signals: self.register_issue( defect_type="int_timing", severity=syntribos.MEDIUM, confidence=syntribos.MEDIUM, description=(_("The time it took to resolve a request with an " "invalid integer was too long compared to the " "baseline request. This could indicate a " "vulnerability to buffer overflow attacks")))
def wrap(*args): try: func(*args) except IOError: msg = _( "\nCan't open a file or directory specified in the " "config file under the section `[syntribos]`; verify " "if the path exists.\nFor more information please refer " "the debug logs.") print(msg) exit(1)
def list_remote_opts(): """Method defining remote URIs for payloads and templates.""" return [ cfg.StrOpt("cache_dir", default="", help=_("Base directory where cached files can be saved")), cfg.StrOpt("payloads_uri", default=("https://github.com/openstack/syntribos-payloads/" "archive/master.tar.gz"), help=_("Remote URI to download payloads.")), cfg.StrOpt( "templates_uri", default=("https://github.com/openstack/" "syntribos-openstack-templates/archive/master.tar.gz"), help=_("Remote URI to download templates.")), cfg.BoolOpt( "enable_cache", default=True, help=_("Cache remote template & payload resources locally")), ]
def list_remote_opts(): """Method defining remote URIs for payloads and templates.""" return [ cfg.StrOpt( "cache_dir", default="", help=_("Base directory where cached files can be saved")), cfg.StrOpt( "payloads_uri", default=("https://github.com/openstack/syntribos-payloads/" "archive/master.tar.gz"), help=_("Remote URI to download payloads.")), cfg.StrOpt( "templates_uri", default=("https://github.com/openstack/" "syntribos-openstack-templates/archive/master.tar.gz"), help=_("Remote URI to download templates.")), cfg.BoolOpt("enable_cache", default=True, help=_( "Cache remote template & payload resources locally")), ]
def list_test_opts(): return [ cfg.FloatOpt("length_diff_percent", default=1000.0, help=_( "Percentage difference between initial request " "and test request body length to trigger a signal")), cfg.FloatOpt("time_diff_percent", default=1000.0, help=_( "Percentage difference between initial response " "time and test response time to trigger a signal")), cfg.IntOpt("max_time", default=10, help=_( "Maximum absolute time (in seconds) to wait for a " "response before triggering a timeout signal")), cfg.IntOpt("max_length", default=500, help=_( "Maximum length (in characters) of the response text")), cfg.ListOpt("failure_keys", default="[`syntax error`]", help=_( "Comma seperated list of keys for which the test " "would fail.")) ]
def from_generic_exception(exception): """Return a SynSignal from a generic Exception :param exception: A generic Exception that can't be identified :type exception: Exception :rtype: :class:`SynSignal` :returns: A signal describing the exception """ if not isinstance(exception, Exception): raise Exception(_("This function accepts only Exception objects")) exc_text = str(exception) text = _("This request raised an exception: '%s'") % exc_text data = { _("exception_name"): exception.__class__.__name__, _("exception_text"): exc_text, _("exception"): exception } slug = "GENERIC_EXCEPTION_{name}".format( name=data["exception_name"].upper()) tags = ["EXCEPTION_RAISED"] return SynSignal(text=text, slug=slug, strength=1.0, tags=tags, data=data)
def test_case(self): self.test_signals.register(xst(self)) xst_slugs = [ slugs for slugs in self.test_signals.all_slugs if "HEADER_XST" in slugs] for i in xst_slugs: # noqa test_severity = syntribos.LOW self.register_issue( defect_type="XST_HEADER", severity=test_severity, confidence=syntribos.HIGH, description=(_("XST vulnerability found.\n" "Make sure that response to a " "TRACE request is filtered.")))
def _create_var_obj(cls, var): """Given the name of a variable, creates VariableObject :param str var: name of the variable in meta.json :rtype: :class:`syntribos.clients.http.parser.VariableObject` :returns: VariableObject holding the attributes defined in the JSON object read in from meta.json """ if var not in cls.meta_vars: msg = _("Expected to find %s in meta.json, but didn't. " "Check your templates") % var raise TemplateParseException(msg) var_dict = cls.meta_vars[var] if "type" in var_dict: var_dict["var_type"] = var_dict.pop("type") var_obj = VariableObject(var, **var_dict) return var_obj
def test_case(self): self.test_signals.register(cors(self)) cors_slugs = [ slugs for slugs in self.test_signals.all_slugs if "HEADER_CORS" in slugs] for slug in cors_slugs: if "ORIGIN" in slug: test_severity = syntribos.HIGH else: test_severity = syntribos.MEDIUM self.register_issue( defect_type="CORS_HEADER", severity=test_severity, confidence=syntribos.HIGH, description=( _("CORS header vulnerability found.\n" "Make sure that the header is not assigned " "a wildcard character.")))