Skip to content
Merged
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
34 changes: 19 additions & 15 deletions badsecrets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,21 +190,25 @@ def _carve_body(self, body, cookies, headers, **kwargs):
"""Extract secrets from HTML body text. Override in subclasses for custom body carving."""
results = []
if self.carve_regex():
s = re.search(self.carve_regex(), body)
if s:
if not self.validate_carve or self.identify(s.groups()[0]):
r = self.carve_to_check_secret(
s, url=kwargs.get("url"), body=body, cookies=cookies, headers=headers
)
if r:
r["type"] = "SecretFound"
else:
r = {"type": "IdentifyOnly"}
r["hashcat"] = self._safe_hashcat(s.groups()[0])
if "product" not in r:
r["product"] = self.get_product_from_carve(s)
r["location"] = "body"
results.append(r)
# Walk every match rather than only the first. A page can carry several fields the
# carve regex matches -- an empty or decoy __VIEWSTATE alongside a real payload --
# and stopping at match #1 lets whichever appears first in the markup hide the rest.
for s in re.finditer(self.carve_regex(), body):
if self.validate_carve and not self.identify(s.groups()[0]):
continue
r = self.carve_to_check_secret(s, url=kwargs.get("url"), body=body, cookies=cookies, headers=headers)
if r:
r["type"] = "SecretFound"
else:
r = {"type": "IdentifyOnly"}
r["hashcat"] = self._safe_hashcat(s.groups()[0])
if "product" not in r:
r["product"] = self.get_product_from_carve(s)
r["location"] = "body"
results.append(r)
# First candidate that identifies wins, keeping the one-result-per-body
# contract and avoiding repeat check_secret() work on expensive modules.
break
return results

@classmethod
Expand Down
8 changes: 6 additions & 2 deletions badsecrets/modules/passive/aspnet_compressedviewstate.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@ class ASPNET_compressedviewstate(BadsecretsBase):
yara_carve_rule = (
"rule ASPNET_compressedviewstate_carve {"
' strings: $vs = "__VIEWSTATE" $vstate = "__VSTATE" $cvs = "__COMPRESSEDVIEWSTATE"'
" condition: $vs or $vstate or $cvs }"
' $cvs_u = "__COMPRESSED_VSTATE"'
" condition: $vs or $vstate or $cvs or $cvs_u }"
)
description = {"product": "ASP.NET Compressed Viewstate", "secret": "unprotected", "severity": "CRITICAL"}
carve_locations = ("body",)

def carve_regex(self):
return re.compile(r"<input[^>]+__(?:VIEWSTATE|VSTATE|COMPRESSEDVIEWSTATE)\"\s*value=\"(.*?)\"")
return re.compile(
r"<input(?=[^>]*__(?:COMPRESSEDVIEWSTATE|COMPRESSED_VSTATE|VIEWSTATE|VSTATE)\")"
r"[^>]*?\svalue=\"(.*?)\""
)

def check_secret(self, compressed_viewstate):
if not self.identify(compressed_viewstate):
Expand Down
2 changes: 1 addition & 1 deletion badsecrets/modules/passive/jsf_viewstate.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class Jsf_viewstate(BadsecretsBase):
carve_locations = ("body",)

def carve_regex(self):
return re.compile(r"<input.+?name=\"javax\.faces\.ViewState\".+?value=\"([^\"]*)\"")
return re.compile(r"<input(?=[^>]*name=\"javax\.faces\.ViewState\")[^>]*?\svalue=\"([^\"]*)\"")

# Mojarra 1.2.x - 2.0.3
def DES3_decrypt(self, ct, password):
Expand Down
71 changes: 71 additions & 0 deletions tests/aspnet_compressedviewstate_test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import gzip
import time
import base64

from badsecrets import modules_loaded
Expand Down Expand Up @@ -89,6 +90,76 @@ def test_aspnet_compressedviewstate_carve_compressedviewstate_field():
assert results[0]["type"] == "SecretFound"


