Skip to content

Commit 18a8a16

Browse files
Merge pull request #2228 from blacklanternsecurity/dev
Dev -> Stable 2.3.2
2 parents 0b83416 + f55f502 commit 18a8a16

9 files changed

Lines changed: 141 additions & 59 deletions

File tree

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ jobs:
110110
token: ${{ secrets.BBOT_DOCS_UPDATER_PAT }}
111111
- uses: actions/setup-python@v5
112112
with:
113-
python-version: "3.x"
113+
python-version: "3.11"
114114
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
115115
- uses: actions/cache@v4
116116
with:

bbot/modules/telerik.py

Lines changed: 57 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,21 @@
55

66

77
class telerik(BaseModule):
8+
"""
9+
Test for endpoints associated with Telerik.Web.UI.dll
10+
11+
Telerik.Web.UI.WebResource.axd (CVE-2017-11317)
12+
Telerik.Web.UI.DialogHandler.aspx (CVE-2017-9248)
13+
Telerik.Web.UI.SpellCheckHandler.axd (associated with CVE-2017-9248)
14+
ChartImage.axd (CVE-2019-19790)
15+
16+
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)
17+
18+
With exploit_RAU_crypto enabled, the module will attempt to exploit CVE-2017-11317. THIS WILL UPLOAD A (benign) FILE IF SUCCESSFUL.
19+
20+
Will dedupe to host by default (running against first received URL). With include_subdirs enabled, will run against every directory.
21+
"""
22+
823
watched_events = ["URL", "HTTP_RESPONSE"]
924
produced_events = ["VULNERABILITY", "FINDING"]
1025
flags = ["active", "aggressive", "web-thorough"]
@@ -139,8 +154,11 @@ class telerik(BaseModule):
139154

140155
RAUConfirmed = []
141156

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

145163
in_scope_only = True
146164

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

163181
_module_threads = 5
164182

183+
@staticmethod
184+
def normalize_url(url):
185+
return str(url.rstrip("/") + "/").lower()
186+
165187
def _incoming_dedup_hash(self, event):
166188
if event.type == "URL":
167-
return hash(event.host)
168-
else:
169-
return hash(event.data["url"])
189+
if self.config.get("include_subdirs") is True:
190+
return hash(f"{event.type}{self.normalize_url(event.data)}")
191+
else:
192+
return hash(f"{event.type}{event.netloc}")
193+
else: # HTTP_RESPONSE
194+
return hash(f"{event.type}{event.data['url']}")
170195

171196
async def handle_event(self, event):
172197
if event.type == "URL":
198+
if self.config.get("include_subdirs"):
199+
base_url = self.normalize_url(event.data) # Use the entire URL including subdirectories
200+
201+
else:
202+
base_url = f"{event.parsed_url.scheme}://{event.parsed_url.netloc}/" # path will be omitted
203+
204+
# Check for RAU AXD Handler
173205
webresource = "Telerik.Web.UI.WebResource.axd?type=rau"
174-
result, _ = await self.test_detector(event.data, webresource)
206+
result, _ = await self.test_detector(base_url, webresource)
175207
if result:
176208
if "RadAsyncUpload handler is registered successfully" in result.text:
177-
self.debug("Detected Telerik instance (Telerik.Web.UI.WebResource.axd?type=rau)")
209+
self.verbose("Detected Telerik instance (Telerik.Web.UI.WebResource.axd?type=rau)")
178210

179211
probe_data = {
180212
"rauPostData": (
@@ -211,15 +243,14 @@ async def handle_event(self, event):
211243

212244
description = f"Telerik RAU AXD Handler detected. Verbose Errors Enabled: [{str(verbose_errors)}] Version Guess: [{version}]"
213245
await self.emit_event(
214-
{"host": str(event.host), "url": f"{event.data}{webresource}", "description": description},
246+
{"host": str(event.host), "url": f"{base_url}{webresource}", "description": description},
215247
"FINDING",
216248
event,
217-
context=f"{{module}} scanned {event.data} and identified {{event.type}}: Telerik RAU AXD Handler",
249+
context=f"{{module}} scanned {base_url} and identified {{event.type}}: Telerik RAU AXD Handler",
218250
)
219251
if self.config.get("exploit_RAU_crypto") is True:
220-
hostname = urlparse(event.data).netloc
221-
if hostname not in self.RAUConfirmed:
222-
self.RAUConfirmed.append(hostname)
252+
if base_url not in self.RAUConfirmed:
253+
self.RAUConfirmed.append(base_url)
223254
root_tool_path = self.scan.helpers.tools_dir / "telerik"
224255
self.debug(root_tool_path)
225256

@@ -242,17 +273,17 @@ async def handle_event(self, event):
242273
"severity": "CRITICAL",
243274
"description": description,
244275
"host": str(event.host),
245-
"url": f"{event.data}{webresource}",
276+
"url": f"{base_url}{webresource}",
246277
},
247278
"VULNERABILITY",
248279
event,
249-
context=f"{{module}} scanned {event.data} and identified critical {{event.type}}: {description}",
280+
context=f"{{module}} scanned {base_url} and identified critical {{event.type}}: {description}",
250281
)
251282
break
252283

