Skip to content

Commit 466150f

Browse files
authored
Merge pull request #200 from blacklanternsecurity/dev
dev-main
2 parents ddc5dc2 + 40c9d3c commit 466150f

File tree

13 files changed

+1669
-645
lines changed

13 files changed

+1669
-645
lines changed

.github/workflows/tests.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ jobs:
5252
uses: nick-fields/retry@v2
5353
with:
5454
max_attempts: 3
55-
timeout_minutes: 20
55+
timeout_minutes: 40
5656
retry_wait_seconds: 0
5757
command: |
5858
poetry run pytest --exitfirst --disable-warnings --log-cli-level=DEBUG --cov-report xml:cov.xml --cov=badsecrets --cov=examples

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,30 @@ python ./badsecrets/examples/blacklist3r.py --viewstate /wEPDwUJODExMDE5NzY5ZGQM
215215
216216
### telerik_knownkey.py
217217
218+
```bash
219+
badsecrets - Telerik UI known key exploitation tool
220+
221+
usage: telerik_knownkey.py [-h] -u URL [-p PROXY] [-a USER_AGENT] [-m] [-f] [-v VERSION] [-c CUSTOM_KEYS] [-d] [--modern-dialog-params]
222+
223+
options:
224+
-h, --help show this help message and exit
225+
-u URL, --url URL The URL of the page to access and attempt to pull viewstate and generator from
226+
-p PROXY, --proxy PROXY
227+
Optionally specify an HTTP proxy
228+
-a USER_AGENT, --user-agent USER_AGENT
229+
Optionally set a custom user-agent
230+
-m, --machine-keys Optionally include ASP.NET MachineKeys when loading keys
231+
-f, --force Force enumeration of vulnerable AsyncUpload endpoint without user confirmation
232+
-v VERSION, --version VERSION
233+
Specify a custom Telerik version to test
234+
-c CUSTOM_KEYS, --custom-keys CUSTOM_KEYS
235+
Specify custom keys in format 'encryptionkey,hashkey'. When provided, only these keys will be tested.
236+
-d, --debug Enable debug mode to show detailed request information
237+
--modern-dialog-params
238+
Use modern dialog parameters format (may work better for newer Telerik versions 2018+)
239+
240+
```
241+
218242
Fully functional CLI example for identifying known Telerik Hash keys (`Telerik.Upload.ConfigurationHashKey`) and Encryption keys (`Telerik.Web.UI.DialogParametersEncryptionKey`) used with Telerik DialogHandler instances for Post-2017 versions (those patched for CVE-2017-9248), and brute-forcing version / generating exploitation DialogParameters values.
219243
220244
Currently, this appears to be the only tool capable of building a working exploit URL for "patched" versions of Telerik.

