From 453e1b0368b171a7fe9d606dd66135b564b4d793 Mon Sep 17 00:00:00 2001 From: liquidsec Date: Mon, 20 Nov 2023 15:18:29 -0500 Subject: [PATCH 1/4] adding new telerik module functionality --- bbot/modules/telerik.py | 262 ++++++++++++------ .../module_tests/test_module_telerik.py | 38 ++- 2 files changed, 210 insertions(+), 90 deletions(-) diff --git a/bbot/modules/telerik.py b/bbot/modules/telerik.py index e012b6af6..e437f501e 100644 --- a/bbot/modules/telerik.py +++ b/bbot/modules/telerik.py @@ -6,7 +6,7 @@ class telerik(BaseModule): - watched_events = ["URL"] + watched_events = ["URL", "HTTP_RESPONSE"] produced_events = ["VULNERABILITY", "FINDING"] flags = ["active", "aggressive", "slow", "web-thorough"] meta = {"description": "Scan for critical Telerik vulnerabilities"} @@ -90,6 +90,7 @@ class telerik(BaseModule): "2016.3.914", "2016.3.1018", "2016.3.1027", + "2016.1.1213", "2017.1.118", "2017.1.228", "2017.2.503", @@ -139,7 +140,6 @@ class telerik(BaseModule): options_desc = {"exploit_RAU_crypto": "Attempt to confirm any RAU AXD detections are vulnerable"} in_scope_only = True - per_host_only = True deps_pip = ["pycryptodome~=3.17"] @@ -159,114 +159,200 @@ class telerik(BaseModule): _max_event_handlers = 5 + def _incoming_dedup_hash(self, event): + if event.type == "URL": + return hash(event.host) + else: + return hash(event.data["url"]) + async def setup(self): self.timeout = self.scan.config.get("httpx_timeout", 5) return True async def handle_event(self, event): - webresource = "Telerik.Web.UI.WebResource.axd?type=rau" - result, _ = await self.test_detector(event.data, webresource) - if result: - if "RadAsyncUpload handler is registered succesfully" in result.text: - self.debug(f"Detected Telerik instance (Telerik.Web.UI.WebResource.axd?type=rau)") - description = f"Telerik RAU AXD Handler detected" - self.emit_event( - {"host": str(event.host), "url": f"{event.data}{webresource}", "description": description}, - "FINDING", - event, - ) - if self.config.get("exploit_RAU_crypto") == True: - hostname = urlparse(event.data).netloc - if hostname not in self.RAUConfirmed: - self.RAUConfirmed.append(hostname) - root_tool_path = self.scan.helpers.tools_dir / "telerik" - self.debug(root_tool_path) + self.warning(event.type) + if event.type == "URL": + webresource = "Telerik.Web.UI.WebResource.axd?type=rau" + result, _ = await self.test_detector(event.data, webresource) + if result: + if "RadAsyncUpload handler is registered succesfully" in result.text: + self.debug(f"Detected Telerik instance (Telerik.Web.UI.WebResource.axd?type=rau)") - for version in self.telerikVersions: - command = [ - executable, - str(root_tool_path / "RAU_crypto-master/RAU_crypto.py"), - "-P", - "C:\\\\Windows\\\\Temp", - version, - str(root_tool_path / "testfile.txt"), - result.url, - ] - output = await self.helpers.run(command) - description = f"[CVE-2017-11317] [{str(version)}] {webresource}" - if "fileInfo" in output.stdout: - self.debug(f"Confirmed Vulnerable Telerik (version: {str(version)}") - self.emit_event( - { - "severity": "CRITICAL", - "description": description, - "host": str(event.host), - "url": f"{event.data}{webresource}", - }, - "VULNERABILITY", - event, - ) - break + probe_data = { + "rauPostData": ( + None, + "mQheol55IDiQWWSxl+Atkc68JXWUJ6QSirwLhEwleMiw3vN4cwABE74V2fWsLGg8CFXHOP6np90M+sLrLDqFACGNvonxmgT8aBsTZPWbXErewMGNWBP34aX0DmMvXVyTEpQ6FkFhZi19cTtdYfRLI8Uc04uNSsdWnltDMQ2CX/sSLOXUFNnZdAwAXgUuprYhU28Zwh/GdgYh447ksXfAC2fuPqEJqKDDwBlltxsS/zSq8ipIg326ymB2dmOpH/P3hcAmTKOyzB0dW6a6pmJvqNVU+50DlrUC00RbBbTJwlV6Xm4s4XTvgXLvMQ6czz2OAYY18HI+HYX5uvajctj/25UR8edwu68ZCgedsD7EZHRSSthjxohxfAyrfshjcu1LnhCEd0ClowKxBS4eiaLxVxhJAdB7XcbbXxIS9WWKa7gtRMNc/jUAOlIpvOZ3N+bOQ6rsNMHv7TZk1g0bxPl99yBn9qvtAwDMNPDoADxoBSisAkIIl9mImKv7y7nAiKoj7ukApdu5XQuVo10SxwkLkqHcvEEgjxTrOlCbEbxK2/du9TgXxD9iqKyaPLHPzNZsnzCsG6qNXv0fNkeASP9tZAyvi/y1eLrpScE+J7blfT+kBkGPTTFc6Z4z6lN7GqSHofq/CDHC2S2+qdoRdC3C25V74j+Ae6MkpSfqYx4KZYNtxBAxjf9Uf3JVSiZh3X2W/7aFeimFft0h/liybSjJTzO+AwNJluI4kXqemFoHnjVFfUQViaIuk4UP0D861kCU6KIGLZLpOaa0g0KM8hmu3OjwVOy8QVXYtbx5lOmSX9h3imRzMDFRTXK25YpUJgD0/LFMgCeZLA8SCYzkThyN2d8f8n5l8iOScR47o8i8sqCp/fd3JTogSbwD7LxnHudpiw2W/OfpMGipgc6loQFoX4klQaYwKkA4w+GUzahfAJmIiukZuTLOPCPQvX4wKtLqw1YiHtuaLHvLYq2/F66QQXNrZ4SucUNED0p5TUVTvHGUbuA0zxAyYSfYVgTNZjXGguQBY7DsN1SkpCa/ltvIiGtCbHQR86OrvjJMACe0wdpMCqEg7JiGym3RrLqvmjpS&sbZRwxJ96gmXFBSbSvT0ve7jpvDoieqd6RbG+GIP0H7sO5/0ZnvheosB9jQAifuMabY7lW4UzZgr5o2iqE0tBl4SGhfWyYW7iCFXnd3aIuCnUvhT58Rp8g7kGkA/eU/s68E66KOBXNuBnokZR9cIsjE0Tt3Jfxrk018+CmVcXpjXp/RmhRwCJTgEAXQuNplb/KdkLxqDn519iRtbiU6aLZX8YctdFQBqyKVgkk8WYXxcXQ8wYnxtpEtGuBcsndUi1iPp4Od8rYY1HPWg+FIquW17YPHjfP4gO4dhZe4sd7gH0ARyGDjiYVj7ODDE0wGmwmFVdQTrDX5AaxKuJy0NbQ==", + ), + "file": ("blob", b"e1daf48a", "application/octet-stream"), + "fileName": (None, "df8dbc7a"), + "contentType": (None, "text/html"), + "lastModifiedDate": (None, "2020-01-02T08:02:01.067Z"), + "metadata": ( + None, + '{"TotalChunks":1,"ChunkIndex":0,"TotalFileSize":1,"UploadID":"3ea7b19db6c5.txt"}', + ), + } - tasks = [] - for dh in self.DialogHandlerUrls: - tasks.append(self.helpers.create_task(self.test_detector(event.data, f"{dh}?dp=1"))) - - fail_count = 0 - gen = self.helpers.as_completed(tasks) - async for task in gen: - try: - result, dh = await task - except asyncio.CancelledError: - continue + version = "unknown" + verbose_errors = False + # send probe + probe_response = await self.helpers.request( + f"{event.data}{webresource}", method="POST", files=probe_data + ) - # cancel if we run into timeouts etc. - if result is None: - fail_count += 1 + if probe_response: + if "Exception Details: " in probe_response.text: + verbose_errors = True + if ( + "Telerik.Web.UI.CryptoExceptionThrower.ThrowGenericCryptoException" + in probe_response.text + ): + version = "Post-2020 (Encrypt-Then-Mac Enabled, with Generic Crypto Failure Message)" + elif "Padding is invalid and cannot be removed" in probe_response.text: + version = "<= 2019 (Either Pre-2017 (vulnerable), or 2017-2019 w/ Encrypt-Then-Mac)" - # tolerate some random errors - if fail_count < 2: - continue - self.debug(f"Cancelling run against {event.data} due to failed request") - await self.helpers.cancel_tasks(tasks) - await gen.aclose() - else: - if "Cannot deserialize dialog parameters" in result.text: - await self.helpers.cancel_tasks(tasks) - self.debug(f"Detected Telerik UI instance ({dh})") - description = f"Telerik DialogHandler detected" + description = f"Telerik RAU AXD Handler detected. Verbose Errors Enabled: [{str(verbose_errors)}] Version Guess: [{version}]" self.emit_event( - {"host": str(event.host), "url": f"{event.data}{dh}", "description": description}, + {"host": str(event.host), "url": f"{event.data}{webresource}", "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 + if self.config.get("exploit_RAU_crypto") == True: + hostname = urlparse(event.data).netloc + if hostname not in self.RAUConfirmed: + self.RAUConfirmed.append(hostname) + root_tool_path = self.scan.helpers.tools_dir / "telerik" + self.debug(root_tool_path) + + for version in self.telerikVersions: + command = [ + executable, + str(root_tool_path / "RAU_crypto-master/RAU_crypto.py"), + "-P", + "C:\\\\Windows\\\\Temp", + version, + str(root_tool_path / "testfile.txt"), + result.url, + ] + output = await self.helpers.run(command) + description = f"[CVE-2017-11317] [{str(version)}] {webresource}" + if "fileInfo" in output.stdout: + self.debug(f"Confirmed Vulnerable Telerik (version: {str(version)}") + self.emit_event( + { + "severity": "CRITICAL", + "description": description, + "host": str(event.host), + "url": f"{event.data}{webresource}", + }, + "VULNERABILITY", + event, + ) + break + + tasks = [] + for dh in self.DialogHandlerUrls: + tasks.append(self.helpers.create_task(self.test_detector(event.data, f"{dh}?dp=1"))) + + fail_count = 0 + gen = self.helpers.as_completed(tasks) + async for task in gen: + try: + result, dh = await task + except asyncio.CancelledError: + continue + + # cancel if we run into timeouts etc. + if result is None: + fail_count += 1 + + # tolerate some random errors + if fail_count < 2: + continue + self.debug(f"Cancelling run against {event.data} due to failed request") + await self.helpers.cancel_tasks(tasks) await gen.aclose() + else: + if "Cannot deserialize dialog parameters" in result.text: + await self.helpers.cancel_tasks(tasks) + self.debug(f"Detected Telerik UI instance ({dh})") + description = f"Telerik DialogHandler detected" + self.emit_event( + {"host": str(event.host), "url": f"{event.data}{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() + + await self.helpers.cancel_tasks(tasks) + + spellcheckhandler = "Telerik.Web.UI.SpellCheckHandler.axd" + result, _ = await self.test_detector(event.data, spellcheckhandler) + try: + # The standard behavior for the spellcheck handler without parameters is a 500 + if result.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()) + self.debug(validate_result) + if validate_result.status_code != 500: + self.debug(f"Detected Telerik UI instance (Telerik.Web.UI.SpellCheckHandler.axd)") + description = f"Telerik SpellCheckHandler detected" + self.emit_event( + { + "host": str(event.host), + "url": f"{event.data}{spellcheckhandler}", + "description": description, + }, + "FINDING", + event, + ) + except Exception: + pass - await self.helpers.cancel_tasks(tasks) + chartimagehandler = "ChartImage.axd?ImageName=bqYXJAqm315eEd6b%2bY4%2bGqZpe7a1kY0e89gfXli%2bjFw%3d" + result, _ = await self.test_detector(event.data, chartimagehandler) - spellcheckhandler = "Telerik.Web.UI.SpellCheckHandler.axd" - result, _ = await self.test_detector(event.data, spellcheckhandler) - try: - # The standard behavior for the spellcheck handler without parameters is a 500 - if result.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()) - self.debug(validate_result) - if validate_result.status_code != 500: - self.debug(f"Detected Telerik UI instance (Telerik.Web.UI.SpellCheckHandler.axd)") - description = f"Telerik SpellCheckHandler detected" + if result: + if result.status_code == 200: + chartimagehandler_error = "ChartImage.axd?ImageName=" + result_error, _ = await self.test_detector(event.data, chartimagehandler_error) + if result_error.status_code != 200: + self.emit_event( + { + "host": str(event.host), + "url": f"{event.data}{chartimagehandler}", + "description": "Telerik ChartImage AXD Handler Detected", + }, + "FINDING", + event, + ) + + elif event.type == "HTTP_RESPONSE": + resp_body = event.data.get("body", None) + if resp_body: + if '":{"SerializedParameters":"' in resp_body: self.emit_event( { "host": str(event.host), - "url": f"{event.data}{spellcheckhandler}", - "description": description, + "url": event.data["url"], + "description": "Telerik DialogHandler [SerializedParameters] Detected in HTTP Response", }, "FINDING", event, ) - except Exception: - pass + elif '"_serializedConfiguration":"' in resp_body: + self.emit_event( + { + "host": str(event.host), + "url": event.data["url"], + "description": "Telerik AsyncUpload [serializedConfiguration] Detected in HTTP Response", + }, + "FINDING", + event, + ) + + # Check for RAD Controls in URL async def test_detector(self, baseurl, detector): result = None @@ -278,7 +364,7 @@ async def test_detector(self, baseurl, detector): return result, detector async def filter_event(self, event): - if "endpoint" in event.tags: + if event.type == "URL" and "endpoint" in event.tags: return False else: return True diff --git a/bbot/test/test_step_2/module_tests/test_module_telerik.py b/bbot/test/test_step_2/module_tests/test_module_telerik.py index 6a4a7d97d..98c511f2a 100644 --- a/bbot/test/test_step_2/module_tests/test_module_telerik.py +++ b/bbot/test/test_step_2/module_tests/test_module_telerik.py @@ -3,7 +3,7 @@ class TestTelerik(ModuleTestBase): - targets = ["http://127.0.0.1:8888"] + targets = ["http://127.0.0.1:8888", "http://127.0.0.1:8888/telerik.aspx"] modules_overrides = ["httpx", "telerik"] config_overrides = {"modules": {"telerik": {"exploit_RAU_crypto": True}}} @@ -29,7 +29,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 - expect_args = {"method": "GET", "uri": "Telerik.Web.UI.SpellCheckHandler.axd"} + 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) @@ -40,6 +40,24 @@ async def setup_before_prep(self, module_test): } module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) + # Simulate ChartImage.axd Detection + expect_args = { + "method": "GET", + "uri": "/ChartImage.axd", + "query_string": "ImageName=bqYXJAqm315eEd6b%2bY4%2bGqZpe7a1kY0e89gfXli%2bjFw%3d", + } + respond_args = {"status": 200} + module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) + + expect_args = {"method": "GET", "uri": "/ChartImage.axd", "query_string": "ImageName="} + respond_args = {"status": 500} + module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) + + # Simulate Dialog Parameters in URL + expect_args = {"method": "GET", "uri": "/telerik.aspx"} + respond_args = {"response_data": '{"ImageManager":{"SerializedParameters":"MBwZB"}'} + module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args) + # Fallback expect_args = {"uri": re.compile(r"^/\w{10}$")} respond_args = {"status": 200} @@ -58,9 +76,12 @@ def check(self, module_test, events): telerik_axd_vulnerable = False telerik_spellcheck_detection = False telerik_dialoghandler_detection = False + telerik_chartimage_detection = False + telerik_http_response_parameters_detection = False for e in events: if e.type == "FINDING" and "Telerik RAU AXD Handler detected" in e.data["description"]: + e.data["description"] telerik_axd_detection = True continue @@ -76,7 +97,20 @@ def check(self, module_test, events): telerik_spellcheck_detection = True continue + if e.type == "FINDING" and "Telerik ChartImage AXD Handler Detected" in e.data["description"]: + telerik_chartimage_detection = True + continue + + if ( + e.type == "FINDING" + and "Telerik DialogHandler [SerializedParameters] Detected in HTTP Response" in e.data["description"] + ): + telerik_http_response_parameters_detection = True + continue + assert telerik_axd_detection, "Telerik AXD detection failed" assert telerik_axd_vulnerable, "Telerik vulnerable AXD detection failed" assert telerik_spellcheck_detection, "Telerik spellcheck detection failed" 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" From f04dfd760a806947b895cfb7e9589ae3593a118b Mon Sep 17 00:00:00 2001 From: liquidsec Date: Mon, 20 Nov 2023 15:25:34 -0500 Subject: [PATCH 2/4] oops --- bbot/modules/telerik.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bbot/modules/telerik.py b/bbot/modules/telerik.py index e437f501e..e983268c5 100644 --- a/bbot/modules/telerik.py +++ b/bbot/modules/telerik.py @@ -170,7 +170,6 @@ async def setup(self): return True async def handle_event(self, event): - self.warning(event.type) if event.type == "URL": webresource = "Telerik.Web.UI.WebResource.axd?type=rau" result, _ = await self.test_detector(event.data, webresource) From 5499f014d1a7975bc92dc7a9b07cde347aa4cd8c Mon Sep 17 00:00:00 2001 From: TheTechromancer Date: Mon, 20 Nov 2023 15:46:49 -0500 Subject: [PATCH 3/4] fixed tests: --- bbot/test/test_step_1/test_modules_basic.py | 22 ++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/bbot/test/test_step_1/test_modules_basic.py b/bbot/test/test_step_1/test_modules_basic.py index 6f8b8870f..7872095fc 100644 --- a/bbot/test/test_step_1/test_modules_basic.py +++ b/bbot/test/test_step_1/test_modules_basic.py @@ -171,6 +171,8 @@ async def test_modules_basic(scan, helpers, events, bbot_config, bbot_scanner, h @pytest.mark.asyncio async def test_modules_basic_perhostonly(scan, helpers, events, bbot_config, bbot_scanner, httpx_mock, monkeypatch): + from bbot.modules.base import BaseModule + per_host_scan = bbot_scanner( "evilcorp.com", modules=list(set(available_modules + available_internal_modules)), @@ -198,15 +200,17 @@ async def test_modules_basic_perhostonly(scan, helpers, events, bbot_config, bbo valid_1, reason_1 = await module._event_postcheck(url_1) valid_2, reason_2 = await module._event_postcheck(url_2) - if module.per_host_only == True: - assert valid_1 == True - assert valid_2 == False - assert hash("http://evilcorp.com/") in module._per_host_tracker - assert reason_2 == "per_host_only enabled and already seen host" - - else: - assert valid_1 == True - assert valid_2 == True + # if the module overrides _incoming_dedup_hash, this test won't work. + if module._incoming_dedup_hash == BaseModule._incoming_dedup_hash: + if module.per_host_only == True: + assert valid_1 == True + assert valid_2 == False + assert hash("http://evilcorp.com/") in module._per_host_tracker + assert reason_2 == "per_host_only enabled and already seen host" + + else: + assert valid_1 == True + assert valid_2 == True @pytest.mark.asyncio From 028965fbb502c8c681ae5be72d96762fa1c7396d Mon Sep 17 00:00:00 2001 From: TheTechromancer Date: Tue, 21 Nov 2023 10:11:16 -0500 Subject: [PATCH 4/4] fix dup detection bug --- bbot/modules/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bbot/modules/base.py b/bbot/modules/base.py index 6dee96745..601ccaeac 100644 --- a/bbot/modules/base.py +++ b/bbot/modules/base.py @@ -892,6 +892,8 @@ def set_error_state(self, message=None, clear_outgoing_queue=False): self.outgoing_event_queue.get_nowait() def is_incoming_duplicate(self, event, add=False): + if event.type in ("FINISHED",): + return False event_hash = self._incoming_dedup_hash(event) is_dup = event_hash in self._incoming_dup_tracker if add: