Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed 'escaping the escape' xss technique false positives #2173

Merged
merged 7 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 54 additions & 17 deletions bbot/modules/lightfuzz_submodules/xss.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,58 @@
from .base import BaseLightfuzz

import re

import regex as re

class XSSLightfuzz(BaseLightfuzz):
def determine_context(self, cookies, html, random_string):
async def determine_context(self, cookies, html, random_string):
between_tags = False
in_tag_attribute = False
in_javascript = False

between_tags_regex = re.compile(rf"<(\/?\w+)[^>]*>.*?{random_string}.*?<\/?\w+>")
in_tag_attribute_regex = re.compile(rf'<(\w+)\s+[^>]*?(\w+)="([^"]*?{random_string}[^"]*?)"[^>]*>')
in_javascript_regex = re.compile(
rf"<script\b[^>]*>(?:(?!<\/script>)[\s\S])*?{random_string}(?:(?!<\/script>)[\s\S])*?<\/script>"
)
in_javascript_regex = re.compile(rf"<script\b[^>]*>[^<]*(?:<(?!\/script>)[^<]*)*{random_string}[^<]*(?:<(?!\/script>)[^<]*)*<\/script>")

between_tags_match = re.search(between_tags_regex, html)
between_tags_match = await self.lightfuzz.helpers.re.search(between_tags_regex, html)
if between_tags_match:
between_tags = True

in_tag_attribute_match = re.search(in_tag_attribute_regex, html)
in_tag_attribute_match = await self.lightfuzz.helpers.re.search(in_tag_attribute_regex, html)
if in_tag_attribute_match:
in_tag_attribute = True

in_javascript_regex = re.search(in_javascript_regex, html)
if in_javascript_regex:
in_javascript_match = await self.lightfuzz.helpers.re.search(in_javascript_regex, html)
if in_javascript_match:
in_javascript = True

return between_tags, in_tag_attribute, in_javascript

async def determine_javascript_quote_context(self, target, text):
# Define and compile regex patterns for double and single quotes
quote_patterns = {
"double": re.compile(f'"[^"]*{target}[^"]*"'),
"single": re.compile(f"'[^']*{target}[^']*'")
}

# Split the text by semicolons to isolate JavaScript statements
statements = text.split(";")

def is_balanced(section, target_index, quote_char):
left = section[:target_index]
right = section[target_index + len(target):]
return left.count(quote_char) % 2 == 0 and right.count(quote_char) % 2 == 0

for statement in statements:
for quote_type, pattern in quote_patterns.items():
match = await self.lightfuzz.helpers.re.search(pattern, statement)
if match:
context = match.group(0)
target_index = context.find(target)
opposite_quote = "'" if quote_type == "double" else '"'
if is_balanced(context, target_index, opposite_quote):
return quote_type

return "outside"

async def check_probe(self, cookies, probe, match, context):
probe_result = await self.standard_probe(self.event.data["type"], cookies, probe)
if probe_result and match in probe_result.text:
Expand Down Expand Up @@ -67,7 +91,7 @@ async def fuzz(self):
if not reflection or reflection is False:
return

