Created
April 28, 2025 14:30
-
-
Save sam2332/2555ec8af7943bbfdb4820886b3e4c3f to your computer and use it in GitHub Desktop.
Decrypt web.config to cloned directory structure
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
""" | |
scan-decrypt-configs.py | |
‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |
Recursively looks for Web.config files that contain encrypted | |
sections (identified by `configProtectionProvider=` or <EncryptedData>), | |
then decrypts each protected section into a mirror tree under ./decrypted/ | |
❱❱ python scan-decrypt-configs.py D:\inetpub\wwwroot --dry-run | |
❱❱ python scan-decrypt-configs.py D:\inetpub\wwwroot | |
""" | |
import argparse, os, subprocess, xml.etree.ElementTree as ET, logging, sys | |
FRAMEWORK = r"C:\Windows\Microsoft.NET\Framework64\v4.0.30319\aspnet_regiis.exe" | |
DECRYPTED_ROOT = os.path.abspath("decrypted") | |
logging.basicConfig(level=logging.INFO, | |
format="%(levelname)s %(message)s", | |
handlers=[logging.StreamHandler(sys.stdout)]) | |
def encrypted_sections(config_path: str): | |
tree = ET.parse(config_path) | |
root = tree.getroot() | |
pmap = {c: p for p in tree.iter() for c in p} # 1-liner parent map | |
sects = set() | |
for elem in root.iter(): | |
if 'configProtectionProvider' in elem.attrib: | |
sects.add(elem.tag.split('}')[-1]) | |
if elem.tag.endswith('EncryptedData'): | |
parent = pmap.get(elem) | |
if parent is not None: | |
sects.add(parent.tag.split('}')[-1]) | |
return sects | |
def decrypt_section(app_root: str, section: str): | |
"""aspnet_regiis -pdf <section> <physical path>""" | |
app_root = os.path.abspath(app_root) | |
cmd = [FRAMEWORK, "-pdf", section, app_root] | |
logging.debug("CMD %s", " ".join(cmd)) | |
subprocess.check_call(cmd) | |
def copy_web_config(source: str, dest: str): | |
os.makedirs(os.path.dirname(dest), exist_ok=True) | |
with open(source, "rb") as fsrc, open(dest, "wb") as fdst: | |
fdst.write(fsrc.read()) | |
def walk_and_decrypt(root: str, dry: bool): | |
root = os.path.abspath(root) | |
for dirpath, _, files in os.walk(root): | |
if "web.config" not in [f.lower() for f in files]: | |
continue | |
cfg = os.path.join(dirpath, "web.config") | |
sects = encrypted_sections(cfg) | |
if not sects: | |
continue | |
relative_path = os.path.relpath(dirpath, root) | |
decrypted_path = os.path.join(DECRYPTED_ROOT, relative_path) | |
decrypted_cfg = os.path.join(decrypted_path, "web.config") | |
copy_web_config(cfg, decrypted_cfg) | |
logging.info("⤷ %s ➜ %s", decrypted_cfg, ", ".join(sects)) | |
if dry: | |
continue | |
for s in sects: | |
decrypt_section(decrypted_path, s) | |
if __name__ == "__main__": | |
p = argparse.ArgumentParser() | |
p.add_argument("root", help="Root directory to scan (e.g. D:\\inetpub\\wwwroot)") | |
p.add_argument("--dry-run", action="store_true", help="List only; don’t decrypt") | |
args = p.parse_args() | |
walk_and_decrypt(args.root, args.dry_run) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment