From e6462a5412863cde15d5dbeeddede32dcf8effcc Mon Sep 17 00:00:00 2001 From: redblaze-chtsec Date: Fri, 28 Mar 2025 16:24:33 +0800 Subject: [PATCH 1/2] add .NET 4.5 algorithm --- README.md | 8 ++++ sp800_108.py | 61 +++++++++++++++++++++++++ viewgen | 123 ++++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 176 insertions(+), 16 deletions(-) create mode 100644 sp800_108.py diff --git a/README.md b/README.md index 9d8e5ed..14c9218 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,8 @@ options: -e, --encrypted ViewState is encrypted -f FILE, --file FILE read ViewState payload from file --version show viewgen version + --path PATH target web page(only for .NET >= 4.5) + --apppath APPPATH application path(only for .NET >= 4.5) ``` --------------- @@ -74,6 +76,12 @@ $ viewgen --decode --check --webconfig web.config --modifier CA0B0334 "zUylqfbpW $ viewgen --webconfig web.config --modifier CA0B0334 "/wEPDwUKMTYyODkyNTEzMw9kFgICAw8WAh4HZW5jdHlwZQUTbXVsdGlwYXJ0L2Zvcm0tZGF0YWRk" r4zCP5CdSo5R9XmiEXvp1LHVzX1uICmY7oW2WD/gKS/Mt/s+NKXrMpScr4Gvrji7lFdHPOttFpi2x7YbmQjEjJ2NdBMuzeKFzIuno2DenYF8yVVKx5+LL7LYmI0CVcNQ+jH8VxvzVG58NQIJ/rSr6NqNMBahrVfAyVPgdL4Eke3Bq4XWk6BYW2Bht6ykSHF9szT8tG6KUKwf+T94hFUFNIXXkURptwQJEC/5AMkFXMU0VXDa +$ viewgen --check -e --vkey '6F16768FDF1E1E4A23CE58625C761F55FD13A1681F7AE3BDB4BB8F99874C7485' --dkey 'F42E58ECE66428868A69ED1F5B6721364A3FC3C490C7FFAAE1EAE96BBA45C88D' "EZgmsZEuppW0eZTumHR9toJTc4MDPJ0CzjIVt6u3FO8TQLDiFbGqvHwKAz72nGrhrCZP3rYE1uXqeU160jZo7xY1v48MACMKLrPiljD3yaM=" --dalg AES --valg SHA256 --decode --path '/default.aspx' --apppath '/' +[+] ViewState +(('91245346', None), None) +[+] Signature: ac264fdeb604d6e5ea794d7ad23668ef1635bf8f0c00230a2eb3e29630f7c9a3 +[+] Signature match + $ viewgen --guess "/wEPDwUKMTYyODkyNTEzMw9kFgICAw8WAh4HZW5jdHlwZQUTbXVsdGlwYXJ0L2Zvcm0tZGF0YWRkuVmqYhhtcnJl6Nfet5ERqNHMADI=" [+] ViewState is not encrypted [+] Signature algorithm: SHA1 diff --git a/sp800_108.py b/sp800_108.py new file mode 100644 index 0000000..775d8aa --- /dev/null +++ b/sp800_108.py @@ -0,0 +1,61 @@ +import hmac +import hashlib +from struct import pack +import binascii + + +class SP800_108: + @staticmethod + def derive_key(key_derivation_key: bytes, purpose) -> bytes: + label, context = purpose.get_key_derivation_parameters() + return SP800_108.derive_key_impl(key_derivation_key, label, context, len(key_derivation_key) * 8) + + @staticmethod + def derive_key_impl(key: bytes, label: bytes, context: bytes, key_length_in_bits: int) -> bytes: + hmac_sha = hmac.new(key, digestmod=hashlib.sha512) + key_length_in_bytes = key_length_in_bits // 8 + + label = label if label else b'' + context = context if context else b'' + + # Construct fixed input data: Counter (4 bytes) || Label || 0x00 || Context || L (4 bytes) + fixed_input = label + b'\x00' + context + SP800_108.uint32_to_bytes(key_length_in_bits) + + derived_key = bytearray() + counter = 1 + + while len(derived_key) < key_length_in_bytes: + counter_bytes = SP800_108.uint32_to_bytes(counter) + data = counter_bytes + fixed_input + hmac_sha_copy = hmac_sha.copy() + hmac_result = hmac_sha_copy.update(data) + derived_key.extend(hmac_sha_copy.digest()) + counter += 1 + + return bytes(derived_key[:key_length_in_bytes]) + + @staticmethod + def uint32_to_bytes(value: int) -> bytes: + return pack(">I", value) + + +class Purpose: + def __init__(self, primary_purpose: str, specific_purposes: list): + self.primary_purpose = primary_purpose + self.specific_purposes = specific_purposes + self._derived_key_label = None + self._derived_key_context = None + + def get_key_derivation_parameters(self): + if self._derived_key_label is None: + self._derived_key_label = self.primary_purpose.encode('utf-8') + + if self._derived_key_context is None: + context_parts = [sp.encode('utf-8') for sp in self.specific_purposes] + prefix = len(context_parts[0]).to_bytes(1, 'big') + separator = len(context_parts[1]).to_bytes(1, 'big') + context_stream = prefix + separator.join(context_parts) + self._derived_key_context = context_stream + + return self._derived_key_label, self._derived_key_context + diff --git a/viewgen b/viewgen index 97281ff..aabc46f 100755 --- a/viewgen +++ b/viewgen @@ -5,6 +5,8 @@ from pprint import pprint from Crypto.Cipher import AES from Crypto.Cipher import DES from Crypto.Cipher import DES3 +from Crypto.Util.Padding import unpad +from Crypto.Util.Padding import pad from viewstate import ViewState from xml.dom import minidom from colored import fg, attr @@ -18,14 +20,11 @@ import binascii import struct import sys import urllib.parse - +import sp800_108 VERSION = 0.3 -pad = lambda s, bs: s + (bs - len(s) % bs) * chr(bs - len(s) % bs).encode("ascii") -unpad = lambda s: s[:-ord(s[len(s)-1:])] - def success(s): print("[%s+%s] %s%s%s%s" % (fg("light_green"), attr(0), attr(1), s, attr(21), attr(0))) @@ -40,20 +39,32 @@ class ViewGen: hash_algs = {"SHA1": hashlib.sha1, "MD5": hashlib.md5, "SHA256": hashlib.sha256, "SHA384": hashlib.sha384, "SHA512": hashlib.sha512, "AES": hashlib.sha1, "3DES": hashlib.sha1} hash_sizes = {"SHA1": 20, "MD5": 16, "SHA256": 32, "SHA384": 48, "SHA512": 64, "AES": 20, "3DES": 20} - def __init__(self, validation_key=None, validation_alg=None, dec_key=None, dec_alg=None, modifier=None, encrypted=False, viewstateuserkey=None): + def __init__(self, validation_key=None, validation_alg=None, dec_key=None, dec_alg=None, modifier=None, encrypted=False, viewstateuserkey=None, path=None, apppath=None): self.validation_key = validation_key self.dec_key = dec_key self._init_validation_alg(validation_alg) self._init_dec_alg(dec_alg) self.encrypted = encrypted + self.path = path + self.apppath = apppath if modifier is None: - self.modifier = ViewGen.MD5_MODIFIER + if not apppath and not path: + self.modifier = ViewGen.MD5_MODIFIER + else: + self.encrypted = True + self.modifier = "" + self.purpose = sp800_108.Purpose("WebForms.HiddenFieldPageStatePersister.ClientState", ["TemplateSourceDirectory: " + simulate_template_source_directory(path, is_debug=False), "Type: " + simulate_get_type_name(path, apppath, is_debug=False)]) + self.dec_key = sp800_108.SP800_108.derive_key(dec_key, self.purpose) + self.validation_key = sp800_108.SP800_108.derive_key(validation_key, self.purpose) else: self.modifier = struct.pack(" str: + if not str_path.startswith("/"): + str_path = "/" + str_path + + result = str_path + + if result.rfind(".") > result.rfind("/"): + result = result[:result.rfind("/") + 1] + + result = remove_slash_from_path_if_needed(result) + + if is_debug: + print(f"simulateTemplateSourceDirectory returns: {result}") + + return result + + +def simulate_get_type_name(str_path: str, IIS_app_in_path: str, is_debug: bool = False) -> str: + if not str_path.startswith("/"): + str_path = "/" + str_path + + result = str_path + + if not result.lower().endswith(".aspx"): + result += "/default.aspx" + + IIS_app_in_path = IIS_app_in_path.lower() + + if not IIS_app_in_path.startswith("/"): + IIS_app_in_path = "/" + IIS_app_in_path + + if not IIS_app_in_path.endswith("/"): + IIS_app_in_path += "/" + + if IIS_app_in_path in result.lower(): + result = result[result.lower().index(IIS_app_in_path) + len(IIS_app_in_path):] + + if result.startswith("/"): + result = result[1:] + + result = result.replace(".", "_").replace("/", "_") + + result = remove_slash_from_path_if_needed(result) + + if is_debug: + print(f"simulateGetTypeName returns: {result}") + + return result.upper() + + +def remove_slash_from_path_if_needed(path: str) -> str: + if not path: + return None + + length = len(path) + + if length <= 1 or path[-1] != '/': + return path + + return path[:-1] + + if __name__ == "__main__": args = parse_args() run_viewgen(args) From 5f5d978e3c35656cd9c82c8d3aa9dc219a825d57 Mon Sep 17 00:00:00 2001 From: redblaze-chtsec Date: Tue, 8 Apr 2025 17:19:37 +0800 Subject: [PATCH 2/2] Bug fixes --- README.md | 3 ++- viewgen | 8 +++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 14c9218..76d6b4a 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,8 @@ $ viewgen -h usage: viewgen [-h] [--webconfig WEBCONFIG] [-m MODIFIER] [--viewstateuserkey VIEWSTATEUSERKEY] [-c COMMAND] [--decode] [--guess] [--check] [--vkey VKEY] [--valg VALG] [--dkey DKEY] - [--dalg DALG] [-u] [-e] [-f FILE] [--version] + [--dalg DALG] [-u] [-e] [-f FILE] [--version] [--path PATH] + [--apppath APPPATH] [payload] viewgen is a ViewState tool capable of generating both signed and encrypted diff --git a/viewgen b/viewgen index aabc46f..2b127c2 100755 --- a/viewgen +++ b/viewgen @@ -137,9 +137,7 @@ class ViewGen: enc = data[AES.block_size:-hash_size] cipher = AES.new(self.dec_key, AES.MODE_CBC, iv) block_size = AES.block_size - random_bytes_size = 0 - if not self.path and not self.apppath: - random_bytes_size = len(self.dec_key) - AES.block_size + random_bytes_size = len(self.dec_key) - AES.block_size elif self.dec_alg == "DES": iv = data[0:DES.block_size] enc = data[DES.block_size:-hash_size] @@ -151,7 +149,7 @@ class ViewGen: enc = data[DES3.block_size:-hash_size] cipher = DES3.new(self.dec_key[:24], DES3.MODE_CBC, iv) block_size = DES3.block_size - random_bytes_size = 0 + random_bytes_size = len(self.dec_key) - DES3.block_size else: return None @@ -447,7 +445,7 @@ def remove_slash_from_path_if_needed(path: str) -> str: if length <= 1 or path[-1] != '/': return path - return path[:-1] + return path[:-1].upper() if __name__ == "__main__":