Skip to content

Commit efe6c93

Browse files
committed
Add module http_post
Check if credentials are transported over an encrypted channel
1 parent 26d16d0 commit efe6c93

File tree

8 files changed

+202
-3
lines changed

8 files changed

+202
-3
lines changed

make_exe.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ def build_file_list(results, dest, root, src=""):
110110
"wapitiCore.attack.mod_methods",
111111
"wapitiCore.attack.mod_nikto",
112112
"wapitiCore.attack.mod_permanentxss",
113+
"wapitiCore.attack.mod_http_post",
113114
"wapitiCore.attack.mod_redirect",
114115
"wapitiCore.attack.mod_shellshock",
115116
"wapitiCore.attack.mod_sql",

tests/attack/test_mod_http_post.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
from unittest.mock import Mock
2+
from asyncio import Event
3+
4+
import respx
5+
import httpx
6+
import pytest
7+
8+
from wapitiCore.net.web import Request
9+
from wapitiCore.net.crawler import AsyncCrawler
10+
from wapitiCore.attack.mod_http_post import mod_http_post
11+
from wapitiCore.language.vulnerability import _
12+
from tests import AsyncMock
13+
14+
15+
@pytest.mark.asyncio
16+
@respx.mock
17+
18+
async def test_no_login_form():
19+
respx.get("http://perdu.com/").mock(
20+
return_value=httpx.Response(
21+
200,
22+
text="<html><head><title>Vous Etes Perdu ?</title></head><body><h1>Perdu sur l'Internet ?</h1> \
23+
<h2>Pas de panique, on va vous aider</h2> \
24+
<strong><pre> * <----- vous &ecirc;tes ici</pre></strong></body></html>"
25+
)
26+
)
27+
persister = AsyncMock()
28+
29+
request = Request("http://perdu.com/")
30+
request.path_id = 1
31+
# persister.requests.append(request)
32+
33+
crawler = AsyncCrawler("http://perdu.com/")
34+
35+
options = {"timeout": 10, "level": 2}
36+
logger = Mock()
37+
38+
module = mod_http_post(crawler, persister, logger, options, Event())
39+
module.verbose = 2
40+
41+
await module.attack(request)
42+
43+
assert not persister.add_payload.call_count
44+
await crawler.close()
45+
46+
@pytest.mark.asyncio
47+
@respx.mock
48+
async def test_login_form_https():
49+
url = "https://perdu.com/"
50+
body = """<html>
51+
<body>
52+
<form method="POST">
53+
<input type="text" name="username" />
54+
<input type="password" name="pass" />
55+
</form>
56+
</body>
57+
</html>
58+
"""
59+
60+
respx.get(url).mock(return_value=httpx.Response(200, text=body))
61+
62+
persister = AsyncMock()
63+
request = Request("https://perdu.com/")
64+
request.path_id = 1
65+
# persister.requests.append(request)
66+
67+
crawler = AsyncCrawler("https://perdu.com/")
68+
69+
options = {"timeout": 10, "level": 2}
70+
logger = Mock()
71+
72+
module = mod_http_post(crawler, persister, logger, options, Event())
73+
module.verbose = 2
74+
75+
await module.attack(request)
76+
assert not persister.add_payload.call_count
77+
await crawler.close()
78+
79+
@pytest.mark.asyncio
80+
@respx.mock
81+
async def test_login_form_http():
82+
url = "http://perdu.com/"
83+
body = """<html>
84+
<body>
85+
<form method="POST">
86+
<input type="text" name="username" />
87+
<input type="password" name="pass" />
88+
</form>
89+
</body>
90+
</html>
91+
"""
92+
93+
respx.get(url).mock(return_value=httpx.Response(200, text=body))
94+
95+
persister = AsyncMock()
96+
request = Request("http://perdu.com/")
97+
request.path_id = 1
98+
# persister.requests.append(request)
99+
100+
crawler = AsyncCrawler("http://perdu.com/")
101+
102+
options = {"timeout": 10, "level": 2}
103+
logger = Mock()
104+
105+
module = mod_http_post(crawler, persister, logger, options, Event())
106+
module.verbose = 2
107+
108+
await module.attack(request)
109+
assert persister.add_payload.call_count
110+
assert persister.add_payload.call_args_list[0][1]["module"] == "http_post"
111+
assert persister.add_payload.call_args_list[0][1]["category"] == _("POST HTTP")
112+
await crawler.close()

wapitiCore/attack/attack.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@
5858
"mod_redirect",
5959
"mod_xxe",
6060
"mod_wapp",
61-
"mod_wp_enum"
61+
"mod_wp_enum",
62+
"mod_http_post"
6263
]
6364

