sudo Logs: macOS Privilege Escalation Evidence
sudo events in macOS Unified Logs, the sudoers configuration and the /var/db/sudo timestamp files show who ran which commands as root, and when.
- Location
- /private/var/db/diagnostics/
- Proves
- Which account ran which command as root (or another user), from which directory and terminal, and failed attempts
- Timestamps
- Unified Logs: timestamp with UTC offset; ts files: file mtime plus monotonic counters
- Access
- root to read the log store, /etc/sudoers and /var/db/sudo
- Retention
- Unified Log rotation (days to weeks); ts records until reboot or overwrite
- Collection
- log collect, Aftermath, UAC, mac_apt
Tools
Compare all toolsWhat it is
macOS ships the standard sudo with the sudoers policy plugin (1.9.17p2 on macOS 26.6). By default sudoers logs every allowed and denied attempt through syslog(3). Since macOS 10.12 Sierra those messages are captured by the Unified Logging system under process sudo, with the invoking user, working directory, target user and full command line.
Three other sources complete the picture: the sudoers policy files (who may do what), the credential-cache timestamp files in /var/db/sudo/ts (who authenticated recently), and the lecture status files in /var/db/sudo/lectured (who has ever used sudo interactively).
Where it lives
| Source | Path | Versions | Protection |
|---|---|---|---|
| sudo events | /private/var/db/diagnostics/ (+ uuidtext) | 10.12 Sierra+ | root / admin |
| sudo events (legacy) | /private/var/log/system.log | 10.11 and earlier, via the auth / authpriv rules in /etc/asl.conf | root / admin |
| Policy | /private/etc/sudoers, /private/etc/sudoers.d/* | All | root, mode 0440 |
| PAM stack | /private/etc/pam.d/sudo, /private/etc/pam.d/sudo_local | sudo_local.template ships from 14 Sonoma; sudo_local exists only if created | Readable |
| Credential cache | /private/var/db/sudo/ts/<user> | All current | root only |
| Lecture status | /private/var/db/sudo/lectured/<user> | All current | root only |
| I/O logs (only if enabled) | iolog_dir, default /var/log/sudo-io | Opt-in | root |
The default macOS sudoers grants root and %admin ALL = (ALL) ALL and reads drop-in files with #includedir /private/etc/sudoers.d. The drop-in directory is empty on a clean install.
What it proves
- Which local account ran
sudo, as which target user (USER=), with which exact command (COMMAND=), from which directory (PWD=) and terminal (TTY=). - Failed authentication (
incorrect password attempts) and policy refusals (user NOT in sudoers,a password is requiredfor non-interactivesudo -n). - That an account has used sudo interactively at least once (a lecture file exists) and roughly when it last authenticated (ts file).
- Tampering with policy: unexpected
NOPASSWDrules or new files insudoers.d.
It does not prove what happened inside a root shell. sudo -s, sudo -i or sudo su log only the shell as the command; follow up in shell history of root and the Unified Logs of child processes.
Key fields
A typical Unified Log message (one line):
<user> : TTY=ttys003 ; PWD=/Users/<user> ; USER=root ; COMMAND=/usr/bin/whoami
| Token | Meaning |
|---|---|
| leading name | Invoking user (not redacted on current releases) |
TTY= | Terminal; absent when sudo ran without a terminal |
PWD= | Working directory at invocation |
USER= | Target user (-u), root by default |
COMMAND= | Resolved path and arguments |
3 incorrect password attempts | Failed authentication (Aftermath uses this string) |
a password is required | sudo -n without a cached credential |
Timestamp record fields (see sudoers_timestamp(5)): version, size, type (TS_GLOBAL, TS_TTY, TS_PPID, TS_LOCKEXCL), flags (TS_DISABLED after sudo -k), auth_uid, sid, start_time, ts, and ttydev or ppid.
Timestamps
Unified Log entries carry a precise UTC instant; print them with --timezone UTC because log show otherwise uses the zone recorded with each entry.
The ts field in a timestamp record is read from a monotonic clock, so it is time since boot, not a date. start_time (session leader or parent start, record version 2) is a wall-clock value. For dating, prefer the file system times of the ts and lectured files:
stat -f '%Sm %N' -t '%Y-%m-%dT%H:%M:%S%z' /private/var/db/sudo/ts/* /private/var/db/sudo/lectured/*
Retention
- Unified Log events follow store rotation (size-based, days to weeks). Collect early.
- The sudoers manual says
timestampdirshould be cleared at reboot. On macOS it sits under/var/db/sudoon persistent storage, sotsfiles may survive a reboot; sudo ignores stale records, but compare file times with the last boot. lecturedfiles are designed to persist across reboots: their birth time approximates the first interactive sudo by that account.- Policy files persist until edited.
Collection
sudo log collect --output /Volumes/EVIDENCE/host.logarchive
sudo ditto /private/etc/sudoers /private/etc/sudoers.d /private/etc/pam.d ./case/sudo_cfg/
sudo ditto /private/var/db/sudo ./case/var_db_sudo
- Aftermath copies
/etc/sudoersand runs afailed_sudoUnified Log query. - UAC collects
/private/etc(includessudoers.d) and recordsstatoutput for/var/db/sudo/lectured. - mac_apt
SUDOLASTRUNreads/private/var/db/sudo/ts.
Parsing
# All sudo command lines, UTC, JSON for a timeline
log show --archive host.logarchive --timezone UTC --style ndjson \
--predicate 'process == "sudo" AND eventMessage CONTAINS "COMMAND="' > sudo.ndjson
# Failures and refusals
log show --archive host.logarchive --timezone UTC --style syslog \
--predicate 'process == "sudo" AND (eventMessage CONTAINS "incorrect password"
OR eventMessage CONTAINS "NOT in sudoers")'
# su for comparison (lowercase "tty" in su messages)
log show --archive host.logarchive --predicate 'process == "su"'
- macos-UnifiedLogs
unifiedlog_iteratoroutput can be filtered onprocessandmessageoffline. - mac_apt
SUDOLASTRUNlists uid and dates from ts records; cross-check its dates against file times becausetsis monotonic. - Plaso
unified_loggingputs sudo events in a super-timeline.
Unified Log Parser opens a .logarchive, the diagnostics and uuidtext folders or a log show export in the browser, applies DFIR triage rules and exports CSV or Timesketch; nothing is uploaded.
Investigator tips
- Search
COMMAND=rather thanTTY=: commands run by scripts or remote tools without a terminal have noTTY=field. - Commands like
/usr/bin/tee /etc/...,launchctl bootstrap system,defaults write /Library/...,spctl,csrutilortccutilare high-value pivots to launchd jobs and other persistence. - A new file in
/private/etc/sudoers.dor aNOPASSWDline is persistence in itself. Check its birth time and look for the sudo command that wrote it. - A
/etc/pam.d/sudo_localcreated from the Sonoma template to enablepam_tid.so(Touch ID) is a common legitimate change; other added PAM modules are not. - Map the invoking user to its account record and group membership in user accounts: only
adminmembers can sudo by default. - On pre-Sierra images,
grep 'sudo' system.log*including rotated.gzor.bz2copies; see system logs.