From 50cb5a5b1ddf9cc4ed68772730a92c02edad35c4 Mon Sep 17 00:00:00 2001 From: Q_back Date: Tue, 12 May 2020 19:32:00 +0200 Subject: [PATCH 01/14] brought back FormInputSubmitStrategy, better alghoritm of discovering username input and login button --- .../controllers/chrome/js/dom_analyzer.js | 50 ++++++++++++++++--- .../chrome/login/submit_form/main.py | 2 +- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/w3af/core/controllers/chrome/js/dom_analyzer.js b/w3af/core/controllers/chrome/js/dom_analyzer.js index b8077b60a3..64da224d0f 100644 --- a/w3af/core/controllers/chrome/js/dom_analyzer.js +++ b/w3af/core/controllers/chrome/js/dom_analyzer.js @@ -330,7 +330,7 @@ var _DOMAnalyzer = _DOMAnalyzer || { if( !_DOMAnalyzer.eventIsValidForTagName( tag_name, type ) ) return false; let selector = OptimalSelect.getSingleSelector(element); - + // node_type is https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType#Node_type_constants _DOMAnalyzer.event_listeners.push({"tag_name": tag_name, "node_type": element.nodeType, @@ -865,6 +865,44 @@ var _DOMAnalyzer = _DOMAnalyzer || { return false; }, + /** + * This is naive function which takes parentElement (the login form) and + * tries to find username input field within it. + * @param {Node} parentElement - parent element to scope to document.querySelectorAll() + * @returns {NodeList} - result of querySelectorAll() + */ + _getUsernameInput(parentElement) { + result = document.querySelectorAll("input[type='email']", parentElement); + if (!result.length) { + result = document.querySelectorAll("input[type='text']", parentElement); + } + return result; + }, + + /** + * This is naive function which takes parentElement (the login form) and tries + * to find submit button within it. + * @param {Node} parentElement - parent element to scope to document.querySelectorAll() + * @returns {NodeList} - result of querySelectorAll() + */ + _getSubmitButton(parentElement) { + result = document.querySelectorAll("input[type='submit']", parentElement); + if (!result.length) { + result = document.querySelectorAll("button[type='submit']", parentElement); + } + // Maybe it's just normal button with innerText: 'Login'... + if (!result.length) { + result = []; + let buttons = document.querySelectorAll('button', parentElement); + for (let button of buttons) { + if (button.innerText.toLocaleLowerCase().includes('log')) { + result.push(button); + } + } + } + return result; + }, + /** * Return the CSS selector for the login forms which exist in the DOM. * @@ -898,7 +936,7 @@ var _DOMAnalyzer = _DOMAnalyzer || { let form = forms[0]; // Finally we confirm that the form has a type=text input - let text_fields = document.querySelectorAll("input[type='text']", form) + let text_fields = _getUsernameInput(form); // Zero text fields is most likely a password-only login form // Two text fields or more is most likely a registration from @@ -906,7 +944,7 @@ var _DOMAnalyzer = _DOMAnalyzer || { if (text_fields.length !== 1) continue; // And if there is a submit button I want that selector too - let submit_fields = document.querySelectorAll("input[type='submit']", form) + let submit_fields = _getSubmitButton(form); let submit_selector = null; if (submit_fields.length !== 0) { @@ -962,7 +1000,7 @@ var _DOMAnalyzer = _DOMAnalyzer || { // go up one more level, and so one. // // Find if this parent has a type=text input - let text_fields = document.querySelectorAll("input[type='text']", parent) + let text_fields = _getUsernameInput(parent) // Zero text fields is most likely a password-only login form // Two text fields or more is most likely a registration from @@ -974,7 +1012,7 @@ var _DOMAnalyzer = _DOMAnalyzer || { } // And if there is a submit button I want that selector too - let submit_fields = document.querySelectorAll("input[type='submit']", parent) + let submit_fields = _getSubmitButton(parent) let submit_selector = null; if (submit_fields.length !== 0) { @@ -1142,4 +1180,4 @@ var _DOMAnalyzer = _DOMAnalyzer || { }; -_DOMAnalyzer.initialize(); \ No newline at end of file +_DOMAnalyzer.initialize(); diff --git a/w3af/core/controllers/chrome/login/submit_form/main.py b/w3af/core/controllers/chrome/login/submit_form/main.py index b3954a5b92..4220629022 100644 --- a/w3af/core/controllers/chrome/login/submit_form/main.py +++ b/w3af/core/controllers/chrome/login/submit_form/main.py @@ -31,7 +31,7 @@ class FormSubmitter(object): STRATEGIES = [ PressEnterStrategy, PressTabEnterStrategy, - #FormInputSubmitStrategy + FormInputSubmitStrategy ] def __init__(self, chrome, form, login_form_url, username, password, debugging_id): From 13a5d0db1289da1b4128f5c67975a2a4e7236ee5 Mon Sep 17 00:00:00 2001 From: Q_back Date: Wed, 13 May 2020 00:08:25 +0200 Subject: [PATCH 02/14] fix _handle_authentication_success breaking parent's functionality in autocomplete_js --- w3af/plugins/auth/autocomplete_js.py | 1 + 1 file changed, 1 insertion(+) diff --git a/w3af/plugins/auth/autocomplete_js.py b/w3af/plugins/auth/autocomplete_js.py index 6b1d39e2cd..49ef5cd167 100644 --- a/w3af/plugins/auth/autocomplete_js.py +++ b/w3af/plugins/auth/autocomplete_js.py @@ -81,6 +81,7 @@ def login(self, debugging_id=None): return True def _handle_authentication_success(self): + self._login_result_log.append(True) # # Logging # From e04104f6f08bf1018c2b3cfc437b3fc46bd36b82 Mon Sep 17 00:00:00 2001 From: Q_back Date: Wed, 13 May 2020 01:58:39 +0200 Subject: [PATCH 03/14] revert enabling FormInputSubmitStrategy from pre-previous commit --- w3af/core/controllers/chrome/login/submit_form/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/w3af/core/controllers/chrome/login/submit_form/main.py b/w3af/core/controllers/chrome/login/submit_form/main.py index 4220629022..adc75e4037 100644 --- a/w3af/core/controllers/chrome/login/submit_form/main.py +++ b/w3af/core/controllers/chrome/login/submit_form/main.py @@ -31,7 +31,7 @@ class FormSubmitter(object): STRATEGIES = [ PressEnterStrategy, PressTabEnterStrategy, - FormInputSubmitStrategy + # FormInputSubmitStrategy ] def __init__(self, chrome, form, login_form_url, username, password, debugging_id): From 389646da571ba98d9e7b4e3ed033c28c35c49a41 Mon Sep 17 00:00:00 2001 From: Q_back Date: Wed, 13 May 2020 13:46:58 +0200 Subject: [PATCH 04/14] new options in autocomplete_js to manually provide username/submit button CSS selectors --- .../controllers/chrome/instrumented/main.py | 26 +++++++-- .../controllers/chrome/js/dom_analyzer.js | 36 +++++++++--- .../chrome/login/find_form/main.py | 11 +++- .../strategies/base_find_form_strategy.py | 18 ++++++ .../login/find_form/strategies/form_tag.py | 9 ++- .../strategies/password_and_parent.py | 12 ++-- w3af/core/data/options/option_list.py | 12 ++++ w3af/plugins/auth/autocomplete_js.py | 56 ++++++++++++++++++- 8 files changed, 153 insertions(+), 27 deletions(-) create mode 100644 w3af/core/controllers/chrome/login/find_form/strategies/base_find_form_strategy.py diff --git a/w3af/core/controllers/chrome/instrumented/main.py b/w3af/core/controllers/chrome/instrumented/main.py index 41262e49ba..092f6c19ff 100644 --- a/w3af/core/controllers/chrome/instrumented/main.py +++ b/w3af/core/controllers/chrome/instrumented/main.py @@ -297,11 +297,20 @@ def dispatch_js_event(self, selector, event_type): return True - def get_login_forms(self): + def get_login_forms(self, exact_css_selectors): """ + :param dict exact_css_selectors: Optional parameter containing css selectors + for part of form like username input or login button. :return: Yield LoginForm instances """ - result = self.js_runtime_evaluate('window._DOMAnalyzer.getLoginForms()') + func = ( + 'window._DOMAnalyzer.getLoginForms("{}", "{}")' + ) + func = func.format( + exact_css_selectors.get('username_input', ''), + exact_css_selectors.get('login_button', ''), + ) + result = self.js_runtime_evaluate(func) if result is None: raise EventTimeout('The event execution timed out') @@ -316,11 +325,20 @@ def get_login_forms(self): yield login_form - def get_login_forms_without_form_tags(self): + def get_login_forms_without_form_tags(self, exact_css_selectors): """ + :param dict exact_css_selectors: Optional parameter containing css selectors + for part of form like username input or login button. :return: Yield LoginForm instances """ - result = self.js_runtime_evaluate('window._DOMAnalyzer.getLoginFormsWithoutFormTags()') + func = ( + 'window._DOMAnalyzer.getLoginFormsWithoutFormTags("{}", "{}")' + ) + func = func.format( + exact_css_selectors.get('username_input', ''), + exact_css_selectors.get('login_button', ''), + ) + result = self.js_runtime_evaluate(func) if result is None: raise EventTimeout('The event execution timed out') diff --git a/w3af/core/controllers/chrome/js/dom_analyzer.js b/w3af/core/controllers/chrome/js/dom_analyzer.js index 64da224d0f..c512ce52b3 100644 --- a/w3af/core/controllers/chrome/js/dom_analyzer.js +++ b/w3af/core/controllers/chrome/js/dom_analyzer.js @@ -868,10 +868,15 @@ var _DOMAnalyzer = _DOMAnalyzer || { /** * This is naive function which takes parentElement (the login form) and * tries to find username input field within it. - * @param {Node} parentElement - parent element to scope to document.querySelectorAll() + * @param {Node} parentElement - parent element to scope to document.querySelectorAll() + * @param {String} exactSelector - optional CSS selector. If provided prevents + * using standard selectors * @returns {NodeList} - result of querySelectorAll() */ - _getUsernameInput(parentElement) { + _getUsernameInput(parentElement, exactSelector = '') { + if (exactSelector) { + return document.querySelectorAll(exactSelector, parentElement); + } result = document.querySelectorAll("input[type='email']", parentElement); if (!result.length) { result = document.querySelectorAll("input[type='text']", parentElement); @@ -882,10 +887,15 @@ var _DOMAnalyzer = _DOMAnalyzer || { /** * This is naive function which takes parentElement (the login form) and tries * to find submit button within it. - * @param {Node} parentElement - parent element to scope to document.querySelectorAll() + * @param {Node} parentElement - parent element to scope to document.querySelectorAll() + * @param {String} exactSelector - optional CSS selector. If provided prevents + * using standard selectors * @returns {NodeList} - result of querySelectorAll() */ - _getSubmitButton(parentElement) { + _getSubmitButton(parentElement, exactSelector = '') { + if (exactSelector) { + return document.querySelectorAll(exactSelector, parentElement); + } result = document.querySelectorAll("input[type='submit']", parentElement); if (!result.length) { result = document.querySelectorAll("button[type='submit']", parentElement); @@ -912,8 +922,12 @@ var _DOMAnalyzer = _DOMAnalyzer || { * - , and * - * + * @param {String} usernameCssSelector - CSS selector for username input. If + * provided we won't try to find username input automatically. + * @param {String} submitButtonCssSelector - CSS selector for submit button. If + * provided we won't try to find submit button autmatically. */ - getLoginForms: function () { + getLoginForms: function (usernameCssSelector = '', submitButtonCssSelector = '') { let login_forms = []; // First we identify the forms with a password field using a descendant Selector @@ -936,7 +950,7 @@ var _DOMAnalyzer = _DOMAnalyzer || { let form = forms[0]; // Finally we confirm that the form has a type=text input - let text_fields = _getUsernameInput(form); + let text_fields = this._getUsernameInput(form, usernameCssSelector); // Zero text fields is most likely a password-only login form // Two text fields or more is most likely a registration from @@ -944,7 +958,7 @@ var _DOMAnalyzer = _DOMAnalyzer || { if (text_fields.length !== 1) continue; // And if there is a submit button I want that selector too - let submit_fields = _getSubmitButton(form); + let submit_fields = this._getSubmitButton(form, submitButtonCssSelector); let submit_selector = null; if (submit_fields.length !== 0) { @@ -974,6 +988,10 @@ var _DOMAnalyzer = _DOMAnalyzer || { * - , and * - * + * @param {String} usernameCssSelector - CSS selector for username input. If + * provided we won't try to find username input automatically. + * @param {String} submitButtonCssSelector - CSS selector for submit button. If + * provided we won't try to find submit button autmatically. */ getLoginFormsWithoutFormTags: function () { let login_forms = []; @@ -1000,7 +1018,7 @@ var _DOMAnalyzer = _DOMAnalyzer || { // go up one more level, and so one. // // Find if this parent has a type=text input - let text_fields = _getUsernameInput(parent) + let text_fields = this._getUsernameInput(parent, usernameCssSelector); // Zero text fields is most likely a password-only login form // Two text fields or more is most likely a registration from @@ -1012,7 +1030,7 @@ var _DOMAnalyzer = _DOMAnalyzer || { } // And if there is a submit button I want that selector too - let submit_fields = _getSubmitButton(parent) + let submit_fields = this._getSubmitButton(parent, submitButtonCssSelector) let submit_selector = null; if (submit_fields.length !== 0) { diff --git a/w3af/core/controllers/chrome/login/find_form/main.py b/w3af/core/controllers/chrome/login/find_form/main.py index 2ee45ad7f5..1c0c7917b6 100644 --- a/w3af/core/controllers/chrome/login/find_form/main.py +++ b/w3af/core/controllers/chrome/login/find_form/main.py @@ -36,14 +36,21 @@ def __init__(self, chrome, debugging_id): self.chrome = chrome self.debugging_id = debugging_id - def find_forms(self): + def find_forms(self, css_selectors=None): """ + :param dict css_selectors: optional dict of css selectors used to find + elements of form (like username input or login button) :return: Yield forms as they are found by each strategy """ + if css_selectors: + msg = 'Form finder uses the CSS selectors: "%s" (did: %s)' + args = (css_selectors, self.debugging_id) + om.out.debug(msg % args) + identified_forms = [] for strategy_klass in self.STRATEGIES: - strategy = strategy_klass(self.chrome, self.debugging_id) + strategy = strategy_klass(self.chrome, self.debugging_id, css_selectors) try: for form in strategy.find_forms(): diff --git a/w3af/core/controllers/chrome/login/find_form/strategies/base_find_form_strategy.py b/w3af/core/controllers/chrome/login/find_form/strategies/base_find_form_strategy.py new file mode 100644 index 0000000000..00caf488cf --- /dev/null +++ b/w3af/core/controllers/chrome/login/find_form/strategies/base_find_form_strategy.py @@ -0,0 +1,18 @@ +class BaseFindFormStrategy: + def __init__(self, chrome, debugging_id, exact_css_selectors=None): + """ + :param InstrumentedChrome chrome: + :param String debugging_id: + :param dict exact_css_selectors: Optional parameter containing css selectors + for part of form like username input or login button. + """ + self.chrome = chrome + self.debugging_id = debugging_id + self.exact_css_selectors = exact_css_selectors or {} + + def find_forms(self): + raise NotImplementedError + + @staticmethod + def get_name(): + return 'BaseFindFormStrategy' diff --git a/w3af/core/controllers/chrome/login/find_form/strategies/form_tag.py b/w3af/core/controllers/chrome/login/find_form/strategies/form_tag.py index bf47ba4a17..ec6da6aab0 100644 --- a/w3af/core/controllers/chrome/login/find_form/strategies/form_tag.py +++ b/w3af/core/controllers/chrome/login/find_form/strategies/form_tag.py @@ -19,12 +19,11 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA """ +from w3af.core.controllers.chrome.login.find_form.strategies.base_find_form_strategy import \ + BaseFindFormStrategy -class FormTagStrategy(object): - def __init__(self, chrome, debugging_id): - self.chrome = chrome - self.debugging_id = debugging_id +class FormTagStrategy(BaseFindFormStrategy): def find_forms(self): """ @@ -37,7 +36,7 @@ def _simple_form_with_username_password_submit(self): """ :return: Yield forms that have username, password and submit inputs """ - for login_form in self.chrome.get_login_forms(): + for login_form in self.chrome.get_login_forms(self.exact_css_selectors): yield login_form @staticmethod diff --git a/w3af/core/controllers/chrome/login/find_form/strategies/password_and_parent.py b/w3af/core/controllers/chrome/login/find_form/strategies/password_and_parent.py index 1f64780502..4dbf7c654a 100644 --- a/w3af/core/controllers/chrome/login/find_form/strategies/password_and_parent.py +++ b/w3af/core/controllers/chrome/login/find_form/strategies/password_and_parent.py @@ -19,12 +19,11 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA """ +from w3af.core.controllers.chrome.login.find_form.strategies.base_find_form_strategy import \ + BaseFindFormStrategy -class PasswordAndParentStrategy(object): - def __init__(self, chrome, debugging_id): - self.chrome = chrome - self.debugging_id = debugging_id +class PasswordAndParentStrategy(BaseFindFormStrategy): def find_forms(self): """ @@ -32,8 +31,9 @@ def find_forms(self): :return: Yield forms which are identified by the strategy algorithm """ - for login_form in self.chrome.get_login_forms_without_form_tags(): + for login_form in self.chrome.get_login_forms_without_form_tags(self.exact_css_selectors): yield login_form - def get_name(self): + @staticmethod + def get_name(): return 'PasswordAndParent' diff --git a/w3af/core/data/options/option_list.py b/w3af/core/data/options/option_list.py index 74f3c4820d..ff4a1d5207 100644 --- a/w3af/core/data/options/option_list.py +++ b/w3af/core/data/options/option_list.py @@ -35,6 +35,18 @@ def add(self, option): self._internal_opt_list.append(option) append = add + def pop(self, option): + """ + DANGEROUS!! + You will probably want to deepcopy the OptionList instance before + modifying it with this method to don't block the user from accessing options + again. + """ + if not isinstance(option, int): + option_names = [item.get_name() for item in self._internal_opt_list] + option = option_names.index(option) + return self._internal_opt_list.pop(option) + def __len__(self): return len(self._internal_opt_list) diff --git a/w3af/plugins/auth/autocomplete_js.py b/w3af/plugins/auth/autocomplete_js.py index 49ef5cd167..e1e250ac8d 100644 --- a/w3af/plugins/auth/autocomplete_js.py +++ b/w3af/plugins/auth/autocomplete_js.py @@ -20,7 +20,10 @@ """ import Queue +from copy import deepcopy +from w3af.core.data.options.opt_factory import opt_factory +from w3af.core.data.options.option_types import STRING from w3af.core.data.request.fuzzable_request import FuzzableRequest from w3af.core.controllers.chrome.instrumented.main import InstrumentedChrome from w3af.core.controllers.chrome.login.find_form.main import FormFinder @@ -36,6 +39,10 @@ class autocomplete_js(autocomplete): def __init__(self): autocomplete.__init__(self) + # default values for autocomplete_js options + self.username_field_css_selector = '' + self.login_button_css_selector = '' + self._login_form = None self._http_traffic_queue = None @@ -208,8 +215,12 @@ def _find_all_login_forms(self, chrome): * Use the FormFinder class to yield all existing forms """ form_finder = FormFinder(chrome, self._debugging_id) + css_selectors = { + 'username_input': self.username_field_css_selector, + 'login_button': self.login_button_css_selector, + } - for form in form_finder.find_forms(): + for form in form_finder.find_forms(css_selectors): msg = 'Found potential login form: %s' args = (form,) @@ -273,6 +284,49 @@ def has_active_session(self, debugging_id=None): chrome.terminate() return has_active_session + def get_options(self): + """ + :returns OptionList: list of option objects for plugin + """ + option_list = super(autocomplete_js, self).get_options() + autocomplete_js_options = [ + ( + 'username_field_css_selector', + self.username_field_css_selector, + STRING, + "(Optional) Exact CSS selector which will be used to retrieve " + "the username input field. If provided then w3af won't try " + "to detect input field automatically." + ), + ( + 'login_button_css_selector', + self.login_button_css_selector, + STRING, + "(Optional) Exact CSS selector which will be used to retrieve " + "the login button field. If provided then w3af won't try " + "to detect button automatically." + ), + ] + for option in autocomplete_js_options: + option_list.add(opt_factory( + option[0], + option[1], + option[3], + option[2], + help=option[3], + )) + return option_list + + def set_options(self, options_list): + options_list_copy = deepcopy(options_list) # we don't want to touch real option_list + self.username_field_css_selector = options_list_copy.pop( + 'username_field_css_selector' + ).get_value() + self.login_button_css_selector = options_list_copy.pop( + 'login_button_css_selector' + ).get_value() + super(autocomplete_js, self).set_options(options_list_copy) + def get_long_desc(self): """ :return: A DETAILED description of the plugin functions and features. From 52b8f39c473c30ceb19fe37f6ca7d30397a72bf2 Mon Sep 17 00:00:00 2001 From: Q_back Date: Wed, 13 May 2020 23:49:03 +0200 Subject: [PATCH 05/14] don't create new chrome instance when running autocomplete_js.has_active_session() --- w3af/plugins/auth/autocomplete_js.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/w3af/plugins/auth/autocomplete_js.py b/w3af/plugins/auth/autocomplete_js.py index e1e250ac8d..ab232008c4 100644 --- a/w3af/plugins/auth/autocomplete_js.py +++ b/w3af/plugins/auth/autocomplete_js.py @@ -251,7 +251,7 @@ def _find_form_submit_strategy(self, chrome, form): for form_submit_strategy in form_submitter.submit_form(): - if not self.has_active_session(debugging_id=self._debugging_id): + if not self.has_active_session(debugging_id=self._debugging_id, chrome=chrome): # No need to set the state of the chrome browser back to the # login page, that is performed inside the FormSubmitter continue @@ -268,17 +268,24 @@ def _find_form_submit_strategy(self, chrome, form): return None - def has_active_session(self, debugging_id=None): + def has_active_session(self, debugging_id=None, chrome=None): """ Check user session with chrome """ has_active_session = False self._set_debugging_id(debugging_id) - chrome = self._get_chrome_instance(load_url=False) + if not chrome: + chrome = self._get_chrome_instance(load_url=False) try: chrome.load_url(self.check_url) - chrome.wait_for_load() + chrome.chrome_conn.Page.reload(ignore_cache=True) + loaded = chrome.wait_for_load() + if not loaded: + msg = 'Failed to load %s in chrome for autocomplete_js' + args = (self.check_url,) + self._log_debug(msg % args) + return False has_active_session = self.check_string in chrome.get_dom() finally: chrome.terminate() From 2192ca51bd7b84ef3f616f7ebae264b9eb9057cd Mon Sep 17 00:00:00 2001 From: Q_back Date: Thu, 14 May 2020 00:27:16 +0200 Subject: [PATCH 06/14] fix after rebase --- w3af/core/controllers/chrome/js/dom_analyzer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/w3af/core/controllers/chrome/js/dom_analyzer.js b/w3af/core/controllers/chrome/js/dom_analyzer.js index c512ce52b3..b5c34f5fcd 100644 --- a/w3af/core/controllers/chrome/js/dom_analyzer.js +++ b/w3af/core/controllers/chrome/js/dom_analyzer.js @@ -993,7 +993,7 @@ var _DOMAnalyzer = _DOMAnalyzer || { * @param {String} submitButtonCssSelector - CSS selector for submit button. If * provided we won't try to find submit button autmatically. */ - getLoginFormsWithoutFormTags: function () { + getLoginFormsWithoutFormTags: function (usernameCssSelector = '', submitButtonCssSelector = '') { let login_forms = []; // First we identify the password fields From 1723b6b38b329513eec326bcd70bf4756b05c417 Mon Sep 17 00:00:00 2001 From: Q_back Date: Thu, 21 May 2020 19:52:06 +0200 Subject: [PATCH 07/14] reloading chrome when checking active session may break the session --- w3af/plugins/auth/autocomplete_js.py | 1 - 1 file changed, 1 deletion(-) diff --git a/w3af/plugins/auth/autocomplete_js.py b/w3af/plugins/auth/autocomplete_js.py index ab232008c4..128104af3d 100644 --- a/w3af/plugins/auth/autocomplete_js.py +++ b/w3af/plugins/auth/autocomplete_js.py @@ -279,7 +279,6 @@ def has_active_session(self, debugging_id=None, chrome=None): try: chrome.load_url(self.check_url) - chrome.chrome_conn.Page.reload(ignore_cache=True) loaded = chrome.wait_for_load() if not loaded: msg = 'Failed to load %s in chrome for autocomplete_js' From b81b71d8d9c9c038161807a98c457bc6ead1f165 Mon Sep 17 00:00:00 2001 From: Q_back Date: Thu, 21 May 2020 20:35:27 +0200 Subject: [PATCH 08/14] implemented _login_using_existing_form --- w3af/plugins/auth/autocomplete_js.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/w3af/plugins/auth/autocomplete_js.py b/w3af/plugins/auth/autocomplete_js.py index 128104af3d..a76b5803a0 100644 --- a/w3af/plugins/auth/autocomplete_js.py +++ b/w3af/plugins/auth/autocomplete_js.py @@ -137,7 +137,12 @@ def _login_using_existing_form(self, chrome): :param chrome: The chrome instance to use during login :return: True if login was successful """ - raise NotImplementedError + form_submit_strategy = self._find_form_submit_strategy(chrome, self._login_form) + if form_submit_strategy is None: + return False + self._login_form.set_submit_strategy(form_submit_strategy) + self._log_debug('Identified valid login form: %s' % self._login_form) + return True def _login_and_save_form(self, chrome): """ From d4b6531d78f3d492daf98c8ba8558db126987c9b Mon Sep 17 00:00:00 2001 From: Q_back Date: Mon, 1 Jun 2020 20:03:05 +0200 Subject: [PATCH 09/14] fix iterate error in frame manager, don't kill chrome in autocomplete_js.has_active_session if chrome belongs to outer scope --- .../chrome/instrumented/frame_manager.py | 2 +- .../chrome/login/submit_form/main.py | 3 +++ w3af/plugins/auth/autocomplete_js.py | 27 +++++++++++++------ 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/w3af/core/controllers/chrome/instrumented/frame_manager.py b/w3af/core/controllers/chrome/instrumented/frame_manager.py index 14f660559a..f96f46e8c6 100644 --- a/w3af/core/controllers/chrome/instrumented/frame_manager.py +++ b/w3af/core/controllers/chrome/instrumented/frame_manager.py @@ -166,7 +166,7 @@ def _on_frame_navigated(self, message): # URL all the child frames are removed from Chrome, we should remove # them from our code too to mirror state if frame: - for child_frame_id, child_frame in frame.child_frames: + for child_frame_id, child_frame in frame.child_frames.items(): child_frame.detach(self) frame.set_navigated() diff --git a/w3af/core/controllers/chrome/login/submit_form/main.py b/w3af/core/controllers/chrome/login/submit_form/main.py index adc75e4037..a4726663c8 100644 --- a/w3af/core/controllers/chrome/login/submit_form/main.py +++ b/w3af/core/controllers/chrome/login/submit_form/main.py @@ -19,6 +19,8 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA """ +import traceback + from w3af.core.controllers import output_manager as om from w3af.core.controllers.chrome.login.submit_form.strategies.press_enter import PressEnterStrategy @@ -91,3 +93,4 @@ def _handle_exception(self, strategy, e): e, self.debugging_id) om.out.debug(msg % args) + om.out.error(traceback.format_exc()) diff --git a/w3af/plugins/auth/autocomplete_js.py b/w3af/plugins/auth/autocomplete_js.py index a76b5803a0..e7ff9b2d3a 100644 --- a/w3af/plugins/auth/autocomplete_js.py +++ b/w3af/plugins/auth/autocomplete_js.py @@ -257,6 +257,9 @@ def _find_form_submit_strategy(self, chrome, form): for form_submit_strategy in form_submitter.submit_form(): if not self.has_active_session(debugging_id=self._debugging_id, chrome=chrome): + msg = '%s is invalid form submit strategy for %s' + args = (form_submit_strategy.get_name(), form) + self._log_debug(msg % args) # No need to set the state of the chrome browser back to the # login page, that is performed inside the FormSubmitter continue @@ -276,23 +279,31 @@ def _find_form_submit_strategy(self, chrome, form): def has_active_session(self, debugging_id=None, chrome=None): """ Check user session with chrome + :param str debugging_id: string representing debugging id. + :param InstrumentedChrome chrome: chrome instance passed from outer scope + to reuse. EDGE CASE EXAMPLE: + Sometimes we don't want to create new chrome instance. For example + when we login for the first time to webapp and in _find_form_submit_strategy() + we just pressed enter in login form. Browser may take some actions under + the hood like sending XHR to backend API and after receiving response + setting API token at localStorage. Before token will be saved to localStorage + it may exist only in webapp's code, so using the same chrome will prevent + us from performing check without credentials. """ has_active_session = False + is_new_chrome_instance_created = False self._set_debugging_id(debugging_id) - if not chrome: + if not chrome or not chrome.chrome_conn: chrome = self._get_chrome_instance(load_url=False) + is_new_chrome_instance_created = True try: chrome.load_url(self.check_url) - loaded = chrome.wait_for_load() - if not loaded: - msg = 'Failed to load %s in chrome for autocomplete_js' - args = (self.check_url,) - self._log_debug(msg % args) - return False + chrome.wait_for_load() has_active_session = self.check_string in chrome.get_dom() finally: - chrome.terminate() + if is_new_chrome_instance_created: + chrome.terminate() return has_active_session def get_options(self): From 25a11251a900c7f5362417f3a2a6b922b301baae Mon Sep 17 00:00:00 2001 From: Q_back Date: Wed, 3 Jun 2020 16:24:47 +0200 Subject: [PATCH 10/14] better description for new params in autocomplete_js --- w3af/plugins/auth/autocomplete_js.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/w3af/plugins/auth/autocomplete_js.py b/w3af/plugins/auth/autocomplete_js.py index e7ff9b2d3a..6f3af562ea 100644 --- a/w3af/plugins/auth/autocomplete_js.py +++ b/w3af/plugins/auth/autocomplete_js.py @@ -317,16 +317,16 @@ def get_options(self): self.username_field_css_selector, STRING, "(Optional) Exact CSS selector which will be used to retrieve " - "the username input field. If provided then w3af won't try " - "to detect input field automatically." + "the username input field. When provided the scanner is not going" + " to try to detect the input field in an automated way" ), ( 'login_button_css_selector', self.login_button_css_selector, STRING, "(Optional) Exact CSS selector which will be used to retrieve " - "the login button field. If provided then w3af won't try " - "to detect button automatically." + "the login button field. When provided the scanner is not going " + "to try to detect the login button in an automated way" ), ] for option in autocomplete_js_options: @@ -360,7 +360,11 @@ def get_long_desc(self): The plugin loads the `login_form_url` to obtain the login form, automatically identifies the inputs where the `username` and `password` should be entered, - and then submits the form by clicking on the login button. + and then submits the form by clicking on the login button. You can specify + the exact CSS selectors (like ".login > input #password") in + `username_filed_css_selector` and `login_button_css_selector` to force + plugin to use those selectors in case when it can't find username field + or login button automatically. The following configurable parameters exist: - username @@ -368,4 +372,6 @@ def get_long_desc(self): - login_form_url - check_url - check_string + - username_field_css_selector + - login_button_css_selector """ From 888eea6f9860dbf000d42706b334796b03391128 Mon Sep 17 00:00:00 2001 From: Q_back Date: Wed, 3 Jun 2020 17:05:38 +0200 Subject: [PATCH 11/14] sometimes login button doesn't contain 'log' characters in it's text, so we return all buttons in parent node --- w3af/core/controllers/chrome/js/dom_analyzer.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/w3af/core/controllers/chrome/js/dom_analyzer.js b/w3af/core/controllers/chrome/js/dom_analyzer.js index b5c34f5fcd..d1f95e5159 100644 --- a/w3af/core/controllers/chrome/js/dom_analyzer.js +++ b/w3af/core/controllers/chrome/js/dom_analyzer.js @@ -900,15 +900,9 @@ var _DOMAnalyzer = _DOMAnalyzer || { if (!result.length) { result = document.querySelectorAll("button[type='submit']", parentElement); } - // Maybe it's just normal button with innerText: 'Login'... + // Maybe it's just normal button without type="submit"... if (!result.length) { - result = []; - let buttons = document.querySelectorAll('button', parentElement); - for (let button of buttons) { - if (button.innerText.toLocaleLowerCase().includes('log')) { - result.push(button); - } - } + result = document.querySelectorAll('button', parentElement); } return result; }, From 27ede049987a78554da7dcd55837194079fd777d Mon Sep 17 00:00:00 2001 From: Q_back Date: Thu, 4 Jun 2020 10:43:00 +0200 Subject: [PATCH 12/14] add option to click on element before autocompleting form --- w3af/core/controllers/chrome/js/dom_analyzer.js | 5 +++++ .../controllers/chrome/login/find_form/main.py | 1 + .../strategies/base_find_form_strategy.py | 17 +++++++++++++++++ w3af/plugins/auth/autocomplete_js.py | 17 +++++++++++++++++ 4 files changed, 40 insertions(+) diff --git a/w3af/core/controllers/chrome/js/dom_analyzer.js b/w3af/core/controllers/chrome/js/dom_analyzer.js index d1f95e5159..8b4ae2c106 100644 --- a/w3af/core/controllers/chrome/js/dom_analyzer.js +++ b/w3af/core/controllers/chrome/js/dom_analyzer.js @@ -1049,6 +1049,11 @@ var _DOMAnalyzer = _DOMAnalyzer || { return JSON.stringify(login_forms); }, + clickOnSelector(exactSelector) { + let element = document.querySelector(exactSelector); + element.click(); + }, + sliceAndSerialize: function (filtered_event_listeners, start, count) { return JSON.stringify(filtered_event_listeners.slice(start, start + count)); }, diff --git a/w3af/core/controllers/chrome/login/find_form/main.py b/w3af/core/controllers/chrome/login/find_form/main.py index 1c0c7917b6..8d50f93b81 100644 --- a/w3af/core/controllers/chrome/login/find_form/main.py +++ b/w3af/core/controllers/chrome/login/find_form/main.py @@ -53,6 +53,7 @@ def find_forms(self, css_selectors=None): strategy = strategy_klass(self.chrome, self.debugging_id, css_selectors) try: + strategy.prepare() for form in strategy.find_forms(): if form in identified_forms: continue diff --git a/w3af/core/controllers/chrome/login/find_form/strategies/base_find_form_strategy.py b/w3af/core/controllers/chrome/login/find_form/strategies/base_find_form_strategy.py index 00caf488cf..8d5b3f683d 100644 --- a/w3af/core/controllers/chrome/login/find_form/strategies/base_find_form_strategy.py +++ b/w3af/core/controllers/chrome/login/find_form/strategies/base_find_form_strategy.py @@ -1,3 +1,6 @@ +from w3af.core.controllers.chrome.instrumented.exceptions import EventTimeout + + class BaseFindFormStrategy: def __init__(self, chrome, debugging_id, exact_css_selectors=None): """ @@ -10,6 +13,20 @@ def __init__(self, chrome, debugging_id, exact_css_selectors=None): self.debugging_id = debugging_id self.exact_css_selectors = exact_css_selectors or {} + def prepare(self): + """ + :raises EventTimeout: + Hook called before find_forms() + """ + form_activator_selector = self.exact_css_selectors.get('form_activator') + if form_activator_selector: + func = 'window._DOMAnalyzer.clickOnSelector({})'.format( + form_activator_selector + ) + result = self.chrome.js_runtime_evaluate(func) + if result is None: + raise EventTimeout('The event execution timed out') + def find_forms(self): raise NotImplementedError diff --git a/w3af/plugins/auth/autocomplete_js.py b/w3af/plugins/auth/autocomplete_js.py index 6f3af562ea..488a1a5ee9 100644 --- a/w3af/plugins/auth/autocomplete_js.py +++ b/w3af/plugins/auth/autocomplete_js.py @@ -42,6 +42,7 @@ def __init__(self): # default values for autocomplete_js options self.username_field_css_selector = '' self.login_button_css_selector = '' + self.login_form_activator_css_selector = '' self._login_form = None self._http_traffic_queue = None @@ -223,6 +224,7 @@ def _find_all_login_forms(self, chrome): css_selectors = { 'username_input': self.username_field_css_selector, 'login_button': self.login_button_css_selector, + 'form_activator': self.login_form_activator_css_selector, } for form in form_finder.find_forms(css_selectors): @@ -328,6 +330,13 @@ def get_options(self): "the login button field. When provided the scanner is not going " "to try to detect the login button in an automated way" ), + ( + 'login_form_activator_css_selector', + self.login_form_activator_css_selector, + STRING, + "(Optional) Exact CSS selector for the element which needs to be " + "clicked to show login form." + ) ] for option in autocomplete_js_options: option_list.add(opt_factory( @@ -347,6 +356,9 @@ def set_options(self, options_list): self.login_button_css_selector = options_list_copy.pop( 'login_button_css_selector' ).get_value() + self.login_form_activator_css_selector = options_list_copy.pop( + 'login_form_activator_css_selector' + ).get_value() super(autocomplete_js, self).set_options(options_list_copy) def get_long_desc(self): @@ -366,6 +378,10 @@ def get_long_desc(self): plugin to use those selectors in case when it can't find username field or login button automatically. + If the page requires to click on something to show the login form you + can set `login_form_activator_css_selector` and scanner will use it + find and click on element + The following configurable parameters exist: - username - password @@ -374,4 +390,5 @@ def get_long_desc(self): - check_string - username_field_css_selector - login_button_css_selector + - login_form_activator_css_selector """ From 5854763d7786802b7dceac8b686ff9d8dc25454a Mon Sep 17 00:00:00 2001 From: Q_back Date: Tue, 9 Jun 2020 14:15:28 +0200 Subject: [PATCH 13/14] fix error when user provides CSS selectors with quotes. Slightly better exception handling during js login process --- .../controllers/chrome/devtools/exceptions.py | 8 +++++ .../controllers/chrome/instrumented/main.py | 29 ++++++++----------- .../controllers/chrome/js/dom_analyzer.js | 1 + .../chrome/login/find_form/main.py | 2 +- .../strategies/base_find_form_strategy.py | 4 +-- 5 files changed, 24 insertions(+), 20 deletions(-) diff --git a/w3af/core/controllers/chrome/devtools/exceptions.py b/w3af/core/controllers/chrome/devtools/exceptions.py index 38ac0e3561..45214b34c6 100644 --- a/w3af/core/controllers/chrome/devtools/exceptions.py +++ b/w3af/core/controllers/chrome/devtools/exceptions.py @@ -27,3 +27,11 @@ class ChromeInterfaceException(Exception): class ChromeInterfaceTimeout(Exception): pass + + +class ChromeScriptRuntimeException(Exception): + def __init__(self, message, function_called=None, *args): + if function_called: + message = "function: {}, exception: {}".format(function_called, message) + super(ChromeScriptRuntimeException, self).__init__(message, *args) + pass diff --git a/w3af/core/controllers/chrome/instrumented/main.py b/w3af/core/controllers/chrome/instrumented/main.py index 092f6c19ff..6ef3411fe2 100644 --- a/w3af/core/controllers/chrome/instrumented/main.py +++ b/w3af/core/controllers/chrome/instrumented/main.py @@ -23,6 +23,7 @@ import json import w3af.core.controllers.output_manager as om +from w3af.core.controllers.chrome.devtools.exceptions import ChromeScriptRuntimeException from w3af.core.data.parsers.doc.url import URL from w3af.core.controllers.chrome.instrumented.instrumented_base import InstrumentedChromeBase @@ -307,8 +308,8 @@ def get_login_forms(self, exact_css_selectors): 'window._DOMAnalyzer.getLoginForms("{}", "{}")' ) func = func.format( - exact_css_selectors.get('username_input', ''), - exact_css_selectors.get('login_button', ''), + exact_css_selectors.get('username_input', '').replace('"', '\\"'), + exact_css_selectors.get('login_button', '').replace('"', '\\"'), ) result = self.js_runtime_evaluate(func) @@ -335,8 +336,8 @@ def get_login_forms_without_form_tags(self, exact_css_selectors): 'window._DOMAnalyzer.getLoginFormsWithoutFormTags("{}", "{}")' ) func = func.format( - exact_css_selectors.get('username_input', ''), - exact_css_selectors.get('login_button', ''), + exact_css_selectors.get('username_input', '').replace('"', '\\"'), + exact_css_selectors.get('login_button', '').replace('"', '\\"'), ) result = self.js_runtime_evaluate(func) @@ -607,19 +608,13 @@ def js_runtime_evaluate(self, expression, timeout=5): timeout=timeout) # This is a rare case where the DOM is not present - if result is None: - return None - - if 'result' not in result: - return None - - if 'result' not in result['result']: - return None - - if 'value' not in result['result']['result']: - return None - - return result['result']['result']['value'] + runtime_exception = result.get('result', {}).get('exceptionDetails') + if runtime_exception: + raise ChromeScriptRuntimeException( + runtime_exception, + function_called=expression + ) + return result.get('result', {}).get('result', {}).get('value', None) def get_js_variable_value(self, variable_name): """ diff --git a/w3af/core/controllers/chrome/js/dom_analyzer.js b/w3af/core/controllers/chrome/js/dom_analyzer.js index 8b4ae2c106..9b113de676 100644 --- a/w3af/core/controllers/chrome/js/dom_analyzer.js +++ b/w3af/core/controllers/chrome/js/dom_analyzer.js @@ -1052,6 +1052,7 @@ var _DOMAnalyzer = _DOMAnalyzer || { clickOnSelector(exactSelector) { let element = document.querySelector(exactSelector); element.click(); + return 'success' }, sliceAndSerialize: function (filtered_event_listeners, start, count) { diff --git a/w3af/core/controllers/chrome/login/find_form/main.py b/w3af/core/controllers/chrome/login/find_form/main.py index 8d50f93b81..2e42e13c57 100644 --- a/w3af/core/controllers/chrome/login/find_form/main.py +++ b/w3af/core/controllers/chrome/login/find_form/main.py @@ -63,6 +63,6 @@ def find_forms(self, css_selectors=None): except Exception as e: msg = 'Form finder strategy %s raised exception: "%s" (did: %s)' args = (strategy.get_name(), - e, + repr(e), self.debugging_id) om.out.debug(msg % args) diff --git a/w3af/core/controllers/chrome/login/find_form/strategies/base_find_form_strategy.py b/w3af/core/controllers/chrome/login/find_form/strategies/base_find_form_strategy.py index 8d5b3f683d..6c635adc44 100644 --- a/w3af/core/controllers/chrome/login/find_form/strategies/base_find_form_strategy.py +++ b/w3af/core/controllers/chrome/login/find_form/strategies/base_find_form_strategy.py @@ -20,8 +20,8 @@ def prepare(self): """ form_activator_selector = self.exact_css_selectors.get('form_activator') if form_activator_selector: - func = 'window._DOMAnalyzer.clickOnSelector({})'.format( - form_activator_selector + func = 'window._DOMAnalyzer.clickOnSelector("{}")'.format( + form_activator_selector.replace('"', '\\"') ) result = self.chrome.js_runtime_evaluate(func) if result is None: From 8d520bc067455089a9fccac9c395f9114f521e8a Mon Sep 17 00:00:00 2001 From: Q_back Date: Wed, 10 Jun 2020 17:43:15 +0200 Subject: [PATCH 14/14] fix error when empty list was returned by querySelectorAll to InstrumentedChrome.focus() method --- w3af/core/controllers/chrome/instrumented/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/w3af/core/controllers/chrome/instrumented/main.py b/w3af/core/controllers/chrome/instrumented/main.py index 6ef3411fe2..9b3672aa95 100644 --- a/w3af/core/controllers/chrome/instrumented/main.py +++ b/w3af/core/controllers/chrome/instrumented/main.py @@ -425,9 +425,9 @@ def focus(self, selector): if result is None: return None - node_ids = result.get('result', {}).get('nodeIds', None) + node_ids = result.get('result', {}).get('nodeIds') - if node_ids is None: + if not node_ids: msg = ('The call to chrome.focus() failed.' ' CSS selector "%s" returned no nodes (did: %s)') args = (selector, self.debugging_id)