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