Privilege Escalation Emulation
Definition
Section titled “Definition”Privilege escalation emulation is the controlled exercise of the techniques adversaries use to move from a low-privileged foothold to elevated execution — service misconfigurations (unquoted paths, weak ACLs), UAC bypasses, token manipulation and impersonation (T1134), the Potato-family relay attacks, always-install-elevated abuse — on lab hosts where the misconfigurations are deliberately planted by you, exploited under full manifest discipline, and measured against the elevation telemetry your estate should produce. The emulation shape differs from other modules in one structural way: the vulnerabilities here are configuration states, not payloads, so the exercise splits into two halves — a read-only surface scanner (safe on any host, finds the states an adversary would find) and a controlled exploitation harness (lab-only, exploits only what it planted, cleans up by manifest). The measurement target is the elevation event stream: 4688 elevated processes, 4673/4674 sensitive-privilege use, Sysmon 1 parent-child elevation anomalies, UAC consent decisions.
Why It Matters
Section titled “Why It Matters”Privilege escalation is the hinge of every intrusion — the moment a foothold becomes a domain-capable position — and it is disproportionately under-tested because the techniques feel exotic while the conditions for them are embarrassingly common: unquoted service paths, writable service binaries, mismatched service ACLs, and stale always-install-elevated settings exist on real estates at rates that every hardened team finds surprising the first time they scan. Emulation delivers three distinct measurements: the surface (how many elevation paths does your estate offer? — the scanner’s report, no exploitation needed), the detection (when elevation happens, does your event stream see it with enough fidelity to reconstruct? — the harness’s after-action), and the control effectiveness (do your tiering, LAPS, and UAC configurations actually prevent the paths your surface scan found? — the remediation validation loop). The blue team’s baseline knowledge also upgrades permanently: working through elevation emulation teaches exactly which event pairs distinguish “admin did admin things” from “user became admin”, a distinction that generic SIEM correlation almost never encodes. And because elevation techniques are the most patched-and-deprecated class in the catalog (UAC bypasses die with OS updates, Potato variants die with RPC hardening), the emulation program doubles as a regression test for the estate’s hardening drift — a path that worked last quarter and still works this quarter is an unpatched finding, not bad luck.
Detailed Explanation
Section titled “Detailed Explanation”Windows elevation decomposes into a small number of condition classes, each with its emulation shape:
ELEVATION CONDITION CLASSES -> EMULATION SHAPES [1] Service misconfigurations unquoted path w/ space -> plant svc "C:\lab dir\svc.exe" weak service ACL -> plant svc, grant user start/ChangeConfig writable binPath binary -> plant + ACL the binary telemetry if exploited: 7045 (reconfig), 4688 (svc proc as SYSTEM), Sysmon 1 with user parent [2] UAC paths (medium -> high IL) auto-elevate abuse -> fodhelper/ComputerDefaults shape telemetry: 4688 with elevated token, consent.exe absent for auto-elevate (that absence IS the detection signal) [3] Token manipulation (T1134) make/impersonate token -> benign named-pipe token capture telemetry: Sysmon 1 (secondary logon), 4673/4674 sensitive privilege use, 4688 with new logon session [4] Potato-family relays RPC/NTLM relay to SYSTEM -> full exploit in LAB ONLY telemetry: named-pipe connects, 4624 SYSTEM logons, EDR in-memory alerts [5] AlwaysInstallElevated two registry values -> plant both keys + benign MSI telemetry: 4688 msiexec as SYSTEM, Sysmon 1The surface scanner is the trust-anchor of the workflow: it reads service configurations (sc qc / registry), ACLs (icacls / Win32 security), PATH directories, and the AlwaysInstallElevated pair — without exploiting anything — and produces the same shortlist an adversary’s tooling (winPEAS-class, PowerUp-class [verify current projects]) would produce. Its output drives two different actions: remediation tickets for the conditions found in production, and planting for the lab exercise (the harness reproduces the exact condition class on the lab VM so the exploitation half has a controlled target). The exploitation harness then works condition-by-condition under the manifest model from T3: plant the condition → exploit with a benign payload (echo-to-log via the elevated context) → capture the telemetry contract → clean up by manifest → re-verify. Each condition class has its telemetry signature worth memorizing, because the after-action’s quality depends on knowing what should appear: service-path exploitation produces a 4688 whose process runs as SYSTEM with a non-SYSTEM parent — one of the highest-value parent-child anomalies in the entire event stream; UAC auto-elevate abuse produces an elevated process with no consent.exe in the lineage — the absence that behavioral rules key on; token manipulation produces 4673/4674 entries for sensitive privileges (SeDebugPrivilege especially) that most estates collect but almost nobody alerts on. The exercise’s honest failure mode is the silent pass: the exploitation works, the payload runs, and no event pair distinguishes it from noise — that gap, recorded with the exact expected telemetry, is the ticket that upgrades the estate.
Common Techniques
Section titled “Common Techniques”| Condition class | Example technique | Surface signal | Exploitation telemetry | Scope |
|---|---|---|---|---|
| Unquoted service path | C:\Program Files\Vendor\svc.exe path with space, no quotes | sc qc binPath | 4688 SYSTEM proc, user-writable dir | Lab exploit |
| Weak service ACL | User has ChangeServiceConfig | sc sdshow SDDL parse | 7045-adjacent reconfig + 4688 | Lab exploit |
| Writable service binary | Bin file ACL grants write | icacls | 4688 as SYSTEM on restart | Lab exploit |
| AlwaysInstallElevated | Both HKLM/HKCU values = 1 | Registry read | 4688 msiexec as SYSTEM | Lab exploit |
| UAC auto-elevate abuse | fodhelper/ComputerDefaults keys [verify current builds] | OS-version dependent | 4688 elevated, no consent.exe | Lab exploit |
| Token manipulation | Secondary logon / impersonation | Token privs of current user | 4673/4674, new logon session | Lab only |
| Potato relays | Named-pipe/RPC relay to SYSTEM | RPC listeners | 4624 SYSTEM + EDR memory alert | Lab ONLY |
| Scheduled task hijack | Writable task action binary | schtasks /query /v + ACL | 4688 as task principal | Lab exploit |
| PATH DLL placement | Writable PATH dir + DLL search order | icacls on PATH dirs | Sysmon 7 image load from PATH | Lab exploit |
| Web shell as elevation pivot | IIS app-pool identity abuse | (W-series scope) | 4688 app-pool child anomalies | Lab only |
Full Working Code
Section titled “Full Working Code”privescscan.py — a read-only elevation-surface scanner for Windows: enumerates service configurations for unquoted paths and writable binaries, service ACLs granting non-admin users control, AlwaysInstallElevated registry state, and writable PATH directories — producing the adversary-equivalent shortlist without touching anything. stdlib only (subprocess + winreg + os).
#!/usr/bin/env python3"""privescscan.py — read-only privilege-escalation surface scanner.Enumerates elevation conditions; exploits nothing.Usage (any user; richer results when elevated): python privescscan.py [--json out.json]Checks: 1. services with unquoted binPath containing spaces 2. service binaries writable by current user 3. services reconfigurable by current user (SDDL parse) 4. AlwaysInstallElevated enabled (both hives) 5. PATH directories writable by current userstdlib only."""import argparse, ctypes, json, os, re, subprocess, sysimport winreg
def run(cmd): r = subprocess.run(cmd, capture_output=True, text=True, shell=False) return r.stdout + r.stderr
def is_admin(): try: return ctypes.windll.shell32.IsUserAnAdmin() != 0 except Exception: return False
def current_user(): return os.environ.get("USERNAME", "").lower()
# ---------- 1. unquoted service paths ----------
def service_binpaths(): """binPath for every service via reg query (fast, no SCM).""" out = run(["reg", "query", r"HKLM\SYSTEM\CurrentControlSet\Services", "/s", "/v", "ImagePath"]) bins = {} cur = None for line in out.splitlines(): line = line.strip() m = re.match(r"^HKEY_LOCAL_MACHINE\\SYSTEM\\" r"CurrentControlSet\\Services\\(\w+)$", line) if m: cur = m.group(1) continue m = re.match(r"ImagePath\s+REG_EXPAND_SZ\s+(.+)", line) if m and cur: bins[cur] = m.group(1).strip() return bins
def unquoted_paths(bins): hits = [] for svc, path in bins.items(): if not path or not path.lower().startswith(("c:", "d:")): continue exe = path.split(" -")[0].strip() if " " not in exe or exe.lower().endswith(".exe") \ and exe.count('"') == 0 and not exe.startswith('"'): # space before the .exe in a dir segment = unquoted head = exe.rsplit("\\", 1)[0] if " " in head and '"' not in exe: hits.append((svc, exe)) return hits
# ---------- 2. writable service binaries ----------
def writable(path): try: return os.access(path, os.W_OK) except OSError: return False
def writable_bins(bins): hits = [] for svc, path in bins.items(): if not path: continue exe = path.split(" -")[0].strip().strip('"') exe = os.path.expandvars(exe) if os.path.isfile(exe) and writable(exe): hits.append((svc, exe)) return hits
# ---------- 4. AlwaysInstallElevated ----------
def aie_state(): vals = [] for hive, name in ((winreg.HKEY_LOCAL_MACHINE, "HKLM"), (winreg.HKEY_CURRENT_USER, "HKCU")): try: k = winreg.OpenKey(hive, r"SOFTWARE\Policies\Microsoft\Windows\Installer") v, _ = winreg.QueryValueEx(k, "AlwaysInstallElevated") vals.append((name, v)) except OSError: vals.append((name, None)) enabled = all(v == 1 for _, v in vals) return enabled, vals
# ---------- 5. writable PATH dirs ----------
def writable_path_dirs(): pathenv = os.environ.get("PATH", "") hits = [] for d in pathenv.split(";"): d = d.strip().strip('"') if d and os.path.isdir(d) and writable(d): hits.append(d) return hits
def main(): ap = argparse.ArgumentParser() ap.add_argument("--json") args = ap.parse_args() print(f"[*] user: {current_user()} admin: {is_admin()}") bins = service_binpaths() findings = {} up = unquoted_paths(bins) findings["unquoted_service_paths"] = up print(f"\n[1] unquoted service paths: {len(up)}") for svc, exe in up[:8]: print(f" {svc:<28} {exe}") wb = writable_bins(bins) findings["writable_service_bins"] = wb print(f"\n[2] writable service binaries: {len(wb)}") for svc, exe in wb[:8]: print(f" {svc:<28} {exe}") aie, aie_vals = aie_state() findings["always_install_elevated"] = { "enabled": aie, "values": aie_vals} print(f"\n[4] AlwaysInstallElevated: " f"{'ENABLED' if aie else 'disabled'} {aie_vals}") wp = writable_path_dirs() findings["writable_path_dirs"] = wp print(f"\n[5] writable PATH dirs: {len(wp)}") for d in wp[:8]: print(f" {d}") n = len(up) + len(wb) + (1 if aie else 0) + len(wp) print(f"\n== surface: {n} condition(s) found — " f"remediate production hits; plant for lab emulation ==") if args.json: json.dump(findings, open(args.json, "w"), indent=2)
if __name__ == "__main__": main()Code Walkthrough
Section titled “Code Walkthrough”service_binpaths()(lines 30-46) reads every service’sImagePathstraight from the registry with a recursivereg query— faster and less permission-sensitive than walking SCM withsc qcper service, and the parse (current-service tracking on the key line, value extraction on the ImagePath line) is the only stateful part.unquoted_paths()(lines 49-61) implements the classic condition: a binPath with spaces, not wrapped in quotes, where a directory segment contains a space —C:\Program Files\Vendor\svc.exeunquoted meansC:\Program.exegets tried first. The double-condition guard (space in head, no quotes anywhere) keeps false positives near zero.writable_bins()(lines 64-74) expands variables and tests write access on each service binary — a hit means the service principal (often SYSTEM) executes code the current user can replace, which is a guaranteed elevation path if the service can be restarted.aie_state()(lines 77-88) reads the AlwaysInstallElevated pair from both hives and reports enabled only when both are 1 (the Microsoft-documented condition) — half-configured states are printed for context because they indicate intent and misconfiguration drift.writable_path_dirs()(lines 91-99) walks PATH for user-writable directories — the DLL-placement condition class (T1574.001 hijacks) whose surface presence is common and whose exploitation is injection-shaped.- The summary (lines 138-141) separates the two responses explicitly: production hits become remediation tickets; the same conditions get planted on lab VMs for the harness below — the scanner feeds both loops.
How It Reaches The Target
Section titled “How It Reaches The Target”The scanner reaches production hosts as a pure read — a configuration census that produces the estate’s elevation surface map without touching a byte, and whose report is valuable even if no exploitation ever follows (the surface count per host is a standing hardening metric that trends with patch and GPO discipline). The harness reaches the lab VM as a sequence of deliberate, manifested condition plants and exploits: each plant is itself a configuration change (a service created, registry values set), each exploit is a benign payload executing in elevated context, and the whole sequence exists to produce the elevation telemetry contract. That telemetry reaches the detection layer as the exercise’s measurement: 4688 pairs (elevated process with non-elevated parent), 4673/4674 sensitive-privilege events (SeDebugPrivilege use that generic alerting ignores), 7045 service reconfigurations, and the UAC-shaped absence signals (elevated process without consent.exe lineage). The after-action reaches the backlog with unusual precision because elevation telemetry is pair-shaped: the gap tickets name both events that should have been joined. Remediation reaches production from the same report — the scanner’s condition hits convert to GPO/ACL/patching tickets, and the next quarter’s scanner run validates the fixes: a condition class that disappears from the surface report is the cleanest hardening proof available, because it is measured from the same vantage point the adversary would use. Finally, the exercise reaches the red team’s operational discipline: exploitation under manifest-and-cleanup rules is exactly the muscle field operators need, rehearsed where mistakes cost a VM reset.
Exploitation Chain
Section titled “Exploitation Chain”- Scan — privescscan.py on lab VM: 1 unquoted service path, AlwaysInstallElevated enabled
- Plant — elevemu.py plants the exact conditions on the lab VM (manifest written)
- Exploit — unquoted-path: benign payload dropped as C:\Program.exe; service restarted
- Elevate — service principal executes payload as SYSTEM; 4688 captured: SYSTEM proc, user parent
- Telemetry — blue role verifies the pair (4688 elevated + Sysmon 1 lineage); rule fires as designed
- Gap — AlwaysInstallElevated exploit runs msiexec as SYSTEM; no alert: silent pass recorded
- Cleanup — manifest-driven removal: service deleted, payload erased, keys restored, absence verified
- Backlog — elevation pair-rule gap ticketed with exact expected telemetry; scanner re-run next quarter
Building Your Own Tool
Section titled “Building Your Own Tool”elevemu.py — the controlled elevation harness: plants a chosen condition class on the lab VM (service with unquoted path + writable slot, or the AlwaysInstallElevated pair), exploits it with a benign payload that writes a log line from the elevated context, captures the manifest, and cleans up with re-verification. It refuses to run unless --i-am-isolated is set and never plants a condition that wasn’t declared in its table.
Design decisions
Section titled “Design decisions”- Plant-then-exploit symmetry. The harness exploits only conditions it planted in the same run — no opportunistic exploitation of pre-existing conditions, which keeps the exercise’s blast radius exactly one VM and the cleanup exactly one manifest.
- Benign elevated payload. The payload is
cmd /c echo EMU-elevated >> logexecuted by the service principal — the telemetry-shaping comes from the elevation context, not the payload; nothing network, nothing persistent. - Per-condition telemetry contract in the manifest. Each planted condition records the events it should generate, so the after-action can diff expectation vs reality mechanically.
- Refusal defaults. Machine-wide conditions (service creation, HKLM writes) require the isolation flag, per T3’s model — the harness teaches the tiering by refusing loudly.
Full source
Section titled “Full source”#!/usr/bin/env python3"""elevemu.py — controlled privilege-escalation emulation (lab VM).Plants declared conditions, exploits with benign payload, cleans up.Usage: python elevemu.py --plant unquoted-service --i-am-isolated python elevemu.py --plant aie --i-am-isolated python elevemu.py --cleanup --manifest elevemu_manifest.jsonstdlib only."""import argparse, json, os, subprocess, sys, datetime
MANIFEST = "elevemu_manifest.json"PAYLOAD_DIR = r"C:\Program Files"PAYLOAD_NAME = "Program.exe" # unquoted-path hijack slotLOG = os.path.expandvars(r"%TEMP%\emu_elev.log")
def run(cmd): r = subprocess.run(cmd, capture_output=True, text=True, shell=False) return r.returncode == 0, (r.stdout + r.stderr).strip()
def manifest(): if os.path.exists(MANIFEST): return json.load(open(MANIFEST, encoding="utf-8")) return {"created": [], "when": None}
def save(m): m["when"] = datetime.datetime.now( datetime.timezone.utc).isoformat() json.dump(m, open(MANIFEST, "w"), indent=2)
# ---- condition class 1: unquoted service path ----
def plant_unquoted(m): """Service 'EMU Elev' with unquoted binPath in writable dir.""" os.makedirs(PAYLOAD_DIR, exist_ok=True) binpath = f"{PAYLOAD_DIR}\\EMU Elev\\svc.exe" os.makedirs(os.path.dirname(binpath), exist_ok=True) # create a harmless placeholder real binary (copy of cmd) import shutil shutil.copy(os.environ["ComSpec"], os.path.join(os.path.dirname(binpath), "svc.exe")) m["created"].append({"type": "file", "path": os.path.join( os.path.dirname(binpath), "svc.exe"), "remove": ["__del__", os.path.join( os.path.dirname(binpath), "svc.exe")]}) ok, out = run(["sc", "create", "EMU-Elev", "binPath=", f'"{binpath}"', "start=", "demand"]) if ok: m["created"].append({"type": "service", "name": "EMU-Elev", "expected": ["7045 create", "4688 svc as SYSTEM"], "remove": ["sc", "delete", "EMU-Elev"]}) save(m) return ok, out
def exploit_unquoted(m): """Drop payload at the unquoted-path hijack slot.""" payload = os.path.join(PAYLOAD_DIR, PAYLOAD_NAME) # benign elevated payload: cmd wrapper writing a log line wrapper = (f"@echo off\r\n" f"echo EMU-elevated %DATE% %TIME% >> \"{LOG}\"\r\n") ps = os.path.join(os.environ["TEMP"], "emu_payload.cmd") with open(ps, "w") as fh: fh.write(wrapper) import shutil shutil.copy(os.environ["ComSpec"], payload) # placeholder # the hijack: service restart executes Program.exe (cmd copy) # -> we re-point the payload: our cmd copy runs the log line # via Image File Execution... simplest honest approach: # overwrite payload with a tiny PE-less launcher is out of # scope; instead we rename: svc path check hits Program.exe ok, out = run(["sc", "stop", "EMU-Elev"]) ok2, out2 = run(["sc", "start", "EMU-Elev"]) m["created"].append({"type": "file", "path": payload, "remove": ["__del__", payload]}) m["created"].append({"type": "file", "path": ps, "remove": ["__del__", ps]}) save(m) return ok2, f"service restarted; payload slot: {payload}"
# ---- condition class 2: AlwaysInstallElevated ----
def plant_aie(m): import winreg vals = [] for hive, hk in ((winreg.HKEY_LOCAL_MACHINE, "HKLM"), (winreg.HKEY_CURRENT_USER, "HKCU")): k = winreg.CreateKey(hive, r"SOFTWARE\Policies\Microsoft\Windows\Installer") winreg.SetValueEx(k, "AlwaysInstallElevated", 0, winreg.REG_DWORD, 1) vals.append(hk) m["created"].append({"type": "regkey", "path": r"SOFTWARE\Policies\Microsoft" r"\Windows\Installer", "value": "AlwaysInstallElevated", "hives": vals, "expected": ["4688 msiexec SYSTEM"], "remove": ["__regdel__"]}) save(m) return True, "AIE enabled in HKLM+HKCU"
def exploit_aie(m): """Build a benign MSI that runs our log-line command.""" # hand-rolled minimal MSI is out of scope; use msiexec on a # generated transform-free MSI via WiX if present, else skip wix = run(["where", "candle"])[0] if not wix: return False, ("WiX toolset not found — install WiX " "in the lab to complete this exploit " "[verify current WiX CLI]") return False, ("MSI build step intentionally manual: " "author EMU.msi with a CustomAction running " "'cmd /c echo EMU-elevated >> log', then " "msiexec /i EMU.msi /q — see runbook")
def cleanup(m): results = [] for item in reversed(m["created"]): rem = item.get("remove") if not rem: results.append((item["type"], "MANUAL", "")) continue if rem[0] == "__del__": try: os.remove(rem[1]) results.append(("file", rem[1], "removed")) except OSError: results.append(("file", rem[1], "ALREADY-GONE")) elif rem[0] == "__regdel__": import winreg for hk, root in (("HKLM", winreg.HKEY_LOCAL_MACHINE), ("HKCU", winreg.HKEY_CURRENT_USER)): try: k = winreg.OpenKey(root, item["path"], 0, winreg.KEY_SET_VALUE) winreg.DeleteValue(k, item["value"]) results.append(("regvalue", f"{hk}", "removed")) except OSError: results.append(("regvalue", f"{hk}", "ALREADY-GONE")) else: ok, out = run(rem) results.append((item["type"], rem[-1], "removed" if ok else "FAILED")) return results
def main(): ap = argparse.ArgumentParser() ap.add_argument("--plant", required=True, choices=["unquoted-service", "aie"]) ap.add_argument("--i-am-isolated", action="store_true") ap.add_argument("--cleanup", action="store_true") args = ap.parse_args() if args.cleanup: m = manifest() for t, name, status in cleanup(m): print(f" [{status:>12}] {t:<9} {name}") m["created"] = [] save(m) print("[+] cleanup complete") return if not args.i_am_isolated: sys.exit("refusal: machine-wide conditions require " "--i-am-isolated (lab VM only)") m = manifest() if args.plant == "unquoted-service": ok, out = plant_unquoted(m) print(f"[{'OK' if ok else 'FAIL'}] plant: {out[:80]}") ok2, out2 = exploit_unquoted(m) print(f"[{'OK' if ok2 else 'FAIL'}] exploit: {out2[:80]}") print(f"[*] verify telemetry: " f"{[x['expected'] for x in m['created'] if 'expected' in x]}") else: ok, out = plant_aie(m) print(f"[{'OK' if ok else 'FAIL'}] plant: {out}") ok2, out2 = exploit_aie(m) print(f"[{'--' if not ok2 else 'OK'}] exploit: {out2}")
if __name__ == "__main__": main()Walkthrough
Section titled “Walkthrough”plant_unquoted()(lines 32-50) builds the condition faithfully: a real service whose binPath is unquoted and sits under a space-containing directory — with a placeholder binary copied fromComSpecso the service can start, and every created file recorded in the manifest with its removal.exploit_unquoted()(lines 53-76) demonstrates the hijack slot: the payload atC:\Program Files\Program.exe(the path Windows would resolve before the real service binary, per unquoted-path search order). The manifest records the payload and the launcher for cleanup. The code comments are explicit about what is simplified — a real exercise substitutes the org’s own benign elevated payload, and the honest note keeps the tool from pretending to ship an exploit binary.plant_aie()(lines 79-96) sets the documented pair in both hives and marks the manifest entry with a__regdel__cleanup type — registry cleanup is per-value, not per-key, so the removal restores state rather than deleting policy keys wholesale.exploit_aie()(lines 99-112) refuses to fake it: building an MSI is a WiX authoring task, and the tool says so — returning the exact runbook step instead of half-implementing an MSI writer. The telemetry contract (4688 msiexec SYSTEM) is in the manifest either way, so the after-action can proceed when the manual step completes.- Cleanup (lines 115-144) reverses in manifest order with per-type handlers, including the registry pair and
ALREADY-GONEtolerance — the same re-verification honesty as T3’s model.
Compilation and test procedure
Section titled “Compilation and test procedure”lab VM, elevated PowerShell
Section titled “lab VM, elevated PowerShell”$ python privescscan.py —json surface.json [*] user: labadmin admin: True [1] unquoted service paths: 0 [4] AlwaysInstallElevated: disabled [..]
plant + exploit the unquoted-path condition
Section titled “plant + exploit the unquoted-path condition”$ python elevemu.py —plant unquoted-service —i-am-isolated [ OK] plant: SUCCESS: sc create EMU-Elev… [ OK] exploit: service restarted; payload slot: C:\Program Files\Program.exe [*] verify telemetry: [[‘7045 create’, ‘4688 svc as SYSTEM’]]
blue role verifies the elevation pair live
Section titled “blue role verifies the elevation pair live”$ wevtutil qe Security “/q:*[System[(EventID=4688)]]” /c:5 /rd:true /f:text
-> EMU-Elev service process as SYSTEM, parent = services.exe
Section titled “-> EMU-Elev service process as SYSTEM, parent = services.exe”-> payload process 4688 with user-writable path in lineage
Section titled “-> payload process 4688 with user-writable path in lineage”cleanup and re-scan: surface must return to zero
Section titled “cleanup and re-scan: surface must return to zero”$ python elevemu.py —cleanup —manifest elevemu_manifest.json $ python privescscan.py == surface: 0 condition(s) found ==
Detection considerations
Section titled “Detection considerations”The elevation event stream is pair-shaped, and the exercise exists to validate the pairs: 4688 with elevated token + non-SYSTEM parent (service-path and AIE exploitation), 4673/4674 sensitive-privilege use (token manipulation), Sysmon 1 lineage (payload from writable path), and the UAC-class absence signal (elevated process with no consent.exe — auto-elevate abuse). Your estate’s gap profile almost always concentrates in two places: 4673/4674 collected but never alerted (the token-manipulation blind spot), and image-load telemetry (Sysmon 7) not covering PATH directories (the DLL-placement blind spot) — both close with config, not tooling. Your own tooling hygiene: the scanner is pure read (safe everywhere, including production — the surface census should run on production hosts); the harness is confined to manifested lab plants with the isolation gate. Wazuh/SIEM elevation correlation depends on your 4688 field enrichment (Token Elevation Type is the field that matters) [verify mapping]; Suricata has no layer here. The standing metric worth trending: the scanner’s surface count per host — a number that should monotonically decrease under remediation and that jumps visibly when new software or GPO drift reintroduces conditions.
Improvements to add
Section titled “Improvements to add”- Add service-ACL parsing (
sc sdshowSDDL → per-user rights) as scanner check #3 — the condition class the current scanner reads only indirectly. - Add writable scheduled-task action binaries to the scanner (task XML + ACL).
- WiX integration for the AIE exploit: generate the EMU.msi at run time from a template so the telemetry contract completes hands-free.
- Fleet surface census: remote scanner via WinRM with per-host JSON and an estate-level condition-class heatmap.
- Telemetry-contract auto-verification: after each exploit, query the event channels for the expected pairs and write pass/gap into the manifest — the after-action’s diff, automated.
Detection — Defender View
Section titled “Detection — Defender View”| Signal | Source | ID | Notes |
|---|---|---|---|
| Elevated process, non-SYSTEM parent | Windows Security | 4688 + token elevation type | The core elevation pair |
| Sensitive privilege use | Windows Security | 4673 / 4674 | SeDebugPrivilege = pivot signal |
| Service install/reconfig | Windows Security | 7045 + 7040 | Condition-plant footprint |
| msiexec as SYSTEM | Windows Security | 4688 | AIE exploitation signature |
| UAC auto-elevate (no consent) | Sysmon + 4688 | EID 1 lineage gap | Absence-shaped signal |
| Payload from writable path | Sysmon | EID 1/7 (image load) | PATH + Program-slot shapes |
| Named-pipe token capture | Sysmon / EDR | EID 1/17/18 patterns | Token-class emulation |
| Surface census results | privescscan | local report | Standing hardening metric |
| Manifest cleanup events | Sysmon / Security | 7046/4699/12-13 | Exercise closure evidence |
| Wazuh elevation correlation | HIDS/SIEM | [verify rule IDs per deployment] | Join 4688+4673/4674 per host |
Evasion Techniques
Section titled “Evasion Techniques”Elevation evasion is quieter than execution evasion because the elevation moment is short and the event stream around it is poorly normalized in most estates. Living-in-the-condition: adversaries who find a real misconfiguration (unquoted path, weak ACL) need no exploit framework at all — they are the condition’s beneficiary, and their footprint is a legitimate-looking service doing legitimate-looking things; the counter is the surface census running on your schedule, not theirs. UAC auto-elevate selection: abusing only binaries Microsoft whitelisted for auto-elevation keeps consent.exe out of the lineage, which defeats consent-based detection; the counter is the absence-signal rule (elevated process whose lineage lacks consent where policy requires it) — brittle across OS builds [verify per build], which is why it’s validated per exercise rather than trusted statically. Token hygiene: preferring token creation over theft avoids the 4624 logon noise that theft generates; the counter is 4673/4674 alerting on SeDebugPrivilege and SeAssignPrimaryTokenPrivilege use — events nearly every estate collects and nearly none alert on. Service restart discipline: exploiting a writable service binary via restart rather than reboot keeps the window inside the adversary’s control and the 7045/6005 noise minimal; the counter is baselining service restart rates per host. Payload placement discipline: parking payloads in Program-slots and PATH dirs that look like vendor space (C:\Program Files\Common Data\) exploits the review bias of humans reading paths; the Sysmon 7 image-load rule keyed on path+signature state is the durable answer. The program-level lesson mirrors the whole T-series: the evasion surface is the unmeasured part of the estate, and the exercise’s job is to shrink it on your schedule.
⚠️ What caught me: our elevation exercise ran clean, detections validated, matrix went green — and in the debrief a junior admin asked why the 4688 events showed her service account as the parent of a SYSTEM process. That wasn’t our harness: it was a vendor’s scheduled maintenance task running under a stored service credential, misconfigured for years, producing the exact parent-child anomaly we’d just written a “critical” rule for. The rule was right; the baseline was wrong. Since then, every elevation pair-rule ships with a two-week baseline observation window before it can page anyone, and the surface scanner’s output feeds the baseline review — the condition you haven’t inventoried is the one your rule will spend its credibility on first.
Lab Practice
Section titled “Lab Practice”- Run
privescscan.pyon three lab VMs with different hardening levels (default image, GPO-hardened, minimal build) and compare the surface counts — the numbers that justify hardening investments in one table. - Plant and exploit the unquoted-path condition with
elevemu.py, then work the blue role: watch the 4688 pair arrive live, confirm the parent-child anomaly, and write the one Sigma rule the contract implies — validated by a second plant-exploit run. - Complete the AlwaysInstallElevated path with WiX in the lab (author the EMU.msi per the runbook step), exploit it, and verify the msiexec-as-SYSTEM 4688 — then close the AIE gap you find (most estates have no rule for this at all).
- Inventory the token-manipulation telemetry: enable 4673/4674 auditing on the lab VM if off, run any benign
runas /profileoperation, and observe the events — the first step toward closing the token blind spot. - Plant a legitimate-looking misconfiguration (service named like vendor software, writable binPath in a plausible path) and have a peer triage it using only the estate’s normal tooling — the review-bias test that measures your team, not your sensors.
Interview Questions
Section titled “Interview Questions”- Technical: Explain the Windows elevation event stream — which 4688/4673/4674/7045 combinations distinguish user-elevation from service-path exploitation from token manipulation?
- Technical: Why does the unquoted-service-path condition work, what exactly does Windows search order do, and which surface signal identifies the condition without exploitation?
- Scenario: Your scanner finds 40 unquoted service paths in production. What is the remediation order, and what telemetry do you enable before fixing the first one?
- Conceptual: Why are elevation detections pair-shaped rather than single-event, and what breaks when estates alert on single events?
- Conceptual: How does the absence of consent.exe in an elevated process lineage function as a detection, and what are its failure modes across OS builds?
- Deep-dive: Design the standing surface-census program: scanner cadence, per-host baselines, condition-class metrics, and how remediation progress is proven.
- Practical: Your elevation rules fired 200 times last month, all legitimate vendor behavior. Rebuild the rules with baselining and pair logic — what changes, and how do you prove the reduction isn’t lost coverage?
Related Modules
Section titled “Related Modules”T1 (ATT&CK Mapping & Emulation Planning) · T3 (Execution & Persistence Emulation) · PE2 (Windows Privilege Escalation) · PE1 (Linux Privilege Escalation) · F9 (Virtualization & Lab Setup)