between_tags, in_tag_attribute, in_javascript = self.determine_context(cookies, reflection_probe_result.text, random_string)
between_tags, in_tag_attribute, in_javascript = await self.determine_context(cookies, reflection_probe_result.text, random_string)
self.lightfuzz.debug(
f"determine_context returned: between_tags [{between_tags}], in_tag_attribute [{in_tag_attribute}], in_javascript [{in_javascript}]"
)
Expand All @@ -93,10 +117,23 @@ async def fuzz(self):
in_javascript_probe = rf"</script><script>{random_string}</script>"
result = await self.check_probe(cookies, in_javascript_probe, in_javascript_probe, "In Javascript")
if result is False:
in_javasscript_escape_probe = rf"a\';zzzzz({random_string})\\"
in_javasscript_escape_match = rf"a\\';zzzzz({random_string})\\"
await self.check_probe(cookies,
in_javasscript_escape_probe,
in_javasscript_escape_match,
"In Javascript (escaping the escape character)",
quote_context = await self.determine_javascript_quote_context(random_string, reflection_probe_result.text)

# Skip the test if the context is outside
if quote_context == "outside":
return

# Update probes based on the quote context
if quote_context == "single":
in_javascript_escape_probe = rf"a\';zzzzz({random_string})\\"
in_javascript_escape_match = rf"a\\';zzzzz({random_string})\\"
elif quote_context == "double":
in_javascript_escape_probe = rf'a\";zzzzz({random_string})\\'
in_javascript_escape_match = rf'a\\";zzzzz({random_string})\\'

await self.check_probe(
cookies,
in_javascript_escape_probe,
in_javascript_escape_match,
f"In Javascript (escaping the escape character, {quote_context} quote)"
)
135 changes: 135 additions & 0 deletions bbot/test/test_step_2/module_tests/test_module_lightfuzz.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import json
import re
import base64
import logging

from .base import ModuleTestBase, tempwordlist
from werkzeug.wrappers import Response
from urllib.parse import unquote, quote

import xml.etree.ElementTree as ET

from .test_module_paramminer_getparams import TestParamminer_Getparams
from .test_module_paramminer_headers import helper


# Path Traversal single dot tolerance
class Test_Lightfuzz_path_singledot(ModuleTestBase):
Expand Down Expand Up @@ -1499,3 +1503,134 @@ def check(self, module_test, events):
assert web_parameter_extracted, "Web parameter was not extracted"
assert cryptographic_parameter_finding, "Cryptographic parameter not detected"
assert padding_oracle_detected, "Padding oracle vulnerability was not detected"


class Test_Lightfuzz_XSS_jsquotecontext(ModuleTestBase):
targets = ["http://127.0.0.1:8888"]
modules_overrides = ["httpx", "lightfuzz", "excavate", "paramminer_getparams"]
config_overrides = {
"interactsh_disable": True,
"modules": {
"lightfuzz": {"enabled_submodules": ["xss"]},
"paramminer_getparams": {"wordlist": tempwordlist(["junk", "input"]), "recycle_words": True},
},
}

def request_handler(self, request):
# Decode the query string
qs = str(request.query_string.decode())
default_output = """
<html>
<form action="/" method="get">
<input type="text" name="input" value="default">
<input type="submit" value="Submit">
</form>
</html>
"""

if "input=" in qs:
# Split the query string to isolate the 'input' parameter
params = qs.split("&")
input_value = None
for param in params:
if param.startswith("input="):
input_value = param.split("=")[1]
break

if input_value:
# Simulate flawed escaping
sanitized_input = input_value.replace('"', '\\"').replace("'", "\\'")
sanitized_input = sanitized_input.replace('<', '%3C').replace('>', '%3E')

# Construct the reflected block with the sanitized input
reflected_block = f"""
<html>
<script>
let userInput = '{sanitized_input}';
console.log(userInput);
</script>
</html>
"""
return Response(reflected_block, status=200)

return Response(default_output, status=200)

async def setup_after_prep(self, module_test):
module_test.scan.modules["paramminer_getparams"].rand_string = lambda *args, **kwargs: "AAAAAAAAAAAAAA"
module_test.monkeypatch.setattr(
helper.HttpCompare, "gen_cache_buster", lambda *args, **kwargs: {"AAAAAA": "1"}
)
expect_args = re.compile("/")
module_test.set_expect_requests_handler(expect_args=expect_args, request_handler=self.request_handler)

def check(self, module_test, events):
web_parameter_emitted = False
xss_finding_emitted = False

for e in events:
if e.type == "WEB_PARAMETER":
if "[Paramminer] Getparam: [input] Reasons: [body] Reflection: [True]" in e.data["description"]:
web_parameter_emitted = True

if e.type == "FINDING":
if "Possible Reflected XSS. Parameter: [input] Context: [In Javascript (escaping the escape character, single quote)] Parameter Type: [GETPARAM]":
xss_finding_emitted = True

assert web_parameter_emitted, "WEB_PARAMETER for was not emitted"
assert xss_finding_emitted, "XSS FINDING not emitted"


class Test_Lightfuzz_XSS_jsquotecontext_doublequote(Test_Lightfuzz_XSS_jsquotecontext):

def request_handler(self, request):
qs = str(request.query_string.decode())
default_output = """
<html>
<form action="/" method="get">
<input type="text" name="input" value="default">
<input type="submit" value="Submit">
</form>
</html>
"""

if "input=" in qs:
params = qs.split("&")
input_value = None
for param in params:
if param.startswith("input="):
input_value = param.split("=")[1]
break

if input_value:
# Simulate flawed escaping with opposite quotes
sanitized_input = input_value.replace("'", "\\'").replace('"', '\\"')
sanitized_input = sanitized_input.replace('<', '%3C').replace('>', '%3E')

reflected_block = f"""
<html>
<script>
let userInput = "{sanitized_input}";
console.log(userInput);
</script>
</html>
"""
return Response(reflected_block, status=200)

return Response(default_output, status=200)


def check(self, module_test, events):
web_parameter_emitted = False
xss_finding_emitted = False

for e in events:
if e.type == "WEB_PARAMETER":
if "[Paramminer] Getparam: [input] Reasons: [body] Reflection: [True]" in e.data["description"]:
web_parameter_emitted = True

if e.type == "FINDING":
if "Possible Reflected XSS. Parameter: [input] Context: [In Javascript (escaping the escape character, double quote)] Parameter Type: [GETPARAM]":
xss_finding_emitted = True

assert web_parameter_emitted, "WEB_PARAMETER for was not emitted"
assert xss_finding_emitted, "XSS FINDING not emitted"
2 changes: 1 addition & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading