Skip to content

Commit

Permalink
Merge pull request #2228 from blacklanternsecurity/dev
Browse files Browse the repository at this point in the history
Dev -> Stable 2.3.2
  • Loading branch information
TheTechromancer authored Jan 31, 2025
2 parents 1989067 + 703a313 commit fd544e9
Show file tree
Hide file tree
Showing 9 changed files with 141 additions and 59 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ jobs:
token: ${{ secrets.BBOT_DOCS_UPDATER_PAT }}
- uses: actions/setup-python@v5
with:
python-version: "3.x"
python-version: "3.11"
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
- uses: actions/cache@v4
with:
Expand Down
89 changes: 57 additions & 32 deletions bbot/modules/telerik.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@


class telerik(BaseModule):
"""
Test for endpoints associated with Telerik.Web.UI.dll
Telerik.Web.UI.WebResource.axd (CVE-2017-11317)
Telerik.Web.UI.DialogHandler.aspx (CVE-2017-9248)
Telerik.Web.UI.SpellCheckHandler.axd (associated with CVE-2017-9248)
ChartImage.axd (CVE-2019-19790)
For the Telerik Report Server vulnerability (CVE-2024-4358) Use the Nuclei Template: (https://github.com/projectdiscovery/nuclei-templates/blob/main/http/cves/2024/CVE-2024-4358.yaml)
With exploit_RAU_crypto enabled, the module will attempt to exploit CVE-2017-11317. THIS WILL UPLOAD A (benign) FILE IF SUCCESSFUL.
Will dedupe to host by default (running against first received URL). With include_subdirs enabled, will run against every directory.
"""

watched_events = ["URL", "HTTP_RESPONSE"]
produced_events = ["VULNERABILITY", "FINDING"]
flags = ["active", "aggressive", "web-thorough"]
Expand Down Expand Up @@ -139,8 +154,11 @@ class telerik(BaseModule):

RAUConfirmed = []

options = {"exploit_RAU_crypto": False}
options_desc = {"exploit_RAU_crypto": "Attempt to confirm any RAU AXD detections are vulnerable"}
options = {"exploit_RAU_crypto": False, "include_subdirs": False}
options_desc = {
"exploit_RAU_crypto": "Attempt to confirm any RAU AXD detections are vulnerable",
"include_subdirs": "Include subdirectories in the scan (off by default)", # will create many finding events if used in conjunction with web spider or ffuf
}

in_scope_only = True

Expand All @@ -162,19 +180,33 @@ class telerik(BaseModule):

_module_threads = 5

@staticmethod
def normalize_url(url):
return str(url.rstrip("/") + "/").lower()

def _incoming_dedup_hash(self, event):
if event.type == "URL":
return hash(event.host)
else:
return hash(event.data["url"])
if self.config.get("include_subdirs") is True:
return hash(f"{event.type}{self.normalize_url(event.data)}")
else:
return hash(f"{event.type}{event.netloc}")
else: # HTTP_RESPONSE
return hash(f"{event.type}{event.data['url']}")

async def handle_event(self, event):
if event.type == "URL":
if self.config.get("include_subdirs"):
base_url = self.normalize_url(event.data) # Use the entire URL including subdirectories

else:
base_url = f"{event.parsed_url.scheme}://{event.parsed_url.netloc}/" # path will be omitted

# Check for RAU AXD Handler
webresource = "Telerik.Web.UI.WebResource.axd?type=rau"
result, _ = await self.test_detector(event.data, webresource)
result, _ = await self.test_detector(base_url, webresource)
if result:
if "RadAsyncUpload handler is registered successfully" in result.text:
self.debug("Detected Telerik instance (Telerik.Web.UI.WebResource.axd?type=rau)")
self.verbose("Detected Telerik instance (Telerik.Web.UI.WebResource.axd?type=rau)")

