Skip to content

Commit f0a4f7b

Browse files
authored
Merge branch 'dev' into dependabot/pip/django-4.2.22
2 parents f1abc35 + be0c3a3 commit f0a4f7b

8 files changed

Lines changed: 563 additions & 343 deletions

File tree

.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

badsecrets/base.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,15 @@ 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+
try:
98+
cookies = {cookie.name: cookie.value for cookie in requests_response.cookies}
99+
except (AttributeError, KeyError):
100+
cookies = {}
101+
if not headers:
102+
headers = requests_response.headers
103+
if not body and hasattr(requests_response, "text"):
104+
body = requests_response.text
99105
else:
100106
raise badsecrets.errors.CarveException("requests_response must be a requests.models.Response object")
101107

@@ -142,7 +148,9 @@ def carve(self, body=None, cookies=None, headers=None, requests_response=None, *
142148
if self.carve_regex():
143149
s = re.search(self.carve_regex(), body)
144150
if s:
145-
r = self.carve_to_check_secret(s, url=kwargs.get("url", None))
151+
r = self.carve_to_check_secret(
152+
s, url=kwargs.get("url", None), body=body, cookies=cookies, headers=headers
153+
)
146154
if r:
147155
r["type"] = "SecretFound"
148156
else:

badsecrets/modules/aspnet_viewstate.py

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,22 @@ 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+
33+
possible_userkey_cookies = ["ASP.NET_SessionId", "__AntiXsrfToken", "ASPSESSIONID"]
34+
35+
if kwargs.get("cookies") and hasattr(kwargs.get("cookies"), "get"):
36+
for cookie_name in possible_userkey_cookies:
37+
if cookie_name in kwargs.get("cookies"):
38+
cookie_value = kwargs.get("cookies").get(cookie_name)
39+
r = self.check_secret(viewstate, generator, url, cookie_value)
40+
if r:
41+
return r
42+
43+
return self.check_secret(viewstate, generator, url)
3244

3345
@staticmethod
3446
def valid_preamble(sourcebytes):
@@ -92,7 +104,8 @@ def viewstate_decrypt(self, ekey_bytes, hash_alg, viewstate_B64, url, mode):
92104
else:
93105
continue
94106

95-
def viewstate_validate(self, vkey_bytes, encrypted, viewstate_B64, generator, url, mode):
107+
def viewstate_validate(self, vkey_bytes, encrypted, viewstate_B64, generator, url, mode, viewstate_userkey=None):
108+
96109
original_vkey_bytes = vkey_bytes
97110
viewstate_bytes = base64.b64decode(viewstate_B64)
98111

@@ -104,19 +117,32 @@ def viewstate_validate(self, vkey_bytes, encrypted, viewstate_B64, generator, ur
104117
signature_len = len(vs.signature)
105118
candidate_hash_algs = self.search_dict(self.hash_sizes, signature_len)
106119

120+
modifier_bytes = b"\x00" * 4
121+
if viewstate_userkey and viewstate_userkey.strip():
122+
modifier_bytes += viewstate_userkey.encode("utf-16le")
107123
for hash_alg in candidate_hash_algs:
108124
vkey_bytes = original_vkey_bytes
109125
viewstate_data = viewstate_bytes[: -self.hash_sizes[hash_alg]]
110126
signature = viewstate_bytes[-self.hash_sizes[hash_alg] :]
111127
if hash_alg == "MD5":
112-
md5_bytes = viewstate_data + vkey_bytes
113128
if not encrypted:
114-
md5_bytes += b"\x00" * 4
129+
if viewstate_userkey and viewstate_userkey.strip():
130+
md5_bytes = b"will not work, sorry"
131+
# MD5 + ViewStateUserKey is a horrible edge case that may NEVER work. We will not match on it, currently.
132+
# Last attempt:
133+
# md5_bytes = viewstate_data + vkey_bytes + page_hash_bytes + viewstate_userkey.encode('utf-16le')
134+
# But page_hash_bytes is apparently NOT the generator and my have to be brute-forced.
135+
# Probably not worth it for the 3 servers in the entire world probably using these settings in the wild.
136+
else:
137+
md5_bytes = viewstate_data + vkey_bytes + modifier_bytes
138+
else:
139+
md5_bytes = viewstate_data + vkey_bytes
115140
h = hashlib.md5(md5_bytes)
116141
else:
117142
vs_data_bytes = viewstate_data
118143
if not encrypted:
119144
vs_data_bytes += generator
145+
vs_data_bytes += modifier_bytes[4:]
120146
if mode == "DOTNET45" and url:
121147
s = Simulate_dotnet45_kdf_context_parameters(url)
122148
label, context = sp800_108_get_key_derivation_parameters(
@@ -129,7 +155,9 @@ def viewstate_validate(self, vkey_bytes, encrypted, viewstate_B64, generator, ur
129155
self.hash_algs[hash_alg],
130156
)
131157

132-
if h.digest() == signature:
158+
computed_hash = h.digest()
159+
160+
if computed_hash == signature:
133161
return hash_alg
134162

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

141169
url = None
170+
viewstate_userkey = None
142171
generator = "0000"
143172

144173
for arg in args:
@@ -147,14 +176,15 @@ def resolve_args(self, args):
147176
generator = arg
148177
elif url_pattern.match(arg):
149178
url = arg
150-
179+
else:
180+
viewstate_userkey = arg
151181
# Remove query string from the URL, if any
152182
if url:
153183
url = urlsplit(url)._replace(query="").geturl()
154-
return generator, url
184+
return generator, url, viewstate_userkey
155185

156186
def check_secret(self, viewstate_B64, *args):
157-
generator, url = self.resolve_args(args)
187+
generator, url, viewstate_userkey = self.resolve_args(args)
158188

159189
if not self.identify(viewstate_B64):
160190
return None
@@ -182,7 +212,7 @@ def check_secret(self, viewstate_B64, *args):
182212

183213
for mode in ["DOTNET40", "DOTNET45"]:
184214
validationAlgo = self.viewstate_validate(
185-
binascii.unhexlify(vkey), encrypted, viewstate_B64, generator, url, mode
215+
binascii.unhexlify(vkey), encrypted, viewstate_B64, generator, url, mode, viewstate_userkey
186216
)
187217
if validationAlgo:
188218
if encrypted:
@@ -201,6 +231,8 @@ def check_secret(self, viewstate_B64, *args):
201231
product_string = f"Viewstate: {viewstate_B64}"
202232
if generator != "0000":
203233
product_string += f" Generator: {generator[::-1].hex().upper()}"
234+
if viewstate_userkey:
235+
product_string += f" ViewStateUserKey: {viewstate_userkey}"
204236
return {"secret": result, "product": product_string, "details": f"Mode [{mode}]"}
205237
return None
206238

0 commit comments

Comments
 (0)