253284
urls = {}
254285
for dh in self.DialogHandlerUrls:
255-
url = self.create_url(event.data, f"{dh}?dp=1")
286+
url = self.create_url(base_url, f"{dh}?dp=1")
256287
urls[url] = dh
257288

258289
gen = self.helpers.request_batch(list(urls))
@@ -265,27 +296,27 @@ async def handle_event(self, event):
265296
# tolerate some random errors
266297
if fail_count < 2:
267298
continue
268-
self.debug(f"Cancelling run against {event.data} due to failed request")
299+
self.debug(f"Cancelling run against {base_url} due to failed request")
269300
await gen.aclose()
270301
else:
271302
if "Cannot deserialize dialog parameters" in response.text:
272303
self.debug(f"Detected Telerik UI instance ({dh})")
273304
description = "Telerik DialogHandler detected"
274305
await self.emit_event(
275-
{"host": str(event.host), "url": f"{event.data}{dh}", "description": description},
306+
{"host": str(event.host), "url": f"{base_url}{dh}", "description": description},
276307
"FINDING",
277308
event,
278309
)
279310
# Once we have a match we need to stop, because the basic handler (Telerik.Web.UI.DialogHandler.aspx) usually works with a path wildcard
280311
await gen.aclose()
281312

282313
spellcheckhandler = "Telerik.Web.UI.SpellCheckHandler.axd"
283-
result, _ = await self.test_detector(event.data, spellcheckhandler)
314+
result, _ = await self.test_detector(base_url, spellcheckhandler)
284315
status_code = getattr(result, "status_code", 0)
285316
# The standard behavior for the spellcheck handler without parameters is a 500
286317
if status_code == 500:
287318
# Sometimes webapps will just return 500 for everything, so rule out the false positive
288-
validate_result, _ = await self.test_detector(event.data, self.helpers.rand_string())
319+
validate_result, _ = await self.test_detector(base_url, self.helpers.rand_string())
289320
self.debug(validate_result)
290321
validate_status_code = getattr(validate_result, "status_code", 0)
291322
if validate_status_code not in (0, 500):
@@ -294,31 +325,31 @@ async def handle_event(self, event):
294325
await self.emit_event(
295326
{
296327
"host": str(event.host),
297-
"url": f"{event.data}{spellcheckhandler}",
328+
"url": f"{base_url}{spellcheckhandler}",
298329
"description": description,
299330
},
300331
"FINDING",
301332
event,
302-
context=f"{{module}} scanned {event.data} and identified {{event.type}}: Telerik SpellCheckHandler",
333+
context=f"{{module}} scanned {base_url} and identified {{event.type}}: Telerik SpellCheckHandler",
303334
)
304335

305336
chartimagehandler = "ChartImage.axd?ImageName=bqYXJAqm315eEd6b%2bY4%2bGqZpe7a1kY0e89gfXli%2bjFw%3d"
306-
result, _ = await self.test_detector(event.data, chartimagehandler)
337+
result, _ = await self.test_detector(base_url, chartimagehandler)
307338
status_code = getattr(result, "status_code", 0)
308339
if status_code == 200:
309340
chartimagehandler_error = "ChartImage.axd?ImageName="
310-
result_error, _ = await self.test_detector(event.data, chartimagehandler_error)
341+
result_error, _ = await self.test_detector(base_url, chartimagehandler_error)
311342
error_status_code = getattr(result_error, "status_code", 0)
312343
if error_status_code not in (0, 200):
313344
await self.emit_event(
314345
{
315346
"host": str(event.host),
316-
"url": f"{event.data}{chartimagehandler}",
347+
"url": f"{base_url}{chartimagehandler}",
317348
"description": "Telerik ChartImage AXD Handler Detected",
318349
},
319350
"FINDING",
320351
event,
321-
context=f"{{module}} scanned {event.data} and identified {{event.type}}: Telerik ChartImage AXD Handler",
352+
context=f"{{module}} scanned {base_url} and identified {{event.type}}: Telerik ChartImage AXD Handler",
322353
)
323354

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