probe_data = {
"rauPostData": (
Expand Down Expand Up @@ -211,15 +243,14 @@ async def handle_event(self, event):

description = f"Telerik RAU AXD Handler detected. Verbose Errors Enabled: [{str(verbose_errors)}] Version Guess: [{version}]"
await self.emit_event(
{"host": str(event.host), "url": f"{event.data}{webresource}", "description": description},
{"host": str(event.host), "url": f"{base_url}{webresource}", "description": description},
"FINDING",
event,
context=f"{{module}} scanned {event.data} and identified {{event.type}}: Telerik RAU AXD Handler",
context=f"{{module}} scanned {base_url} and identified {{event.type}}: Telerik RAU AXD Handler",
)
if self.config.get("exploit_RAU_crypto") is True:
hostname = urlparse(event.data).netloc
if hostname not in self.RAUConfirmed:
self.RAUConfirmed.append(hostname)
if base_url not in self.RAUConfirmed:
self.RAUConfirmed.append(base_url)
root_tool_path = self.scan.helpers.tools_dir / "telerik"
self.debug(root_tool_path)

Expand All @@ -242,17 +273,17 @@ async def handle_event(self, event):
"severity": "CRITICAL",
"description": description,
"host": str(event.host),
"url": f"{event.data}{webresource}",
"url": f"{base_url}{webresource}",
},
"VULNERABILITY",
event,
context=f"{{module}} scanned {event.data} and identified critical {{event.type}}: {description}",
context=f"{{module}} scanned {base_url} and identified critical {{event.type}}: {description}",
)
break

urls = {}
for dh in self.DialogHandlerUrls:
url = self.create_url(event.data, f"{dh}?dp=1")
url = self.create_url(base_url, f"{dh}?dp=1")
urls[url] = dh

gen = self.helpers.request_batch(list(urls))
Expand All @@ -265,27 +296,27 @@ async def handle_event(self, event):
# tolerate some random errors
if fail_count < 2:
continue
self.debug(f"Cancelling run against {event.data} due to failed request")
self.debug(f"Cancelling run against {base_url} due to failed request")
await gen.aclose()
else:
if "Cannot deserialize dialog parameters" in response.text:
self.debug(f"Detected Telerik UI instance ({dh})")
description = "Telerik DialogHandler detected"
await self.emit_event(
{"host": str(event.host), "url": f"{event.data}{dh}", "description": description},
{"host": str(event.host), "url": f"{base_url}{dh}", "description": description},
"FINDING",
event,
)
# Once we have a match we need to stop, because the basic handler (Telerik.Web.UI.DialogHandler.aspx) usually works with a path wildcard
await gen.aclose()

spellcheckhandler = "Telerik.Web.UI.SpellCheckHandler.axd"
result, _ = await self.test_detector(event.data, spellcheckhandler)
result, _ = await self.test_detector(base_url, spellcheckhandler)
status_code = getattr(result, "status_code", 0)
# The standard behavior for the spellcheck handler without parameters is a 500
if status_code == 500:
# Sometimes webapps will just return 500 for everything, so rule out the false positive
validate_result, _ = await self.test_detector(event.data, self.helpers.rand_string())
validate_result, _ = await self.test_detector(base_url, self.helpers.rand_string())
self.debug(validate_result)
validate_status_code = getattr(validate_result, "status_code", 0)
if validate_status_code not in (0, 500):
Expand All @@ -294,31 +325,31 @@ async def handle_event(self, event):
await self.emit_event(
{
"host": str(event.host),
"url": f"{event.data}{spellcheckhandler}",
"url": f"{base_url}{spellcheckhandler}",
"description": description,
},
"FINDING",
event,
context=f"{{module}} scanned {event.data} and identified {{event.type}}: Telerik SpellCheckHandler",
context=f"{{module}} scanned {base_url} and identified {{event.type}}: Telerik SpellCheckHandler",
)

chartimagehandler = "ChartImage.axd?ImageName=bqYXJAqm315eEd6b%2bY4%2bGqZpe7a1kY0e89gfXli%2bjFw%3d"
result, _ = await self.test_detector(event.data, chartimagehandler)
result, _ = await self.test_detector(base_url, chartimagehandler)
status_code = getattr(result, "status_code", 0)
if status_code == 200:
chartimagehandler_error = "ChartImage.axd?ImageName="
result_error, _ = await self.test_detector(event.data, chartimagehandler_error)
result_error, _ = await self.test_detector(base_url, chartimagehandler_error)
error_status_code = getattr(result_error, "status_code", 0)
if error_status_code not in (0, 200):
await self.emit_event(
{
"host": str(event.host),
"url": f"{event.data}{chartimagehandler}",
"url": f"{base_url}{chartimagehandler}",
"description": "Telerik ChartImage AXD Handler Detected",
},
"FINDING",
event,
context=f"{{module}} scanned {event.data} and identified {{event.type}}: Telerik ChartImage AXD Handler",
context=f"{{module}} scanned {base_url} and identified {{event.type}}: Telerik ChartImage AXD Handler",
)

