Skip to content

Instantly share code, notes, and snippets.

@sam2332
Created April 28, 2025 14:30
Show Gist options
  • Save sam2332/2555ec8af7943bbfdb4820886b3e4c3f to your computer and use it in GitHub Desktop.
Save sam2332/2555ec8af7943bbfdb4820886b3e4c3f to your computer and use it in GitHub Desktop.
Decrypt web.config to cloned directory structure
#!/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