badsecrets/base.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,14 @@ def carve(self, body=None, cookies=None, headers=None, requests_response=None, *
9393
raise badsecrets.errors.CarveException("Body/cookies/headers and requests_response cannot both be set")
9494

9595
if type(requests_response) == requests.models.Response:
96-
body = requests_response.text
97-
cookies = dict(requests_response.cookies)
98-
headers = requests_response.headers
96+
if not cookies:
97+
cookies = (
98+
requests_response.cookies.get_dict() if hasattr(requests_response.cookies, "get_dict") else {}
99+
)
100+
if not headers:
101+
headers = requests_response.headers
102+
if not body and hasattr(requests_response, "text"):
103+
body = requests_response.text
99104
else:
100105
raise badsecrets.errors.CarveException("requests_response must be a requests.models.Response object")
101106

@@ -142,7 +147,9 @@ def carve(self, body=None, cookies=None, headers=None, requests_response=None, *
142147
if self.carve_regex():
143148
s = re.search(self.carve_regex(), body)
144149
if s:
145-
r = self.carve_to_check_secret(s, url=kwargs.get("url", None))
150+
r = self.carve_to_check_secret(
151+
s, url=kwargs.get("url", None), body=body, cookies=cookies, headers=headers
152+
)
146153
if r:
147154
r["type"] = "SecretFound"
148155
else:

badsecrets/examples/telerik_knownkey.py

Lines changed: 354 additions & 97 deletions
Large diffs are not rendered by default.

badsecrets/modules/aspnet_viewstate.py

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,20 @@ def carve_regex(self):
2525
r"<input.+__VIEWSTATE\"\svalue=\"(.+)\"[\S\s]+<input.+__VIEWSTATEGENERATOR\"\svalue=\"(\w+)\""
2626
)
2727

28-
def carve_to_check_secret(self, s, url=None):
28+
def carve_to_check_secret(self, s, url=None, **kwargs):
2929
if len(s.groups()) == 2:
30-
r = self.check_secret(s.groups()[0], s.groups()[1], url)
31-
return r
30+
viewstate = s.groups()[0]
31+
generator = s.groups()[1]
32+
possible_userkey_cookies = ["ASP.NET_SessionId", "__AntiXsrfToken", "ASPSESSIONID"]
33+
34+
if kwargs.get("cookies") and hasattr(kwargs.get("cookies"), "get"):
35+
for cookie_name in possible_userkey_cookies:
36+
if cookie_name in kwargs.get("cookies"):
37+
cookie_value = kwargs.get("cookies").get(cookie_name)
38+
r = self.check_secret(viewstate, generator, url, cookie_value)
39+
if r:
40+
return r
41+
return self.check_secret(viewstate, generator, url)
3242

3343
@staticmethod
3444
def valid_preamble(sourcebytes):
@@ -92,7 +102,8 @@ def viewstate_decrypt(self, ekey_bytes, hash_alg, viewstate_B64, url, mode):
92102
else:
93103
continue
94104

95-
def viewstate_validate(self, vkey_bytes, encrypted, viewstate_B64, generator, url, mode):
105+
def viewstate_validate(self, vkey_bytes, encrypted, viewstate_B64, generator, url, mode, viewstate_userkey=None):
106+
96107
original_vkey_bytes = vkey_bytes
97108
viewstate_bytes = base64.b64decode(viewstate_B64)
98109

@@ -104,19 +115,31 @@ def viewstate_validate(self, vkey_bytes, encrypted, viewstate_B64, generator, ur
104115
signature_len = len(vs.signature)
105116
candidate_hash_algs = self.search_dict(self.hash_sizes, signature_len)
106117

118+
modifier_bytes = b"\x00" * 4
119+
if viewstate_userkey and viewstate_userkey.strip():
120+
modifier_bytes += viewstate_userkey.encode("utf-16le")
107121
for hash_alg in candidate_hash_algs:
108122
vkey_bytes = original_vkey_bytes
109123
viewstate_data = viewstate_bytes[: -self.hash_sizes[hash_alg]]
110124
signature = viewstate_bytes[-self.hash_sizes[hash_alg] :]
111125
if hash_alg == "MD5":
112-
md5_bytes = viewstate_data + vkey_bytes
113126
if not encrypted:
114-
md5_bytes += b"\x00" * 4
127+
# if viewstate_userkey and viewstate_userkey.strip():
128+
# md5_bytes = b"will not work, sorry"
129+
# MD5 + ViewStateUserKey is a horrible edge case that may NEVER work. We will not match on it, currently.
130+
# Last attempt:
131+
# md5_bytes = viewstate_data + vkey_bytes + page_hash_bytes + viewstate_userkey.encode('utf-16le')
132+
# But page_hash_bytes is apparently NOT the generator and my have to be brute-forced.
133+
# Probably not worth it for the 3 servers in the entire world probably using these settings in the wild.
134+
md5_bytes = viewstate_data + vkey_bytes + modifier_bytes
135+
else:
136+
md5_bytes = viewstate_data + vkey_bytes
115137
h = hashlib.md5(md5_bytes)
116138
else:
117139
vs_data_bytes = viewstate_data
118140
if not encrypted:
119141
vs_data_bytes += generator
142+
vs_data_bytes += modifier_bytes[4:]
120143
if mode == "DOTNET45" and url:
121144
s = Simulate_dotnet45_kdf_context_parameters(url)
122145
label, context = sp800_108_get_key_derivation_parameters(
@@ -129,7 +152,7 @@ def viewstate_validate(self, vkey_bytes, encrypted, viewstate_B64, generator, ur
129152
self.hash_algs[hash_alg],
130153
)
131154

132-
if h.digest() == signature:
155+
if signature == h.digest():
133156
return hash_alg
134157

135158
return None
@@ -139,6 +162,7 @@ def resolve_args(self, args):
139162
generator_pattern = re.compile(r"^[A-F0-9]{8}$")
140163

141164
url = None
165+
viewstate_userkey = None
142166
generator = "0000"
143167

144168
for arg in args:
@@ -147,14 +171,15 @@ def resolve_args(self, args):
147171
generator = arg
148172
elif url_pattern.match(arg):
149173
url = arg
150-
174+
else:
175+
viewstate_userkey = arg
151176
# Remove query string from the URL, if any
152177
if url:
153178
url = urlsplit(url)._replace(query="").geturl()
154-
return generator, url
179+
return generator, url, viewstate_userkey
155180

156181
def check_secret(self, viewstate_B64, *args):
157-
generator, url = self.resolve_args(args)
182+
generator, url, viewstate_userkey = self.resolve_args(args)
158183

159184
if not self.identify(viewstate_B64):
160185
return None
@@ -182,7 +207,7 @@ def check_secret(self, viewstate_B64, *args):
182207

183208
for mode in ["DOTNET40", "DOTNET45"]:
184209
validationAlgo = self.viewstate_validate(
185-
binascii.unhexlify(vkey), encrypted, viewstate_B64, generator, url, mode
210+
binascii.unhexlify(vkey), encrypted, viewstate_B64, generator, url, mode, viewstate_userkey
186211
)
187212
if validationAlgo:
188213
if encrypted:
@@ -201,6 +226,8 @@ def check_secret(self, viewstate_B64, *args):
201226
product_string = f"Viewstate: {viewstate_B64}"
202227
if generator != "0000":
203228
product_string += f" Generator: {generator[::-1].hex().upper()}"
229+
if viewstate_userkey:
230+
product_string += f" ViewStateUserKey: {viewstate_userkey}"
204231
return {"secret": result, "product": product_string, "details": f"Mode [{mode}]"}
205232
return None
206233

badsecrets/modules/telerik_encryptionkey.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,11 @@ def check_secret(self, dialogParameters_raw, key_derive_mode=None, include_machi
114114
}
115115
return None
116116

117-
def encryptionkey_probe_generator(self, hash_key, key_derive_mode, include_machinekeys=False):
117+
def encryptionkey_probe_generator(self, hash_key, key_derive_mode, include_machinekeys=False, custom_keys=None):
118118
test_string = b"AAAAAAAAAAAAAAAAAAAA"
119119
dp_enc = base64.b64encode(test_string).decode()
120120

121-
for ekey in self.prepare_keylist(include_machinekeys=include_machinekeys):
121+
for ekey in custom_keys if custom_keys else self.prepare_keylist(include_machinekeys=include_machinekeys):
122122
derivedKey, derivedIV = self.telerik_derivekeys(ekey, key_derive_mode)
123123
ct = self.telerik_encrypt(derivedKey, derivedIV, dp_enc)
124124
h = hmac.new(hash_key.encode(), ct.encode(), self.hash_algs["SHA256"])

badsecrets/modules/telerik_hashkey.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,10 @@ def get_hashcat_commands(self, dialogParameters_raw, *args):
6666
}
6767
]
6868

69-
def hashkey_probe_generator(self, include_machinekeys=False):
69+
def hashkey_probe_generator(self, include_machinekeys=False, custom_keys=None):
7070
test_string = b"EnableAsyncUpload,False,3,True;DeletePaths,True,0,Zmk4dUx3PT0sZmk4dUx3PT0=;EnableEmbeddedBaseStylesheet,False,3,True;RenderMode,False,2,2;UploadPaths,True,0,Zmk4dUx3PT0sZmk4dUx3PT0=;SearchPatterns,True,0,S2k0cQ==;EnableEmbeddedSkins,False,3,True;MaxUploadFileSize,False,1,204800;LocalizationPath,False,0,;FileBrowserContentProviderTypeName,False,0,;ViewPaths,True,0,Zmk4dUx3PT0sZmk4dUx3PT0=;IsSkinTouch,False,3,False;ScriptManagerProperties,False,0,CgoKCkZhbHNlCjAKCgoK;ExternalDialogsPath,False,0,;Language,False,0,ZW4tVVM=;Telerik.DialogDefinition.DialogTypeName,False,0,VGVsZXJpay5XZWIuVUkuRWRpdG9yLkRpYWxvZ0NvbnRyb2xzLkRvY3VtZW50TWFuYWdlckRpYWxvZywgVGVsZXJpay5XZWIuVUksIFZlcnNpb249MjAxOC4xLjExNy40NSwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj0xMjFmYWU3ODE2NWJhM2Q0;AllowMultipleSelection,False,3,False"
7171
dp_enc = base64.b64encode(test_string)
72-
for vkey in self.prepare_keylist(include_machinekeys=include_machinekeys):
72+
for vkey in custom_keys if custom_keys else self.prepare_keylist(include_machinekeys=include_machinekeys):
7373
h = hmac.new(vkey.encode(), dp_enc, self.hash_algs["SHA256"])
7474
yield (f"{dp_enc.decode()}{base64.b64encode(h.digest()).decode()}", vkey)
7575

0 commit comments

Comments
 (0)