diff --git a/bbot/modules/aspnet_bin_exposure.py b/bbot/modules/aspnet_bin_exposure.py index 0198179b0c..fb671cd2d9 100644 --- a/bbot/modules/aspnet_bin_exposure.py +++ b/bbot/modules/aspnet_bin_exposure.py @@ -12,6 +12,8 @@ class aspnet_bin_exposure(BaseModule): } in_scope_only = True + _module_threads = 2 + test_dlls = [ "Telerik.Web.UI.dll", "Newtonsoft.Json.dll", @@ -19,6 +21,10 @@ class aspnet_bin_exposure(BaseModule): "EntityFramework.dll", "AjaxControlToolkit.dll", ] + _techniques = [ + "b/(S(X))in/###DLL_PLACEHOLDER###/(S(X))/", + "(S(X))/b/(S(X))in/###DLL_PLACEHOLDER###", + ] @staticmethod def normalize_url(url): @@ -27,54 +33,52 @@ def normalize_url(url): def _incoming_dedup_hash(self, event): return hash(self.normalize_url(event.url)) + @staticmethod + def _is_dll_download(response): + return ( + response is not None + and response.status_code == 200 + and "content-type" in response.headers + and "application/x-msdownload" in response.headers["content-type"] + ) + async def handle_event(self, event): normalized_url = self.normalize_url(event.url) + kwargs = {"method": "GET", "allow_redirects": False, "timeout": 10} + + probes = [] for test_dll in self.test_dlls: - for technique in ["b/(S(X))in/###DLL_PLACEHOLDER###/(S(X))/", "(S(X))/b/(S(X))in/###DLL_PLACEHOLDER###"]: + for technique in self._techniques: test_url = f"{normalized_url}{technique.replace('###DLL_PLACEHOLDER###', test_dll)}" - self.debug(f"Sending test URL: [{test_url}]") - kwargs = {"method": "GET", "allow_redirects": False, "timeout": 10} - test_result = await self.helpers.request(test_url, **kwargs) - if test_result: - if test_result.status_code == 200 and ( - "content-type" in test_result.headers - and "application/x-msdownload" in test_result.headers["content-type"] - ): - self.debug( - f"Got positive result for probe with test url: [{test_url}]. Status Code: [{test_result.status_code}] Content Length: [{len(test_result.content)}]" - ) + probes.append((test_url, kwargs, technique)) + + async for test_url, test_result, technique in self.helpers.request_batch_stream(probes, threads=10): + if not self._is_dll_download(test_result): + continue + + self.debug( + f"Got positive result for probe with test url: [{test_url}]. Status Code: [{test_result.status_code}] Content Length: [{len(test_result.content)}]" + ) - if test_result.status_code == 200 and ( - "content-type" in test_result.headers - and "application/x-msdownload" in test_result.headers["content-type"] - ): - confirm_url = ( - f"{normalized_url}{technique.replace('###DLL_PLACEHOLDER###', 'oopsnotarealdll.dll')}" - ) - confirm_result = await self.helpers.request(confirm_url, **kwargs) + confirm_url = f"{normalized_url}{technique.replace('###DLL_PLACEHOLDER###', 'oopsnotarealdll.dll')}" + confirm_result = await self.helpers.request(confirm_url, **kwargs) - if confirm_result and ( - confirm_result.status_code != 200 - or not ( - "content-type" in confirm_result.headers - and "application/x-msdownload" in confirm_result.headers["content-type"] - ) - ): - description = f"IIS Bin Directory DLL Exposure. Detection Url: [{test_url}]" - await self.emit_event( - { - "name": "IIS Bin Directory DLL Exposure", - "severity": "HIGH", - "confidence": "HIGH", - "host": str(event.host), - "url": normalized_url, - "description": description, - }, - "FINDING", - event, - context="{module} detected IIS Bin Directory DLL Exposure vulnerability", - ) - return True + if confirm_result and not self._is_dll_download(confirm_result): + description = f"IIS Bin Directory DLL Exposure. Detection Url: [{test_url}]" + await self.emit_event( + { + "name": "IIS Bin Directory DLL Exposure", + "severity": "HIGH", + "confidence": "HIGH", + "host": str(event.host), + "url": normalized_url, + "description": description, + }, + "FINDING", + event, + context="{module} detected IIS Bin Directory DLL Exposure vulnerability", + ) + return True async def filter_event(self, event): if "dir" in event.tags: diff --git a/bbot/test/test_step_2/module_tests/test_module_aspnet_bin_exposure.py b/bbot/test/test_step_2/module_tests/test_module_aspnet_bin_exposure.py index e53e0c4dc2..2af603865f 100644 --- a/bbot/test/test_step_2/module_tests/test_module_aspnet_bin_exposure.py +++ b/bbot/test/test_step_2/module_tests/test_module_aspnet_bin_exposure.py @@ -3,35 +3,76 @@ class TestAspnetBinExposure(ModuleTestBase): + """Only technique 1 is vulnerable; technique 2 returns 404. + + A tracker mis-correlation would either build the confirm URL from the + wrong technique pattern (hitting the 404 fallback) or attribute technique + 2's 404 result to technique 1 (skipping detection). Either way the + assertion on the Detection Url pattern would fail. + """ + targets = ["http://127.0.0.1:8888"] modules_overrides = ["http", "aspnet_bin_exposure"] async def setup_before_prep(self, module_test): - # Simulate successful DLL exposure - expect_args = { - "method": "GET", - "uri": "/b/(S(X))in/Newtonsoft.Json.dll/(S(X))/", - } - respond_args = { - "status": 200, - "headers": {"content-type": "application/x-msdownload"}, - "response_data": b"MZ\x90\x00\x03\x00\x00\x00", - } - module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) + # Technique 1: vulnerable (200 + DLL content-type) + module_test.set_expect_requests( + expect_args={"method": "GET", "uri": "/b/(S(X))in/Newtonsoft.Json.dll/(S(X))/"}, + respond_args={ + "status": 200, + "headers": {"content-type": "application/x-msdownload"}, + "response_data": b"MZ\x90\x00\x03\x00\x00\x00", + }, + ) - # Simulate failed DLL exposure (confirmation test) - expect_args = { - "method": "GET", - "uri": "/b/(S(X))in/oopsnotarealdll.dll/(S(X))/", - } + # Technique 1: confirm (fake DLL -> 404 = genuine vulnerability) + module_test.set_expect_requests( + expect_args={"method": "GET", "uri": "/b/(S(X))in/oopsnotarealdll.dll/(S(X))/"}, + respond_args={"status": 404}, + ) + + # Everything else (technique 2 probes, other DLLs) -> 404 + module_test.set_expect_requests( + expect_args={"uri": re.compile(r"^/.*$")}, + respond_args={"status": 404}, + ) + + def check(self, module_test, events): + findings = [ + e for e in events if e.type == "FINDING" and "IIS Bin Directory DLL Exposure" in e.data["description"] + ] + assert len(findings) == 1, f"Expected exactly 1 finding, got {len(findings)}" + finding = findings[0] + assert finding.data["severity"] == "HIGH" + assert "b/(S(X))in/" in finding.data["description"], ( + f"Detection Url should use technique 1 pattern, got: {finding.data['description']}" + ) + + +class TestAspnetBinExposure_DeadHost(ModuleTestBase): + """Dead host returns 404 for everything -- no FINDING should be emitted.""" + + targets = ["http://127.0.0.1:8888"] + modules_overrides = ["http", "aspnet_bin_exposure"] + + async def setup_before_prep(self, module_test): + expect_args = {"uri": re.compile(r"^/.*$")} respond_args = {"status": 404} module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) - # Simulate alternative technique - expect_args = { - "method": "GET", - "uri": "/(S(X))/b/(S(X))in/Newtonsoft.Json.dll", - } + def check(self, module_test, events): + findings = [e for e in events if e.type == "FINDING"] + assert len(findings) == 0, f"Dead host should not produce findings, got: {findings}" + + +class TestAspnetBinExposure_FalsePositive(ModuleTestBase): + """Host serves DLLs for everything (including the fake DLL) -- no FINDING should be emitted.""" + + targets = ["http://127.0.0.1:8888"] + modules_overrides = ["http", "aspnet_bin_exposure"] + + async def setup_before_prep(self, module_test): + expect_args = {"uri": re.compile(r"^/.*$")} respond_args = { "status": 200, "headers": {"content-type": "application/x-msdownload"}, @@ -39,26 +80,8 @@ async def setup_before_prep(self, module_test): } module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) - # Simulate failed alternative technique (confirmation test) - expect_args = { - "method": "GET", - "uri": "/(S(X))/b/(S(X))in/oopsnotarealdll.dll", - } - respond_args = {"status": 404} - module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) - - # Fallback for any other requests - expect_args = {"uri": re.compile(r"^/.*$")} - respond_args = {"status": 404} - module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) - def check(self, module_test, events): - finding_found = False - for e in events: - if e.type == "FINDING" and "IIS Bin Directory DLL Exposure" in e.data["description"]: - finding_found = True - assert e.data["severity"] == "HIGH", "Vulnerability severity should be HIGH" - assert "Detection Url" in e.data["description"], "Description should include detection URL" - break - - assert finding_found, "No finding event was found" + findings = [e for e in events if e.type == "FINDING"] + assert len(findings) == 0, ( + f"Host that serves DLLs for everything (including fake DLL) should not produce findings, got: {findings}" + )