diff --git a/make_exe.py b/make_exe.py index 274a0c6f2..2ee112c9f 100644 --- a/make_exe.py +++ b/make_exe.py @@ -110,6 +110,7 @@ def build_file_list(results, dest, root, src=""): "wapitiCore.attack.mod_methods", "wapitiCore.attack.mod_nikto", "wapitiCore.attack.mod_permanentxss", + "wapitiCore.attack.mod_http_post", "wapitiCore.attack.mod_redirect", "wapitiCore.attack.mod_shellshock", "wapitiCore.attack.mod_sql", diff --git a/tests/attack/test_mod_http_post.py b/tests/attack/test_mod_http_post.py new file mode 100644 index 000000000..62f1d4b19 --- /dev/null +++ b/tests/attack/test_mod_http_post.py @@ -0,0 +1,116 @@ +from unittest.mock import Mock +from asyncio import Event + +import respx +import httpx +import pytest + +from wapitiCore.net.web import Request +from wapitiCore.net.crawler import AsyncCrawler +from wapitiCore.attack.mod_http_post import mod_http_post +from wapitiCore.language.vulnerability import _ +from tests import AsyncMock + + +@pytest.mark.asyncio +@respx.mock + +async def test_no_login_form(): + respx.get("http://perdu.com/").mock( + return_value=httpx.Response( + 200, + text="Vous Etes Perdu ?

Perdu sur l'Internet ?

\ +

Pas de panique, on va vous aider

\ +
    * <----- vous êtes ici
" + ) + ) + persister = AsyncMock() + + request = Request("http://perdu.com/") + request.path_id = 1 + # persister.requests.append(request) + + crawler = AsyncCrawler("http://perdu.com/") + + options = {"timeout": 10, "level": 2} + logger = Mock() + + module = mod_http_post(crawler, persister, logger, options, Event()) + module.verbose = 2 + + await module.attack(request) + + assert not persister.add_payload.call_count + await crawler.close() + +@pytest.mark.asyncio +@respx.mock +async def test_login_form_https(): + url = "https://perdu.com/" + body = """ + +
+ + +
+ + + """ + + respx.get(url).mock(return_value=httpx.Response(200, text=body)) + + persister = AsyncMock() + request = Request("https://perdu.com/") + request.path_id = 1 + # persister.requests.append(request) + + crawler = AsyncCrawler("https://perdu.com/") + + options = {"timeout": 10, "level": 2} + logger = Mock() + + module = mod_http_post(crawler, persister, logger, options, Event()) + module.verbose = 2 + + await module.attack(request) + assert not persister.add_payload.call_count + await crawler.close() + +@pytest.mark.asyncio +@respx.mock +async def test_login_form_http(): + url = "http://perdu.com/" + body = """ + +
+ + +
+ + + """ + + respx.get(url).mock(return_value=httpx.Response(200, text=body)) + + persister = AsyncMock() + request = Request( + "http://perdu.com/", + method="POST", + post_params=[["email", "wapiti2021@mailinator.com"], ["password", "Letm3in_"]], + ) + request.path_id = 1 + # persister.requests.append(request) + + crawler = AsyncCrawler("http://perdu.com/") + + options = {"timeout": 10, "level": 2} + logger = Mock() + + module = mod_http_post(crawler, persister, logger, options, Event()) + module.verbose = 2 + + await module.attack(request) + assert persister.add_payload.call_count + assert persister.add_payload.call_args_list[0][1]["module"] == "http_post" + assert persister.add_payload.call_args_list[0][1]["category"] == _("POST HTTP") + await crawler.close() diff --git a/wapitiCore/attack/attack.py b/wapitiCore/attack/attack.py index 2d899ce51..8d5187e77 100644 --- a/wapitiCore/attack/attack.py +++ b/wapitiCore/attack/attack.py @@ -58,7 +58,8 @@ "mod_redirect", "mod_xxe", "mod_wapp", - "mod_wp_enum" + "mod_wp_enum", + "mod_http_post" ] # Modules that will be used if option -m isn't used diff --git a/wapitiCore/attack/mod_http_post.py b/wapitiCore/attack/mod_http_post.py new file mode 100644 index 000000000..d1540f65b --- /dev/null +++ b/wapitiCore/attack/mod_http_post.py @@ -0,0 +1,32 @@ +from httpx import RequestError + +from wapitiCore.attack.attack import Attack +from wapitiCore.net.web import Request +from wapitiCore.language.vulnerability import MEDIUM_LEVEL, _ +from wapitiCore.definitions.http_post import NAME + + +# This module check the security of transported credentials of login forms +class mod_http_post(Attack): + """Check if credentials are transported on an encrypted channel.""" + name = "http_post" + + async def must_attack(self, request: Request): + # We leverage the fact that the crawler will fill password entries with a known placeholder + if "https://" in request.url: + return False + + return True + + async def attack(self, request: Request): + + if "Letm3in_" not in request.encoded_data + request.encoded_params: + return + self.finished = True + + await self.add_vuln_medium( + request_id=request.path_id, + category=NAME, + request=request, + info=_("Credentials transported over an Unencrypted Channel on : {0}").format(request.url) + ) diff --git a/wapitiCore/definitions/http_post.py b/wapitiCore/definitions/http_post.py new file mode 100644 index 000000000..13613729e --- /dev/null +++ b/wapitiCore/definitions/http_post.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from wapitiCore.language.language import _ + +TYPE = "vulnerability" +NAME = _("POST HTTP") +SHORT_NAME = NAME + +DESCRIPTION = _( + "The application configuration should ensure that SSL is used for all access controlled pages.\\n)." +) + " " + _( + "If an application uses SSL to guarantee confidential communication with client browsers, " +) + " " + _( + "the application configuration should make it impossible to view any access controlled page without SSL." +) + +SOLUTION = _( + "Force the use of HTTPS for all authentication requests" +) + +REFERENCES = [ +{ + "title": "OWASP: Insecure Transport", + "url": "https://owasp.org/www-community/vulnerabilities/Insecure_Transport" + }, + { + "title": "Acunetix: Insecure Authentication", + "url": "https://owasp.org/www-project-mobile-top-10/2016-risks/m4-insecure-authentication" + } +] diff --git a/wapitiCore/language_sources/en.po b/wapitiCore/language_sources/en.po index c597c561b..86e55e98c 100644 --- a/wapitiCore/language_sources/en.po +++ b/wapitiCore/language_sources/en.po @@ -175,6 +175,9 @@ msgstr "" "Permanent XSS vulnerability found in {0} by injecting the parameter {1} of " "{2}" +msgid "Credentials transported over an Unencrypted Channel on : {0}" +msgstr "Credentials transported over an Unencrypted Channel on : {0}" + msgid "Warning: Content-Security-Policy is present!" msgstr "Warning: Content-Security-Policy is present!" diff --git a/wapitiCore/language_sources/fr.po b/wapitiCore/language_sources/fr.po index 90cffada7..86528f257 100644 --- a/wapitiCore/language_sources/fr.po +++ b/wapitiCore/language_sources/fr.po @@ -177,6 +177,9 @@ msgstr "" "Vulnérabilité de XSS permanent trouvée dans {0} via une injection dans la " "paramètre {1} de {2}" +msgid "Credentials transported over an Unencrypted Channel on : {0}" +msgstr "Identifiants transportés sur un canal non chiffré sur : {0}" + msgid "Warning: Content-Security-Policy is present!" msgstr "Avertissement: L'entête Content-Security-Policy est présent!" diff --git a/wapitiCore/net/page.py b/wapitiCore/net/page.py index 1865249bf..5637008ed 100644 --- a/wapitiCore/net/page.py +++ b/wapitiCore/net/page.py @@ -908,8 +908,10 @@ def find_login_form(self): username_field_idx.append(i) elif input_type == "text" and ( - any(field_name in input_name for field_name in ["mail", "user", "login", "name"]) or - any(field_id in input_id for field_id in ["mail", "user", "login", "name"]) + any(field_name in input_name for field_name in \ + ["mail", "user", "login", "name", "id", "client", "nom"]) + or any(field_id in input_id for field_id in \ + ["mail", "user", "login", "name", "id", "client", "nom"]) ): username_field_idx.append(i)