diff --git a/README.md b/README.md index ed0e32c..ec199ee 100644 --- a/README.md +++ b/README.md @@ -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 [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 diff --git a/badsecrets/modules/passive/aspnet_viewstate.py b/badsecrets/modules/passive/aspnet_viewstate.py index d0df676..bb6e522 100644 --- a/badsecrets/modules/passive/aspnet_viewstate.py +++ b/badsecrets/modules/passive/aspnet_viewstate.py @@ -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})$" @@ -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 @@ -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: diff --git a/tests/aspnet_viewstate_test.py b/tests/aspnet_viewstate_test.py index 2f230ce..9fc2114 100644 --- a/tests/aspnet_viewstate_test.py +++ b/tests/aspnet_viewstate_test.py @@ -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"]