diff --git a/bbot/modules/aspnet_bin_exposure.py b/bbot/modules/aspnet_bin_exposure.py new file mode 100644 index 0000000000..9367058412 --- /dev/null +++ b/bbot/modules/aspnet_bin_exposure.py @@ -0,0 +1,80 @@ +from bbot.modules.base import BaseModule + + +class aspnet_bin_exposure(BaseModule): + watched_events = ["URL"] + produced_events = ["VULNERABILITY"] + flags = ["active", "safe", "web-thorough"] + meta = { + "description": "Check for ASP.NET Security Feature Bypasses (CVE-2023-36899 and CVE-2023-36560)", + "created_date": "2025-01-28", + "author": "@liquidsec", + } + + in_scope_only = True + test_dlls = [ + "Telerik.Web.UI.dll", + "Newtonsoft.Json.dll", + "System.Net.Http.dll", + "EntityFramework.dll", + "AjaxControlToolkit.dll", + ] + + @staticmethod + def normalize_url(url): + return str(url.rstrip("/") + "/").lower() + + def _incoming_dedup_hash(self, event): + return hash(self.normalize_url(event.data)) + + async def handle_event(self, event): + normalized_url = self.normalize_url(event.data) + 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###"]: + 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)}]" + ) + + 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) + + 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( + { + "severity": "HIGH", + "host": str(event.host), + "url": normalized_url, + "description": description, + }, + "VULNERABILITY", + event, + context="{module} detected IIS Bin Directory DLL Exposure vulnerability", + ) + return True + + async def filter_event(self, event): + if "dir" in event.tags: + return True + return False diff --git a/bbot/presets/web/dotnet-audit.yml b/bbot/presets/web/dotnet-audit.yml index 993d6dd198..581c26c7ab 100644 --- a/bbot/presets/web/dotnet-audit.yml +++ b/bbot/presets/web/dotnet-audit.yml @@ -12,6 +12,7 @@ modules: - telerik - ajaxpro - dotnetnuke + - aspnet_bin_exposure config: modules: 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 new file mode 100644 index 0000000000..ca14ff7d03 --- /dev/null +++ b/bbot/test/test_step_2/module_tests/test_module_aspnet_bin_exposure.py @@ -0,0 +1,73 @@ +from .base import ModuleTestBase +import re + + +class TestAspnetBinExposure(ModuleTestBase): + targets = ["http://127.0.0.1:8888"] + modules_overrides = ["httpx", "aspnet_bin_exposure"] + config_overrides = { + "modules": { + "aspnet_bin_exposure": { + "test_dlls": [ + "Newtonsoft.Json.dll", + ] + } + } + } + + 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) + + # Simulate failed DLL exposure (confirmation test) + expect_args = { + "method": "GET", + "uri": "/b/(S(X))in/oopsnotarealdll.dll/(S(X))/", + } + 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", + } + 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) + + # 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): + vulnerability_found = False + for e in events: + if e.type == "VULNERABILITY" and "IIS Bin Directory DLL Exposure" in e.data["description"]: + vulnerability_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 vulnerability_found, "No vulnerability event was found"