6465
# Modules that will be used if option -m isn't used

wapitiCore/attack/mod_http_post.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from httpx import RequestError
2+
3+
from wapitiCore.attack.attack import Attack
4+
from wapitiCore.net.web import Request
5+
from wapitiCore.language.vulnerability import MEDIUM_LEVEL, _
6+
from wapitiCore.definitions.http_post import NAME
7+
8+
9+
# This module check the security of transported credentials of login forms
10+
class mod_http_post(Attack):
11+
"""Check if credentials are transported on an encrypted channel."""
12+
name = "http_post"
13+
14+
async def must_attack(self, request: Request):
15+
# We leverage the fact that the crawler will fill password entries with a known placeholder
16+
if "Letm3in_" not in request.encoded_data + request.encoded_params:
17+
return False
18+
19+
# We may want to remove this but if not available fallback to target URL
20+
if not request.referer:
21+
return False
22+
23+
return True
24+
25+
async def attack(self, request: Request):
26+
try:
27+
page = await self.crawler.async_send(request, follow_redirects=True)
28+
except RequestError:
29+
self.network_errors += 1
30+
return
31+
32+
login_form, username_field, password_field = page.find_login_form()
33+
if not login_form:
34+
return
35+
36+
self.finished = True
37+
38+
if "http://" in login_form.url:
39+
self.log_red(_("Credentials transported over an Unencrypted Channel on : {0}"), login_form.url)
40+
41+
await self.add_vuln_medium(
42+
request_id=request.path_id,
43+
category=NAME,
44+
request=request,
45+
info=_("Credentials transported over an Unencrypted Channel on : {0}").format(login_form.url)
46+
)

wapitiCore/definitions/http_post.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
from wapitiCore.language.language import _
5+
6+
TYPE = "vulnerability"
7+
NAME = _("POST HTTP")
8+
SHORT_NAME = NAME
9+
10+
DESCRIPTION = _(
11+
"The application configuration should ensure that SSL is used for all access controlled pages.\\n)."
12+
) + " " + _(
13+
"If an application uses SSL to guarantee confidential communication with client browsers, "
14+
) + " " + _(
15+
"the application configuration should make it impossible to view any access controlled page without SSL."
16+
)
17+
18+
SOLUTION = _(
19+
"Force the use of HTTPS for all authentication requests"
20+
)
21+
22+
REFERENCES = [
23+
{
24+
"title": "OWASP: Insecure Transport",
25+
"url": "https://owasp.org/www-community/vulnerabilities/Insecure_Transport"
26+
},
27+
{
28+
"title": "Acunetix: Insecure Authentication",
29+
"url": "https://owasp.org/www-project-mobile-top-10/2016-risks/m4-insecure-authentication"
30+
}
31+
]

wapitiCore/language_sources/en.po

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,9 @@ msgstr ""
175175
"Permanent XSS vulnerability found in {0} by injecting the parameter {1} of "
176176
"{2}"
177177

178+
msgid "Credentials transported over an Unencrypted Channel on : {0}"
179+
msgstr "Credentials transported over an Unencrypted Channel on : {0}"
180+
178181
msgid "Warning: Content-Security-Policy is present!"
179182
msgstr "Warning: Content-Security-Policy is present!"
180183

wapitiCore/language_sources/fr.po

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,9 @@ msgstr ""
177177
"Vulnérabilité de XSS permanent trouvée dans {0} via une injection dans la "
178178
"paramètre {1} de {2}"
179179

180+
msgid "Credentials transported over an Unencrypted Channel on : {0}"
181+
msgstr "Identifiants transportés sur un canal non chiffré sur : {0}"
182+
180183
msgid "Warning: Content-Security-Policy is present!"
181184
msgstr "Avertissement: L'entête Content-Security-Policy est présent!"
182185

wapitiCore/net/page.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -908,8 +908,10 @@ def find_login_form(self):
908908
username_field_idx.append(i)
909909

910910
elif input_type == "text" and (
911-
any(field_name in input_name for field_name in ["mail", "user", "login", "name"]) or
912-
any(field_id in input_id for field_id in ["mail", "user", "login", "name"])
911+
any(field_name in input_name for field_name in \
912+
["mail", "user", "login", "name", "id", "client", "nom"])
913+
or any(field_id in input_id for field_id in \
914+
["mail", "user", "login", "name", "id", "client", "nom"])
913915
):
914916
username_field_idx.append(i)
915917

0 commit comments

Comments
 (0)