Skip to content

ExecutionLogs

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

What 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

SourcePathVersionsProtection
sudo events/private/var/db/diagnostics/ (+ uuidtext)10.12 Sierra+root / admin
sudo events (legacy)/private/var/log/system.log10.11 and earlier, via the auth / authpriv rules in /etc/asl.confroot / admin
Policy/private/etc/sudoers, /private/etc/sudoers.d/*Allroot, mode 0440
PAM stack/private/etc/pam.d/sudo, /private/etc/pam.d/sudo_localsudo_local.template ships from 14 Sonoma; sudo_local exists only if createdReadable
Credential cache/private/var/db/sudo/ts/<user>All currentroot only
Lecture status/private/var/db/sudo/lectured/<user>All currentroot only
I/O logs (only if enabled)iolog_dir, default /var/log/sudo-ioOpt-inroot

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 required for non-interactive sudo -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 NOPASSWD rules or new files in sudoers.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
TokenMeaning
leading nameInvoking 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 attemptsFailed authentication (Aftermath uses this string)
a password is requiredsudo -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 timestampdir should be cleared at reboot. On macOS it sits under /var/db/sudo on persistent storage, so ts files may survive a reboot; sudo ignores stale records, but compare file times with the last boot.
  • lectured files 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/sudoers and runs a failed_sudo Unified Log query.
  • UAC collects /private/etc (includes sudoers.d) and records stat output for /var/db/sudo/lectured.
  • mac_apt SUDOLASTRUN reads /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_iterator output can be filtered on process and message offline.
  • mac_apt SUDOLASTRUN lists uid and dates from ts records; cross-check its dates against file times because ts is monotonic.
  • Plaso unified_logging puts 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 than TTY=: commands run by scripts or remote tools without a terminal have no TTY= field.
  • Commands like /usr/bin/tee /etc/..., launchctl bootstrap system, defaults write /Library/..., spctl, csrutil or tccutil are high-value pivots to launchd jobs and other persistence.
  • A new file in /private/etc/sudoers.d or a NOPASSWD line is persistence in itself. Check its birth time and look for the sudo command that wrote it.
  • A /etc/pam.d/sudo_local created from the Sonoma template to enable pam_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 admin members can sudo by default.
  • On pre-Sierra images, grep 'sudo' system.log* including rotated .gz or .bz2 copies; see system logs.

See also