def test_aspnet_compressedviewstate_carve_compressed_vstate_field():
"""Carve from the __COMPRESSED_VSTATE (underscore) hidden field."""
body = f'<input type="hidden" name="__COMPRESSED_VSTATE" id="__COMPRESSED_VSTATE" value="{KNOWN_GOOD}" />'
x = ASPNETcompressedviewstate()
results = x.carve(body=body)
assert len(results) > 0
assert results[0]["type"] == "SecretFound"
assert results[0]["location"] == "body"


def test_aspnet_compressedviewstate_carve_not_shadowed_by_empty_viewstate():
"""An empty __VIEWSTATE field must not hide a real payload, in either document order."""
payload = f'<input type="hidden" name="__COMPRESSED_VSTATE" id="__COMPRESSED_VSTATE" value="{KNOWN_GOOD}" />'
empty = '<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="" />'
x = ASPNETcompressedviewstate()
for body in (payload + empty, empty + payload):
results = x.carve(body=body)
secret_results = [r for r in results if r["type"] == "SecretFound"]
assert len(secret_results) == 1, f"missed payload for body order: {body[:60]}"
assert secret_results[0]["product"] == KNOWN_GOOD


def test_aspnet_compressedviewstate_carve_all_modules_compressed_vstate():
"""__COMPRESSED_VSTATE next to an empty __VIEWSTATE, as seen in the wild."""
body = (
'<form name="aspnetForm" method="post" action="./Error.aspx" id="aspnetForm">\n'
f'<input type="hidden" name="__COMPRESSED_VSTATE" id="__COMPRESSED_VSTATE" value="{KNOWN_GOOD}" />\n'
'<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="" />\n'
"</form>"
)
results = carve_all_modules(body=body)
found = [r for r in results if r["detecting_module"] == "ASPNET_compressedviewstate"]
assert len(found) == 1
assert found[0]["type"] == "SecretFound"
assert found[0]["description"]["severity"] == "CRITICAL"


def test_aspnet_compressedviewstate_carve_attribute_between_name_and_value():
"""Unrelated attributes between name= and value= must not defeat the carve."""
body = f'<input type="hidden" name="__COMPRESSED_VSTATE" class="x" data-y="z" value="{KNOWN_GOOD}" />'
x = ASPNETcompressedviewstate()
results = x.carve(body=body)
assert len(results) > 0
assert results[0]["type"] == "SecretFound"


def test_aspnet_compressedviewstate_carve_does_not_cross_tag_boundary():
"""A name in one tag must not pair with a value in the next."""
body = '<input type="hidden" name="__VIEWSTATE"><input type="hidden" value="junk">'
x = ASPNETcompressedviewstate()
assert x.carve(body=body) == []


def test_aspnet_compressedviewstate_carve_regex_no_catastrophic_backtracking():
"""Many name hits in one unclosed tag must stay linear, not O(n^2)."""
body = "<input " + ('name="__VIEWSTATE" ' * 8000) + "x"
x = ASPNETcompressedviewstate()
start = time.perf_counter()
x.carve_regex().search(body)
elapsed = time.perf_counter() - start
assert elapsed < 2.0, f"carve regex took {elapsed:.2f}s on 150KB of adversarial input"


def test_aspnet_compressedviewstate_carve_viewstategenerator_not_matched():
"""__VIEWSTATEGENERATOR must not be mistaken for a viewstate field."""
body = f'<input type="hidden" name="__VIEWSTATEGENERATOR" id="__VIEWSTATEGENERATOR" value="{KNOWN_GOOD}" />'
x = ASPNETcompressedviewstate()
assert x.carve(body=body) == []


def test_aspnet_compressedviewstate_carve_bad_value():
"""Carve with a non-compressed value in the field should not return SecretFound."""
body = '<input type="hidden" name="__VIEWSTATE" value="dGhpcyBpcyBub3QgZ3ppcA==">'
Expand Down
Loading