System
User Accounts & Authentication
Overview
macOS stores user account information in directory service plists, login/logout events in system logs, and remote access configuration in SSH directories. User account forensics establishes who had accounts on the system, when they logged in and out, what privilege escalation occurred, and what remote access was configured.
Forensic Significance
| Evidence Type | Forensic Value |
|---|---|
| User accounts | All local accounts, UIDs, shells, home directories |
| Login/logout history | User session timeline |
| Last logged-in user | Most recent interactive user |
| Sudo history | Privilege escalation events |
| SSH authorized keys | Who can remotely access this system |
| SSH known hosts | Remote systems the user has connected to |
| Login items | Applications that auto-start at login |
File Locations
| Artifact | Path | Format |
|---|---|---|
| User account plists | /var/db/dslocal/nodes/Default/users/*.plist | Plist |
| Last logged-in user | /Library/Preferences/com.apple.loginwindow.plist | Plist |
| Login items (user) | ~/Library/Preferences/com.apple.loginitems.plist | Plist |
| Login items (modern) | ~/Library/Application Support/com.apple.backgroundtaskmanagementagent/backgrounditems.btm | Binary |
| Authorization database | /var/db/auth.db | SQLite |
| SSH authorized keys | ~/.ssh/authorized_keys | Text |
| SSH known hosts | ~/.ssh/known_hosts | Text |
| SSH config | ~/.ssh/config | Text |
| SSH private keys | ~/.ssh/id_* | PEM / OpenSSH |
| Sudo log | Unified Logs / /var/log/system.log | Log |
Key Data
User Account Plists
Each file in /private/var/db/dslocal/nodes/Default/users/ (/var is a symlink to /private/var) represents one account in the local directory node, named <name>.plist. Files are binary property lists, readable only by root. Following the Open Directory convention, every top-level value is stored as an array, even single values such as uid (for example uid is ["501"], a string inside an array). The directory contains system and service accounts (such as _www and root) as well as human accounts.
| Key | Description |
|---|---|
name | Record names: the short (login) name first, followed by any aliases (for example an email-style alias) |
realname | Full display name |
uid | Numeric user ID (stored as a string) |
gid | Primary group ID (stored as a string) |
generateduid | Directory services GUID for the account |
home | Home directory path |
shell | Login shell. /usr/bin/false or /sbin/nologin disable interactive login |
IsHidden | Set to 1 if the account is hidden from the login window |
authentication_authority | Authentication mechanisms for the account, as ;-delimited markers (see below) |
accountPolicyData | Embedded binary plist holding timestamps and failed-login counters (see below) |
ShadowHashData | Password hash data (salted PBKDF2-SHA512). Sensitive, see below |
KerberosKeys, HeimdalSRPKey | Key material. Sensitive, see below |
hint | Password hint typed by the user. Sensitive, see below |
authentication_authority markers
The authentication_authority array contains strings whose markers describe how the account authenticates:
| Marker | Meaning |
|---|---|
;ShadowHash; | The account has a local password hash (stored in ShadowHashData) |
;SecureToken; | The account holds a Secure Token and can unlock FileVault and authorize disk-encryption operations |
;Kerberosv5; | The account has a local Kerberos principal |
accountPolicyData
accountPolicyData is a data value containing a second, embedded property list. Its timestamps are Unix epoch seconds (fractional), not Core Data time.
| Key | Description |
|---|---|
creationTime | When the account was created |
passwordLastSetTime | When the password was last changed |
failedLoginCount | Failed logins since the last successful login |
failedLoginTimestamp | When the most recent failed login occurred |
Sensitive keys
The same plist contains credential material: ShadowHashData (password hash), KerberosKeys, HeimdalSRPKey, and the password hint. Treat a raw copy of an account plist as sensitive. The macfor collector deliberately never copies these keys (see macfor Collection below).
Login Window Preferences
defaults read /Library/Preferences/com.apple.loginwindow
| Key | Description |
|---|---|
lastUserName | Most recent user to log in |
autoLoginUser | Auto-login user (if configured) |
SHOWFULLNAME | Whether login shows name field vs. user list |
SSH Known Hosts
Each line represents a remote server the user has connected to via SSH:
server.example.com,192.168.1.100 ssh-ed25519 AAAAC3Nza...
This reveals remote systems the user has accessed, including internal servers, cloud instances, and development environments.
SSH Authorized Keys
Each line represents a public key that can log in as this user:
ssh-ed25519 AAAAC3Nza... user@remote-machine
The comment field (e.g., user@remote-machine) indicates where the key was generated.
Key Fields for Analysis
Enumerate All Users
# List all local user accounts
dscl . -list /Users UniqueID | sort -k2 -n
# Read specific user details
dscl . -read /Users/username
# Check for hidden users
dscl . -list /Users IsHidden | grep 1
Inspecting the Account Plists Directly
Work on a copy of the plist, as root, and handle it as sensitive material.
# Dump an account plist as XML (shows array-wrapped values and the markers)
sudo plutil -p /private/var/db/dslocal/nodes/Default/users/alice.plist
# Extract the embedded accountPolicyData plist and read it
sudo plutil -extract accountPolicyData raw -o - \
/private/var/db/dslocal/nodes/Default/users/alice.plist \
| base64 -d > /tmp/alice-policy.plist
plutil -p /tmp/alice-policy.plist
# Convert a Unix epoch value (for example creationTime) to UTC
date -u -r 1700000000
plutil -p prints the policy values as seconds since 1970; convert them as shown. On a live system dscl . -read /Users/alice returns the same directory data without root, including accountPolicyData and AuthenticationAuthority; the password itself is masked (Password: ********) and ShadowHashData is not shown. Password hashes are not needed for account triage; avoid extracting ShadowHashData unless the case requires it and you are authorized to.
Login History (Unified Logs)
# Login events
log show --predicate 'eventMessage contains "loginwindow" AND eventMessage contains "login"' --last 30d
# Sudo events
log show --predicate 'process == "sudo"' --last 30d
# SSH authentication
log show --predicate 'process == "sshd"' --last 30d
Analysis Notes
- Hidden accounts: Accounts with
IsHidden = 1do not appear on the login screen. These could be legitimate service accounts or unauthorized hidden accounts. - UID analysis: UIDs below 500 are typically system accounts. Regular user accounts start at 501. An account with a low UID that is not a known system account is suspicious, as is a low-UID account with an interactive shell and a password hash (
;ShadowHash;), a common shape for a backdoor account. - Account creation timeline:
creationTimeinaccountPolicyDatadates each account. Compare it against the incident window to spot accounts created shortly before or during suspicious activity, and usepasswordLastSetTimeto spot unexpected password resets. - Brute-force indicators: A high
failedLoginCountwith a recentfailedLoginTimestampsuggests password guessing against the account. The counter resets on a successful login, so a low count does not rule out earlier attempts. Correlate with Unified Logs. - FileVault-capable accounts: Accounts with
;SecureToken;can unlock FileVault. An unexpected Secure Token holder is an account that could decrypt the disk. - SSH key analysis: Review authorized_keys for unexpected entries. Check private keys for keys without passphrases (files with no encrypted header).
- sudo audit: Sudo events in Unified Logs show which users escalated to root and what commands they ran.
- Login items: Applications in login items auto-start when the user logs in, making this a persistence mechanism. Cross-reference with Launch Agents.
Version Differences
| macOS Version | Changes |
|---|---|
| 10.15 (Catalina) | Full Disk Access required to read user plists |
| 13 (Ventura) | Login Items managed via System Settings; backgrounditems.btm format |
| 14 (Sonoma) | Enhanced login item management |
macfor Collection
The system.users plugin (User Accounts, Community, also included in the Pro binary) collects local account metadata from the dslocal user plists. It targets macOS 10.12+.
| Setting | Value |
|---|---|
| Plugin ID | system.users |
| Artifact | local_accounts |
| Source | /private/var/db/dslocal/nodes/Default/users/*.plist |
| Output | Parsed records only (records/system.users/parsed.jsonl and/or user_accounts.parquet); no raw files |
| Record type | user_account |
| Requires root | Yes on a live system |
On a live Mac the directory is readable only by root. Without root the plugin is reported as not applicable (reason: "local account plists are not readable (requires root)"), visible in --dry-run. It works without root against a mounted image or --from tree.
The plugin records all accounts in the node, including system and service accounts, which is what makes low-UID and hidden accounts visible. A plist that fails to parse is recorded as an error and the remaining accounts are still collected.
# Live system
sudo macfor collect --plugin system.users --output ./evidence.zip
# Mounted image or directory tree
macfor collect --from /Volumes/Image --plugin system.users --output ./evidence.zip
Metadata Only
The plugin decodes an allowlist of keys and never copies the raw plist, because it contains credential material: ShadowHashData (password hash), KerberosKeys, HeimdalSRPKey, and the password hint. None of these, nor passwd or jpegphoto, reach the evidence container. For authentication_authority, only the presence of the ;ShadowHash; and ;SecureToken; markers is recorded. The SHA-256 of each source plist is stored so an analyst can verify a record against the original file.
Record Fields
| Field | Type | Description |
|---|---|---|
type | string | Always "user_account" |
username | string | Short (login) name, the first name value |
aliases | []string | Additional record names; omitted if none |
real_name | string | Full name |
uid, gid | int | Omitted if missing or non-numeric |
generated_uid | string | Directory services GUID |
home_dir, shell | string | Home directory and login shell |
hidden | bool | IsHidden is set |
service_account | bool | UID below 500, missing UID, or a non-interactive shell (/usr/bin/false, /bin/false, /sbin/nologin, /usr/sbin/nologin) |
has_password | bool | authentication_authority contains ;ShadowHash; (presence only) |
secure_token | bool | authentication_authority contains ;SecureToken; |
creation_time | time | From accountPolicyData.creationTime, RFC 3339 UTC |
password_last_set_time | time | From accountPolicyData.passwordLastSetTime, RFC 3339 UTC |
failed_login_count | int | From accountPolicyData.failedLoginCount |
failed_login_time | time | From accountPolicyData.failedLoginTimestamp, RFC 3339 UTC |
source_file | string | Path to the account plist |
source_sha256 | string | SHA-256 of the account plist |
Sample record:
{"type":"user_account","username":"alice","aliases":["alice@example.com"],"real_name":"Alice Smith","uid":501,"gid":20,"generated_uid":"4A1B2C3D-0000-1111-2222-333344445555","home_dir":"/Users/alice","shell":"/bin/zsh","hidden":false,"service_account":false,"has_password":true,"secure_token":true,"creation_time":"2023-11-14T22:13:20.5Z","password_last_set_time":"2023-11-16T02:00:00Z","failed_login_count":3,"failed_login_time":"2023-11-17T05:46:40Z","source_file":"/private/var/db/dslocal/nodes/Default/users/alice.plist","source_sha256":"eec7..."}
Manifest source.users
Separately from the plugin, every collection writes the accounts the source enumerated into manifest.json under source.users, regardless of root and of --plugin / --category filters. Each entry has username, uid, gid, and (omitted when empty) name, home_dir, and shell. A null value means enumeration failed; [] means no accounts. statistics.users_processed equals the length of the list.
"source": {
"type": "live",
"users": [
{ "username": "alice", "uid": 501, "gid": 20, "name": "Alice Smith", "home_dir": "/Users/alice", "shell": "/bin/zsh" }
]
}
On a live system the list is built from the dslocal plists (root), falling back to dscl . -readall /Users and then /etc/passwd, keeping only human accounts (UID 500 or above, interactive shell, excluding nobody). For a directory source (--from), it is every folder under Users/ except Shared and hidden folders, enriched from the matching dslocal plist when present. This manifest list drives per-user collection by other plugins; it is not a substitute for the full system.users records.
Not Collected
Apple ID linkage (LinkedIdentity), group membership and admin status, login/logout history, deleted accounts, and network or directory (Active Directory / LDAP) accounts are not collected by this plugin. Use the Unified Log, last, and group data from dscl for those.
Tool Support
| Tool | Support |
|---|---|
| macfor | system.users parses dslocal account plists into metadata-only user_account records (requires root on a live system); the manifest source.users lists enumerated accounts (Community) |
| plutil (macOS built-in) | Read binary plists and extract accountPolicyData |
| dscl (macOS built-in) | Directory service command-line tool |
| log (macOS built-in) | Query login/sudo events |
| last (macOS built-in) | Show login history |
References
- Apple Open Directory Documentation
- macOS User Account Forensics - SANS
- SANS FOR518: Mac and iOS Forensic Analysis