From 1c13941291b92bd245a8290a11d3c531ae57ab4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?An=C5=BEe=20=C5=BDitnik?= <anze.zitnik@xlab.si> Date: Tue, 28 Apr 2020 14:44:32 +0200 Subject: [PATCH] Adding support for w3af authenticated scans. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squashed commit of the following: commit 7d99f5d2c82d84e144b8f099563367a498207251 Author: Anže Žitnik <anze.zitnik@xlab.si> Date: Tue Apr 28 14:44:05 2020 +0200 Updated extended_generic w3af profile to work with newer w3af. Added some config examples. commit 644923a660f031dcfcf3947f4b744c70a1c2d467 Author: Anže Žitnik <anze.zitnik@xlab.si> Date: Tue Apr 28 11:35:20 2020 +0200 Added auth_scan profile and changed configure.py to support its config. Imported extended_generic auth plugin from old version. --- Dockerfile | 2 +- MANIFEST | 2 +- config-examples/auth-config-dvwa.json | 19 ++ config-examples/auth-config.json | 22 ++ .../basic-config.json | 0 configure.py | 8 + install/profiles/auth_scan.template | 140 +++++++++++ install/profiles/extended_generic.py | 232 ++++++++++++++++++ install/w3af.sh | 4 + 9 files changed, 427 insertions(+), 2 deletions(-) create mode 100644 config-examples/auth-config-dvwa.json create mode 100644 config-examples/auth-config.json rename basic-config.json => config-examples/basic-config.json (100%) create mode 100644 install/profiles/auth_scan.template create mode 100644 install/profiles/extended_generic.py diff --git a/Dockerfile b/Dockerfile index 1b0e69b..19a3082 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM ubuntu:18.04 COPY install/base.sh /tmp/install/ RUN chmod +x /tmp/install/base.sh && /tmp/install/base.sh -COPY install/requirements.txt install/w3af_output_fix.patch install/w3af-lz4.patch install/w3af-scans.py.patch /tmp/ +COPY install/requirements.txt install/w3af_output_fix.patch install/w3af-lz4.patch install/w3af-scans.py.patch install/profiles/extended_generic.py install/profiles/auth_scan.template /tmp/ COPY install/w3af.sh /tmp/install/ RUN chmod +x /tmp/install/w3af.sh && /tmp/install/w3af.sh diff --git a/MANIFEST b/MANIFEST index b445243..dbab44d 100644 --- a/MANIFEST +++ b/MANIFEST @@ -1,3 +1,3 @@ -VERSION=v1.4.1 +VERSION=v1.4.2 SERVICE=vat-genscan diff --git a/config-examples/auth-config-dvwa.json b/config-examples/auth-config-dvwa.json new file mode 100644 index 0000000..4d56e98 --- /dev/null +++ b/config-examples/auth-config-dvwa.json @@ -0,0 +1,19 @@ +{ + "target": { + "url": "http://172.17.0.5/" + }, + "config": { + "w3af": { + "profile": "auth_scan", + "parameters": { + "username": "admin", + "password": "password", + "username_field": "username", + "password_field": "password", + "auth_url": "http://172.17.0.5/login.php", + "check_url": "http://172.17.0.5/index.php", + "check_string": "admin" + } + } + } +} diff --git a/config-examples/auth-config.json b/config-examples/auth-config.json new file mode 100644 index 0000000..0b1c31c --- /dev/null +++ b/config-examples/auth-config.json @@ -0,0 +1,22 @@ +{ + "target": { + "url": "http://192.168.1.184/bWAPP/" + }, + "config": { + "w3af": { + "profile": "auth_scan", + "parameters": { + "username": "bee", + "password": "bug", + "username_field": "login", + "password_field": "password", + "auth_url": "http://192.168.1.184/bWAPP/login.php", + "check_url": "http://192.168.1.184/bWAPP/portal.php", + "check_string": "Welcome Bee" + } + }, + "zap": { + "profile": "basic" + } + } +} diff --git a/basic-config.json b/config-examples/basic-config.json similarity index 100% rename from basic-config.json rename to config-examples/basic-config.json diff --git a/configure.py b/configure.py index f56f95c..5b63378 100644 --- a/configure.py +++ b/configure.py @@ -42,6 +42,14 @@ def configure(): cscan_config["W3AF"] = {"CS_W3AF": "/service/w3af/w3af_api"} if profile == "fast_scan": cscan_config["W3AF"]["CS_W3AF_PROFILE"] = "/service/w3af/profiles/fast_scan.pw3af" + elif profile == "auth_scan": + cscan_config["W3AF"]["CS_W3AF_PROFILE"] = "/service/w3af/profiles/auth_scan.pw3af" + w3af_config = configparser.ConfigParser() + w3af_config.read("/service/w3af/profiles/auth_scan.template") + for key in config["config"][scanner]["parameters"]: + w3af_config['auth.extended_generic'][key] = config["config"][scanner]["parameters"][key] + with open ("/service/w3af/profiles/auth_scan.pw3af", "w") as f_out: + w3af_config.write(f_out) else: raise UnsupportedProfileException() cs_scripts.append("w3af.sh") diff --git a/install/profiles/auth_scan.template b/install/profiles/auth_scan.template new file mode 100644 index 0000000..c93c13a --- /dev/null +++ b/install/profiles/auth_scan.template @@ -0,0 +1,140 @@ +[profile] +description = Profile generated using the console UI. +name = fast_scan_auth + +[grep.symfony] +override = False + +[grep.file_upload] + +[grep.wsdl_greper] + +[grep.form_autocomplete] + +[grep.strange_parameters] + +[grep.svn_users] + +[grep.private_ip] + +[grep.motw] + +[grep.code_disclosure] + +[grep.blank_body] + +[grep.path_disclosure] + +[grep.strange_http_codes] + +[grep.http_auth_detect] + +[grep.credit_cards] + +[grep.dom_xss] + +[grep.html_comments] + +[grep.http_in_body] + +[grep.dot_net_event_validation] + +[grep.ssn] + +[grep.error_500] + +[grep.meta_tags] + +[grep.lang] + +[grep.click_jacking] + +[grep.directory_indexing] + +[grep.password_profiling] + +[grep.get_emails] +only_target_domain = True + +[grep.hash_analysis] + +[grep.error_pages] + +[grep.strange_reason] + +[grep.user_defined_regex] +single_regex = +regex_file_path = %ROOT_PATH%/plugins/grep/user_defined_regex/empty.txt + +[grep.strange_headers] + +[grep.objects] + +[grep.oracle] + +[grep.feeds] + +[grep.analyze_cookies] + +[auth.extended_generic] +username = <username_value> +password = <password_value> +username_field = <username_field> +password_field = <password_field> +auth_url = <auth_url> +check_url = <check_url> +check_string = <check_string> + +[audit.xss] +persistent_xss = True + +[audit.sqli] + +[crawl.web_spider] +only_forward = False +follow_regex = .* +ignore_regex = + +[output.console] +verbose = False + +[misc-settings] +fuzz_cookies = False +fuzz_form_files = True +fuzz_url_filenames = False +fuzz_url_parts = False +fuzzed_files_extension = gif +fuzzable_headers = +form_fuzzing_mode = tmb +stop_on_first_exception = False +max_discovery_time = 120 +interface = ppp0 +local_ip_address = 10.32.31.5 +non_targets = +msf_location = /opt/metasploit3/bin/ + +[http-settings] +timeout = 0 +headers_file = +basic_auth_user = +basic_auth_passwd = +basic_auth_domain = +ntlm_auth_domain = +ntlm_auth_user = +ntlm_auth_passwd = +ntlm_auth_url = +cookie_jar_file = +ignore_session_cookies = False +proxy_port = 8080 +proxy_address = +user_agent = w3af.org +rand_user_agent = False +max_file_size = 400000 +max_http_retries = 2 +max_requests_per_second = 0 +always_404 = +never_404 = +string_match_404 = +url_parameter = + + diff --git a/install/profiles/extended_generic.py b/install/profiles/extended_generic.py new file mode 100644 index 0000000..44425ab --- /dev/null +++ b/install/profiles/extended_generic.py @@ -0,0 +1,232 @@ +from urllib import urlencode + +import w3af.core.controllers.output_manager as om + +from w3af.core.data.options.opt_factory import opt_factory +from w3af.core.data.options.option_list import OptionList +from w3af.core.controllers.plugins.auth_plugin import AuthPlugin +from w3af.core.controllers.exceptions import BaseFrameworkException +from lxml import etree + +class extended_generic(AuthPlugin): + """ + Generic authentication plugin extended to support CSRF tokens in login forms. + """ + + def __init__(self): + AuthPlugin.__init__(self) + + # User configuration + self.username = '' + self.password = '' + self.username_field = '' + self.password_field = '' + self.auth_url = 'http://host.tld/' + self.check_url = 'http://host.tld/' + self.check_string = '' + + # Internal attributes + self._attempt_login = True + + def login(self): + """ + Login to the application. + """ + # + # In some cases the authentication plugin is incorrectly configured and + # we don't want to keep trying over and over to login when we know it + # will fail + # + if not self._attempt_login: + return False + + # + # Create a new debugging ID for each login() run + # + self._new_debugging_id() + self._clear_log() + + msg = 'Logging into the application using %s' % self.username + om.out.debug(msg) + + #GET the login page + http_response = self._uri_opener.GET(self.auth_url, grep=False, + cache=False) + body = http_response.get_body() + ht = etree.HTML(body) + forms = ht.xpath('//form') + accepted_form = False + data_dict = dict() + for f in forms: + if f.get('method').lower()!='post' or not bool(f.xpath("//input[@name='%s']" % self.username_field)) or not bool(f.xpath("//input[@name='%s']" % self.password_field)): + continue + inputs = f.xpath("//input") + for i in inputs: + print i.values(), i.keys() + if i.get('name') in [self.username_field, self.password_field]: + #if i.get('type')=='submit' or i.get('name') in [self.username_field, self.password_field]: + continue + print i.values(), i.keys() + data_dict[i.get('name')]=i.get('value') + accepted_form = True + + if not accepted_form: + raise Exception("No matching form found at login page") + + data_dict[self.username_field]=self.username + data_dict[self.password_field]=self.password + data = urlencode(data_dict) + + try: + http_response = self._uri_opener.POST(self.auth_url, + data=data, + grep=False, + cache=False, + follow_redirects=True, + debugging_id=self._debugging_id) + except Exception, e: + msg = 'Failed to login to the application because of exception: %s' + self._log_debug(msg % e) + return False + + self._log_http_response(http_response) + + # + # Check if we're logged in + # + if not self.has_active_session(): + self._log_info_to_kb() + return False + + om.out.debug('Login success for %s' % self.username) + return True + + def logout(self): + """ + User logout + """ + return None + + def has_active_session(self): + """ + Check user session + """ + # Create a new debugging ID for each has_active_session() run + self._new_debugging_id() + + msg = 'Checking if session for user %s is active' + self._log_debug(msg % self.username) + + try: + http_response = self._uri_opener.GET(self.check_url, + grep=False, + cache=False, + follow_redirects=True, + debugging_id=self._debugging_id) + except Exception, e: + msg = 'Failed to check if session is active because of exception: %s' + self._log_debug(msg % e) + return False + + self._log_http_response(http_response) + + body = http_response.get_body() + logged_in = self.check_string in body + + msg_yes = 'User "%s" is currently logged into the application' + msg_yes %= (self.username,) + + msg_no = ('User "%s" is NOT logged into the application, the' + ' `check_string` was not found in the HTTP response' + ' with ID %s.') + msg_no %= (self.username, http_response.id) + + msg = msg_yes if logged_in else msg_no + self._log_debug(msg) + + return logged_in + + def get_options(self): + """ + :return: A list of option objects for this plugin. + """ + options = [ + ('username', self.username, 'string', + 'Username for using in the authentication process'), + + ('password', self.password, 'string', + 'Password for using in the authentication process'), + + ('username_field', self.username_field, + 'string', 'Username parameter name (ie. "uname" if the HTML looks' + ' like <input type="text" name="uname">...)'), + + ('password_field', self.password_field, + 'string', 'Password parameter name (ie. "pwd" if the HTML looks' + ' like <input type="password" name="pwd">...)'), + + ('auth_url', self.auth_url, 'url', + 'URL where the username and password will be sent using a POST' + ' request'), + + ('check_url', self.check_url, 'url', + 'URL used to verify if the session is still active by looking for' + ' the check_string.'), + + ('check_string', self.check_string, 'string', + 'String for searching on check_url page to determine if the' + 'current session is active.'), + ] + + ol = OptionList() + for o in options: + ol.add(opt_factory(o[0], o[1], o[3], o[2], help=o[3])) + + return ol + + def set_options(self, options_list): + """ + This method sets all the options that are configured using + the user interface generated by the framework using + the result of get_options(). + + :param options_list: A dict with the options for the plugin. + :return: No value is returned. + """ + self.username = options_list['username'].get_value() + self.password = options_list['password'].get_value() + self.username_field = options_list['username_field'].get_value() + self.password_field = options_list['password_field'].get_value() + self.check_string = options_list['check_string'].get_value() + self.auth_url = options_list['auth_url'].get_value() + self.check_url = options_list['check_url'].get_value() + + missing_options = [] + + for o in options_list: + if not o.get_value(): + missing_options.append(o.get_name()) + + if missing_options: + msg = ("All parameters are required and can't be empty. The" + " missing parameters are %s") + raise BaseFrameworkException(msg % ', '.join(missing_options)) + + def get_long_desc(self): + """ + :return: A DETAILED description of the plugin functions and features. + """ + return """ + This authentication plugin can login to Web applications which use + common authentication schemes. + Also tries to use additional parameters found in login forms for CSRF prevention. + + Seven configurable parameters exist: + - username + - password + - username_field + - password_field + - auth_url + - check_url + - check_string + """ diff --git a/install/w3af.sh b/install/w3af.sh index 071442a..dddc421 100644 --- a/install/w3af.sh +++ b/install/w3af.sh @@ -35,3 +35,7 @@ patch /service/w3af/w3af/core/ui/api/utils/scans.py /tmp/w3af_output_fix.patch patch /service/w3af/w3af/core/controllers/dependency_check/requirements.py /tmp/w3af-lz4.patch patch /service/w3af/w3af/core/ui/api/utils/scans.py /tmp/w3af-scans.py.patch +#additional profiles and plugins +cp /tmp/extended_generic.py /service/w3af/w3af/plugins/auth/ +cp /tmp/auth_scan.template /service/w3af/profiles/ + -- GitLab