Skip to content
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
88 changes: 46 additions & 42 deletions bbot/modules/aspnet_bin_exposure.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,19 @@ class aspnet_bin_exposure(BaseModule):
}

in_scope_only = True
_module_threads = 2

test_dlls = [
"Telerik.Web.UI.dll",
"Newtonsoft.Json.dll",
"System.Net.Http.dll",
"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):
Expand All @@ -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:
Expand Down
109 changes: 66 additions & 43 deletions bbot/test/test_step_2/module_tests/test_module_aspnet_bin_exposure.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,62 +3,85 @@


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"},
"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):
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}"
)