From 0098faf860d8f323523da2e1ed594fbde7096182 Mon Sep 17 00:00:00 2001 From: liquidsec Date: Tue, 28 Jan 2025 11:34:54 -0500 Subject: [PATCH 1/8] new module: aspnet_bin_exposure --- bbot/modules/aspnet_bin_exposure.py | 52 +++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 bbot/modules/aspnet_bin_exposure.py diff --git a/bbot/modules/aspnet_bin_exposure.py b/bbot/modules/aspnet_bin_exposure.py new file mode 100644 index 0000000000..676339c3d5 --- /dev/null +++ b/bbot/modules/aspnet_bin_exposure.py @@ -0,0 +1,52 @@ +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"] + + async def setup(self): + self.scanned_tracker = set() + return True + + @staticmethod + def normalize_url(url): + return str(url.rstrip("/") + "/").lower() + + async def handle_event(self, event): + + normalized_url = self.normalize_url(event.data) + self.scanned_tracker.add(normalized_url) + 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)}]") + 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: + if self.normalize_url(event.data) not in self.scanned_tracker: + return True + return False + return False From 1dc671647479aea9f1dbab2d536f2cb9f2847c1f Mon Sep 17 00:00:00 2001 From: liquidsec Date: Tue, 28 Jan 2025 11:35:59 -0500 Subject: [PATCH 2/8] adding aspnet_bin_exposure to dotnet-audit preset --- bbot/presets/web/dotnet-audit.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/bbot/presets/web/dotnet-audit.yml b/bbot/presets/web/dotnet-audit.yml index b1cd8e9cac..0186cd3ffe 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: From 0f93014cab59a245da00fee26b560e851cc7e7dd Mon Sep 17 00:00:00 2001 From: liquidsec Date: Tue, 28 Jan 2025 23:02:39 -0500 Subject: [PATCH 3/8] formatting --- bbot/modules/aspnet_bin_exposure.py | 31 +++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/bbot/modules/aspnet_bin_exposure.py b/bbot/modules/aspnet_bin_exposure.py index 676339c3d5..f387bfb7c5 100644 --- a/bbot/modules/aspnet_bin_exposure.py +++ b/bbot/modules/aspnet_bin_exposure.py @@ -1,5 +1,6 @@ from bbot.modules.base import BaseModule + class aspnet_bin_exposure(BaseModule): watched_events = ["URL"] produced_events = ["VULNERABILITY"] @@ -11,7 +12,13 @@ class aspnet_bin_exposure(BaseModule): } in_scope_only = True - test_dlls = ["Telerik.Web.UI.dll", "Newtonsoft.Json.dll", "System.Net.Http.dll", "EntityFramework.dll", "AjaxControlToolkit.dll"] + test_dlls = [ + "Telerik.Web.UI.dll", + "Newtonsoft.Json.dll", + "System.Net.Http.dll", + "EntityFramework.dll", + "AjaxControlToolkit.dll", + ] async def setup(self): self.scanned_tracker = set() @@ -22,22 +29,30 @@ def normalize_url(url): return str(url.rstrip("/") + "/").lower() async def handle_event(self, event): - normalized_url = self.normalize_url(event.data) self.scanned_tracker.add(normalized_url) 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)}" + 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"] + ): + 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)}]" + ) 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}, + { + "severity": "HIGH", + "host": str(event.host), + "url": normalized_url, + "description": description, + }, "VULNERABILITY", event, context="{module} detected IIS Bin Directory DLL Exposure vulnerability", From cea9cff752415c7497bb511ae2f75b1dc66d75e2 Mon Sep 17 00:00:00 2001 From: liquidsec Date: Wed, 29 Jan 2025 23:08:55 -0500 Subject: [PATCH 4/8] add confirmation check --- bbot/modules/aspnet_bin_exposure.py | 40 +++++++++++++++++++---------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/bbot/modules/aspnet_bin_exposure.py b/bbot/modules/aspnet_bin_exposure.py index f387bfb7c5..153559735d 100644 --- a/bbot/modules/aspnet_bin_exposure.py +++ b/bbot/modules/aspnet_bin_exposure.py @@ -45,19 +45,33 @@ async def handle_event(self, event): 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)}]" ) - 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 + + + + 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: From 24f1669604130eb7b53495503044d2c9a67e4661 Mon Sep 17 00:00:00 2001 From: liquidsec Date: Fri, 13 Jun 2025 10:49:34 -0400 Subject: [PATCH 5/8] ruff format --- bbot/modules/aspnet_bin_exposure.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/bbot/modules/aspnet_bin_exposure.py b/bbot/modules/aspnet_bin_exposure.py index 153559735d..7be4da29a7 100644 --- a/bbot/modules/aspnet_bin_exposure.py +++ b/bbot/modules/aspnet_bin_exposure.py @@ -46,23 +46,26 @@ async def handle_event(self, event): 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_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"] - )): + 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", + "severity": "HIGH", "host": str(event.host), "url": normalized_url, "description": description, From 5f9e9b5dae3f60d059467ec5d716f253fa1de150 Mon Sep 17 00:00:00 2001 From: liquidsec Date: Fri, 13 Jun 2025 11:56:04 -0400 Subject: [PATCH 6/8] adding module test --- .../module_tests/test_aspnet_bin_exposure.py | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 bbot/test/test_step_2/module_tests/test_aspnet_bin_exposure.py diff --git a/bbot/test/test_step_2/module_tests/test_aspnet_bin_exposure.py b/bbot/test/test_step_2/module_tests/test_aspnet_bin_exposure.py new file mode 100644 index 0000000000..ca14ff7d03 --- /dev/null +++ b/bbot/test/test_step_2/module_tests/test_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" From 4fe7a7444b806e0a6ca000e4d7330be8d2863b3d Mon Sep 17 00:00:00 2001 From: liquidsec Date: Fri, 13 Jun 2025 12:33:26 -0400 Subject: [PATCH 7/8] correct naming of test --- ..._aspnet_bin_exposure.py => test_module_aspnet_bin_exposure.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename bbot/test/test_step_2/module_tests/{test_aspnet_bin_exposure.py => test_module_aspnet_bin_exposure.py} (100%) diff --git a/bbot/test/test_step_2/module_tests/test_aspnet_bin_exposure.py b/bbot/test/test_step_2/module_tests/test_module_aspnet_bin_exposure.py similarity index 100% rename from bbot/test/test_step_2/module_tests/test_aspnet_bin_exposure.py rename to bbot/test/test_step_2/module_tests/test_module_aspnet_bin_exposure.py From 41586dce39f08012b2f33e99d8e12d280b76ea7a Mon Sep 17 00:00:00 2001 From: liquidsec Date: Mon, 16 Jun 2025 12:10:51 -0400 Subject: [PATCH 8/8] use integrated deduping --- bbot/modules/aspnet_bin_exposure.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/bbot/modules/aspnet_bin_exposure.py b/bbot/modules/aspnet_bin_exposure.py index 7be4da29a7..9367058412 100644 --- a/bbot/modules/aspnet_bin_exposure.py +++ b/bbot/modules/aspnet_bin_exposure.py @@ -20,17 +20,15 @@ class aspnet_bin_exposure(BaseModule): "AjaxControlToolkit.dll", ] - async def setup(self): - self.scanned_tracker = set() - return True - @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) - self.scanned_tracker.add(normalized_url) 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)}" @@ -78,7 +76,5 @@ async def handle_event(self, event): async def filter_event(self, event): if "dir" in event.tags: - if self.normalize_url(event.data) not in self.scanned_tracker: - return True - return False + return True return False