Enumerating, planting, and hunting macOS persistence and auto-start (ASEP) locations during authorized engagements - LaunchAgents/LaunchDaemons, shell rc files, login items, cron/at/periodic jobs, login/logout hooks, Dock and Terminal/iTerm2 preferences, audio/QuickLook/Spotlight plugins, PAM modules, Authorization plugins, emond, and StartupItems - and mapping each to its trigger, required privilege, and sandbox/TCC implications.
Enumerating, planting, and hunting macOS persistence and auto-start (ASEP) locations during authorized engagements - LaunchAgents/LaunchDaemons, shell rc files, login items, cron/at/periodic jobs, login/logout hooks, Dock and Terminal/iTerm2 preferences, audio/QuickLook/Spotlight plugins, PAM modules, Authorization plugins, emond, and StartupItems - and mapping each to its trigger, required privilege, and sandbox/TCC implications.
domain
cybersecurity
subdomain
macos-security
tags
["penetration-testing","macos","persistence"]
version
1.0
author
xalgorix
license
Apache-2.0
Analyzing macOS Persistence and Autostart
When to Use
During authorized macOS assessments to
establish or demonstrate persistence after gaining code execution
During threat hunting / DFIR to enumerate Auto-Start Extensibility Points (ASEPs) and spot implants
When you need a persistence method that survives reboot, relogin, or a specific user action
When choosing a technique by its required privilege (user vs root) and sandbox/TCC posture
macOS exposes many ASEPs beyond LaunchAgents; map each to its trigger and permission cost
Critical: Techniques Most Often Missed
1. User-owned plist in a system LaunchDaemon folder runs as the USER
A plist's owner — not its directory — determines the run identity. A user-owned plist in /Library/LaunchDaemons executes as that user, not root. Conversely, infostealers reuse a captured sudo password to drop a root daemon:
How to CONFIRM:ls -l@ /Library/LaunchDaemons/*.plist for owner and the @ xattr marker; verify run identity with launchctl list and the spawned process's uid.
2. Login Item ZIP / LaunchAgents drop
A ZIP stored as a Login Item is opened by Archive Utility; if it contains LaunchAgents/file.plist (a folder that may not exist yet), that folder is created and the backdoor plist is added — executing on the next login. Same idea with .bash_profile/.zshenv in the user HOME.
osascript -e 'tell application "System Events" to get the name of every login item'
osascript -e 'tell application "System Events" to make login item at end with properties {path:"/path/itemname", hidden:false}'# Login items also live in:ls -l ~/Library/Application\ Support/com.apple.backgroundtaskmanagementagent
plutil -p /var/db/com.apple.xpc.launchd/loginitems.501.plist 2>/dev/null
How to CONFIRM: the login item appears via System Events enumeration or in the backgroundtaskmanagementagent store; a planted ~/Library/LaunchAgents/*.plist exists post-login.
Triggers: /Library and /System/Library agents/daemons fire at reboot (root required); ~/Library agents fire at relogin (no root). Load without reboot via launchctl load <plist>; verify nothing overrides with sudo launchctl load -w ....
~/.zshenv is especially powerful: it runs even non-interactively (and during sudo -s).
Step 3: Scheduled execution — cron, at, periodic
crontab -l # current user jobsls -lR /usr/lib/cron/tabs/ /private/var/at/jobs /etc/periodic/ # root for system tabsecho'* * * * * /bin/bash -c "touch /tmp/cron3"' > /tmp/cron && crontab /tmp/cron
echo"echo 11 > /tmp/at.txt" | at now+1 # at must be enabled (atrun daemon)
/etc/periodic/{daily,weekly,monthly} run via /System/Library/LaunchDaemons/com.apple.periodic* but execute as the file owner (not useful for privesc).
Step 4: Login/logout hooks, Dock, and GUI app plugins
# deprecated but functional login/logout hooks
defaults write com.apple.loginwindow LoginHook /Users/$USER/hook.sh
defaults read /Users/$USER/Library/Preferences/com.apple.loginwindow.plist
# Dock entries (also used for app masquerading)
plutil -p ~/Library/Preferences/com.apple.dock.plist
# plugin ASEPsls -l /Library/Audio/Plug-Ins/HAL ~/Library/Audio/Plug-ins/Components # restart coreaudiodls -l /Library/QuickLook ~/Library/QuickLook # space-bar preview
Third-party automation tools are also ASEPs: xbar (~/Library/Application Support/xbar/plugins/), Hammerspoon (~/.hammerspoon/init.lua), BetterTouchTool, Alfred workflows — many already hold Accessibility/Automation/FDA grants.
Step 5: Deeper/root-level persistence
# PAM: make sudo always succeed (TCC-protected dir; user may be prompted)ls -l /etc/pam.d
# prepend to /etc/pam.d/sudo: auth sufficient pam_permit.so# Authorization plugin (root) - runs at login, classic credential theftls -l /Library/Security/SecurityAgentPlugins/
# emond (obscure, often unmonitored)ls -l /private/var/db/emondClients
# StartupItems (deprecated): /Library/StartupItems , /System/Library/StartupItems# kexts: /Library/Extensions , /System/Library/Extensions (kextstat / kextload)
Step 6: Document trigger, privilege, and detectability
For each implant record the path, trigger (reboot / relogin / app-open / timer / user action), privilege required, and whether it offers sandbox bypass or TCC inheritance, so defenders can prioritize remediation.
Key Concepts
Concept
Description
ASEP
Auto-Start Extensibility Point — any location/mechanism that triggers code on an event
launchd
PID 1; reads/executes LaunchAgents (per-user) and LaunchDaemons (system) plist jobs
Agent vs Daemon
Agents load at user login (may use GUI); daemons load at system startup (background)
Plist owner = run identity
A user-owned plist runs as that user even in a system daemon folder
Login Items
Apps launched at login, stored in backgroundtaskmanagementagent / loginitems plist
Shell rc files
~/.zshenv/.zshrc/.bash_profile etc.; ~/.zshenv even runs non-interactively
Manage Login Items via System Events; AppleScript autoload payloads
crontab / at / periodic
Scheduled-task persistence
ls -l@ / xattr
Reveal ownership and extended-attribute markers on ASEP files
kextstat / kextload
Inspect and load kernel extensions
Common Scenarios
Scenario 1: User LaunchAgent backdoor
A plist in ~/Library/LaunchAgents with RunAtLoad/KeepAlive re-executes a payload at every login with no root needed and survives reboot via relogin.
Scenario 2: Root LaunchDaemon via captured password
An infostealer reuses a phished sudo password to install com.finder.helper.plist as root:wheel and launchctl loads it for system-startup persistence.
Scenario 3: Terminal/iTerm2 FDA inheritance
Injecting a startup CommandString into com.apple.Terminal.plist runs the payload whenever the user opens Terminal, inheriting Full Disk Access.
Scenario 4: Authorization plugin credential theft
A bundle in /Library/Security/SecurityAgentPlugins/ plus an authorization-db rule executes at every login, enabling persistent credential capture and a /etc/sudoers NOPASSWD backdoor.
Output Format
## macOS Persistence Finding
**Finding**: Persistence via user LaunchAgent
**Severity**: Medium (CVSS 6.1)
**Mechanism**: ~/Library/LaunchAgents (RunAtLoad)
**Host**: macHost.local (macOS 14.4)
### Reproduction Steps
1. Write com.attacker.helper.plist to ~/Library/LaunchAgents with RunAtLoad=true
2. launchctl load ~/Library/LaunchAgents/com.attacker.helper.plist
3. Log out / log in -> payload re-executes as the user
### ASEP Summary
| Path | Trigger | Privilege | Notes |
|------|---------|-----------|-------|
| ~/Library/LaunchAgents/com.attacker.helper.plist | Relogin/reboot | User | Survives reboot, no root |
| ~/.zshenv | Terminal open / sudo -s | User | Runs non-interactively |
### Recommendation
1. Baseline and monitor all LaunchAgents/LaunchDaemons for unexpected plists
2. Alert on writes to shell rc files, login hooks, and SecurityAgentPlugins
3. Verify plist ownership; user-owned daemons should be investigated
4. Use MDM + EDR to detect new ASEPs and unsigned autostart binaries
5. Inspect emond, periodic, and StartupItems directories during DFIR