Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,26 @@ badsecrets --url http://example.com/contains_bad_secret.html

You can also set a custom user-agent with `--user-agent "user-agent string"` or a proxy with `--proxy http://127.0.0.1` in this mode.

* ASP.NET Viewstate: supply the URL

The `ASPNET_Viewstate` module accepts up to four positional arguments — the viewstate, the `__VIEWSTATEGENERATOR` value, the page URL, and a `ViewStateUserKey`. Order does not matter; each is identified by its shape (8 hex characters is the generator, anything starting with `http://`/`https://` is the URL, anything else is the user key).

```bash
badsecrets <viewstate> <generator> <url> [viewstateuserkey]
```

**The URL is not optional for every viewstate.** .NET 4.5 (`DOTNET45`) binds the validation key to the page path through the SP800-108 KDF purpose strings, and `DOTNET40` with `IsolateApps` enabled mixes in an app-path hash. Both are computed from the URL. If you supply only the viewstate and generator, badsecrets has no path to derive from and the HMAC can never validate — you will get `No secrets found :(` even when the machine key is in the list.

```bash
# Reports nothing on this DOTNET45 viewstate - no path to derive the key from
badsecrets 3RP87RgckNbfc7fNdaHzH9YLqbIhzpA64gFfWB49lhzHpuHFAAO7C7Dl2zh0dWUqobBb4hwiZJ1a2bhP77aQiwqvVqg= 9BD98A7D

# Same viewstate, with the URL - key is found
badsecrets 3RP87RgckNbfc7fNdaHzH9YLqbIhzpA64gFfWB49lhzHpuHFAAO7C7Dl2zh0dWUqobBb4hwiZJ1a2bhP77aQiwqvVqg= 9BD98A7D http://10.1.1.43/default2.aspx
```

This is the usual reason `--url` mode succeeds where passing the same viewstate and generator by hand does not: URL mode always knows the page path. URL mode additionally tries several `ViewStateUserKey` candidates automatically (the empty string, `mono`, any `__VIEWSTATE_KEY` field, and each cookie value on the response), which the manual path does not — so pass the user key explicitly if you know it.

Example output:

```bash
Expand Down
11 changes: 8 additions & 3 deletions badsecrets/modules/passive/aspnet_viewstate.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@


class ASPNET_Viewstate(BadsecretsBase):
check_secret_args = 3
# viewstate, generator, url, ViewStateUserKey. A DOTNET45 viewstate needs the URL to derive
# the KDF purposes, so capping this below 4 made url+userkey impossible to supply together.
check_secret_args = 4
# Lower minimum than generic_base64_regex (8 groups) to match short MAC_DISABLED viewstates
identify_regex = re.compile(
r"^(?:[A-Za-z0-9+\/]{4}){4,}(?:[A-Za-z0-9+\/]{4}|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{2}={2})$"
Expand Down Expand Up @@ -57,7 +59,9 @@ class ASPNET_Viewstate(BadsecretsBase):

# Pre-compiled regexes for resolve_args
_url_pattern = re.compile(r"http[s]?://(?:[a-zA-Z]|[0-9]|[$\-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+")
_generator_pattern = re.compile(r"^[A-F0-9]{8}$")
# Case-insensitive: a lowercase generator is still a generator. Matching only uppercase let
# one fall through to the ViewStateUserKey branch, silently checking the wrong thing.
_generator_pattern = re.compile(r"^[A-F0-9]{8}$", re.IGNORECASE)

def carve_regex(self):
return self._carve_re_normal
Expand Down Expand Up @@ -374,7 +378,8 @@ def resolve_args(self, args):
for arg in args:
if arg:
if self._generator_pattern.match(arg):
generator = arg
# Canonicalize so downstream path brute-forcing and result strings agree
generator = arg.upper()
elif self._url_pattern.match(arg):
url = arg
else:
Expand Down
32 changes: 32 additions & 0 deletions tests/aspnet_viewstate_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -792,3 +792,35 @@ def test_dotnet45_viewstate_userkey_carve():
assert r_list
found_secret = any(r["type"] == "SecretFound" for r in r_list)
assert found_secret


def test_resolve_args_generator_case_insensitive():
"""A lowercase generator is still a generator, not a ViewStateUserKey."""
x = ASPNETViewstate()
for supplied in ("9BD98A7D", "9bd98a7d", "9Bd98A7d"):
generator, url, userkey = x.resolve_args((supplied,))
assert generator == "9BD98A7D", f"{supplied} resolved to generator {generator}"
assert url is None
assert userkey is None


def test_lowercase_generator_still_cracks():
"""Same viewstate must crack whether the generator is upper or lower case."""
x = ASPNETViewstate()
viewstate = "/wEPDwUJODc0MjgwMjkwZGTCdzCrBtl0AFYdKsWX1bQ8DcMilw=="
url = "http://10.1.1.43/default2.aspx"
for supplied in ("9BD98A7D", "9bd98a7d"):
found_key = x.check_secret(viewstate, supplied, url)
assert found_key, f"failed to crack with generator {supplied}"
assert test_vkey in found_key["secret"]


def test_check_all_modules_passes_url_and_userkey_together():
"""check_secret_args must be wide enough to carry viewstate+generator+url+userkey."""
from badsecrets.base import check_all_modules

results = check_all_modules(vsk_viewstate, vsk_generator, vsk_url, vsk_session_id)
assert results
found = [r for r in results if r["type"] == "SecretFound" and r["detecting_module"] == "ASPNET_Viewstate"]
assert len(found) == 1
assert f"ViewStateUserKey: {vsk_session_id}" in found[0]["product"]
Loading