elif event.type == "HTTP_RESPONSE":
Expand Down Expand Up @@ -348,14 +379,8 @@ async def handle_event(self, event):
context="{module} searched HTTP_RESPONSE and identified {event.type}: Telerik AsyncUpload",
)

# Check for RAD Controls in URL

def create_url(self, baseurl, detector):
if not baseurl.endswith("/"):
url = f"{baseurl}/{detector}"
else:
url = f"{baseurl}{detector}"
return url
return f"{baseurl}{detector}"

async def test_detector(self, baseurl, detector):
result = None
Expand Down
2 changes: 1 addition & 1 deletion bbot/modules/trufflehog.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class trufflehog(BaseModule):
}

options = {
"version": "3.88.2",
"version": "3.88.3",
"config": "",
"only_verified": True,
"concurrency": 8,
Expand Down
1 change: 1 addition & 0 deletions bbot/presets/web/dotnet-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ config:
extensions: asp,aspx,ashx,asmx,ascx
telerik:
exploit_RAU_crypto: True
include_subdirs: True # Run against every directory, not the default first received URL per-host
59 changes: 57 additions & 2 deletions bbot/test/test_step_2/module_tests/test_module_telerik.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import re
from .base import ModuleTestBase
from .base import ModuleTestBase, tempwordlist


class TestTelerik(ModuleTestBase):
Expand Down Expand Up @@ -28,7 +28,7 @@ async def setup_before_prep(self, module_test):
}
module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args)

# Simulate DialogHandler detection
# Simulate SpellCheckHandler detection
expect_args = {"method": "GET", "uri": "/Telerik.Web.UI.SpellCheckHandler.axd"}
respond_args = {"status": 500}
module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args)
Expand Down Expand Up @@ -114,3 +114,58 @@ def check(self, module_test, events):
assert telerik_dialoghandler_detection, "Telerik dialoghandler detection failed"
assert telerik_chartimage_detection, "Telerik chartimage detection failed"
assert telerik_http_response_parameters_detection, "Telerik SerializedParameters detection failed"


class TestTelerikDialogHandler_includesubdirs(TestTelerik):
targets = ["http://127.0.0.1:8888/", "http://127.0.0.1:8888/temp/"]
config_overrides = {
"modules": {
"telerik": {
"include_subdirs": True,
},
}
}
modules_overrides = ["httpx", "telerik"]

async def setup_before_prep(self, module_test):
# Simulate NO SpellCheckHandler detection (not testing for that with this test)
expect_args = {"method": "GET", "uri": "/Telerik.Web.UI.SpellCheckHandler.axd"}
respond_args = {"status": 404}
module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args)

# Simulate DialogHandler detection
expect_args = {"method": "GET", "uri": "/App_Master/Telerik.Web.UI.DialogHandler.aspx"}
respond_args = {
"response_data": '<input type="hidden" name="dialogParametersHolder" id="dialogParametersHolder" /><div style=\'color:red\'>Cannot deserialize dialog parameters. Please refresh the editor page.</div><div>Error Message:Invalid length for a Base-64 char array or string.</div></form></body></html>'
}
module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args)

# Simulate DialogHandler detection (in /temp)
expect_args = {"method": "GET", "uri": "/temp/App_Master/Telerik.Web.UI.DialogHandler.aspx"}
respond_args = {
"response_data": '<input type="hidden" name="dialogParametersHolder" id="dialogParametersHolder" /><div style=\'color:red\'>Cannot deserialize dialog parameters. Please refresh the editor page.</div><div>Error Message:Invalid length for a Base-64 char array or string.</div></form></body></html>'
}
module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args)

# Simulate /temp directory detection
expect_args = {"method": "GET", "uri": "/temp/"}
respond_args = {"response_data": "Temporary directory found"}
module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args)

# Fallback
expect_args = {"method": "GET", "uri": "/"}
respond_args = {"response_data": "alive"}
module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args)

async def setup_after_prep(self, module_test):
module_test.scan.modules["telerik"].telerikVersions = ["2014.2.724", "2014.3.1024", "2015.1.204"]
module_test.scan.modules["telerik"].DialogHandlerUrls = [
"App_Master/Telerik.Web.UI.DialogHandler.aspx",
]

def check(self, module_test, events):
# Check if the expected requests were made
finding_count = sum(
1 for e in events if e.type == "FINDING" and "Telerik DialogHandler detected" in e.data["description"]
)
assert finding_count == 2, "Expected 2 FINDING events (root and /temp), got {finding_count}"
Loading

0 comments on commit fd544e9

Please sign in to comment.