351-
# Check for RAD Controls in URL
352-
353382
def create_url(self, baseurl, detector):
354-
if not baseurl.endswith("/"):
355-
url = f"{baseurl}/{detector}"
356-
else:
357-
url = f"{baseurl}{detector}"
358-
return url
383+
return f"{baseurl}{detector}"
359384

360385
async def test_detector(self, baseurl, detector):
361386
result = None

bbot/modules/trufflehog.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class trufflehog(BaseModule):
1313
}
1414

1515
options = {
16-
"version": "3.88.2",
16+
"version": "3.88.3",
1717
"config": "",
1818
"only_verified": True,
1919
"concurrency": 8,

bbot/presets/web/dotnet-audit.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ config:
1919
extensions: asp,aspx,ashx,asmx,ascx
2020
telerik:
2121
exploit_RAU_crypto: True
22+
include_subdirs: True # Run against every directory, not the default first received URL per-host

bbot/test/test_step_2/module_tests/test_module_telerik.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import re
2-
from .base import ModuleTestBase
2+
from .base import ModuleTestBase, tempwordlist
33

44

55
class TestTelerik(ModuleTestBase):
@@ -28,7 +28,7 @@ async def setup_before_prep(self, module_test):
2828
}
2929
module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args)
3030

31-
# Simulate DialogHandler detection
31+
# Simulate SpellCheckHandler detection
3232
expect_args = {"method": "GET", "uri": "/Telerik.Web.UI.SpellCheckHandler.axd"}
3333
respond_args = {"status": 500}
3434
module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args)
@@ -114,3 +114,58 @@ def check(self, module_test, events):
114114
assert telerik_dialoghandler_detection, "Telerik dialoghandler detection failed"
115115
assert telerik_chartimage_detection, "Telerik chartimage detection failed"
116116
assert telerik_http_response_parameters_detection, "Telerik SerializedParameters detection failed"
117+
118+
119+
class TestTelerikDialogHandler_includesubdirs(TestTelerik):
120+
targets = ["http://127.0.0.1:8888/", "http://127.0.0.1:8888/temp/"]
121+
config_overrides = {
122+
"modules": {
123+
"telerik": {
124+
"include_subdirs": True,
125+
},
126+
}
127+
}
128+
modules_overrides = ["httpx", "telerik"]
129+
130+
async def setup_before_prep(self, module_test):
131+
# Simulate NO SpellCheckHandler detection (not testing for that with this test)
132+
expect_args = {"method": "GET", "uri": "/Telerik.Web.UI.SpellCheckHandler.axd"}
133+
respond_args = {"status": 404}
134+
module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args)
135+
136+
# Simulate DialogHandler detection
137+
expect_args = {"method": "GET", "uri": "/App_Master/Telerik.Web.UI.DialogHandler.aspx"}
138+
respond_args = {
139+
"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>'
140+
}
141+
module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args)
142+
143+
# Simulate DialogHandler detection (in /temp)
144+
expect_args = {"method": "GET", "uri": "/temp/App_Master/Telerik.Web.UI.DialogHandler.aspx"}
145+
respond_args = {
146+
"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>'
147+
}
148+
module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args)
149+
150+
# Simulate /temp directory detection
151+
expect_args = {"method": "GET", "uri": "/temp/"}
152+
respond_args = {"response_data": "Temporary directory found"}
153+
module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args)
154+
155+
# Fallback
156+
expect_args = {"method": "GET", "uri": "/"}
157+
respond_args = {"response_data": "alive"}
158+
module_test.set_expect_requests(expect_args=expect_args, respond_args=respond_args)
159+
160+
async def setup_after_prep(self, module_test):
161+
module_test.scan.modules["telerik"].telerikVersions = ["2014.2.724", "2014.3.1024", "2015.1.204"]
162+
module_test.scan.modules["telerik"].DialogHandlerUrls = [
163+
"App_Master/Telerik.Web.UI.DialogHandler.aspx",
164+
]
165+
166+
def check(self, module_test, events):
167+
# Check if the expected requests were made
168+
finding_count = sum(
169+
1 for e in events if e.type == "FINDING" and "Telerik DialogHandler detected" in e.data["description"]
170+
)
171+
assert finding_count == 2, "Expected 2 FINDING events (root and /temp), got {finding_count}"

0 commit comments

Comments
 (0)