Skip to content

PersistenceExecutionUser activity

Shell Startup Files: Linux bashrc and profile Persistence

System and per-user shell startup files (profile, bashrc, zshrc, logout) on Linux: load order, where attackers hide persistence and how to review them.

Location
/etc/profile.d/
Proves
Whether code was set to run automatically each time a user or root starts or ends a shell session
Timestamps
None inside the files; use inode mtime/ctime/crtime (ext4, XFS v5)
Access
root for /etc files; any user for own dotfiles; root to read other users' homes
Retention
Until modified or deleted; package upgrades may replace /etc defaults
Collection
UAC, Velociraptor, cp -a, tar

What it is

Shells read a set of script files when they start and, for login shells, when they exit. These files set environment variables, aliases, functions and prompts, and they can run any command. Because they execute automatically with the user's privileges every time a terminal, SSH session or su - starts, a single appended line is a cheap and durable persistence mechanism (MITRE ATT&CK T1546.004, Unix Shell Configuration Modification).

The same files also hold configuration that changes how other artifacts behave, such as HISTFILE, HISTCONTROL, HISTTIMEFORMAT and PROMPT_COMMAND, so they are needed to interpret shell history correctly.

Where it lives

Bash

FileWhen it runsFamily notes
/etc/profileLogin shells (also sh login shells)All distributions; normally sources /etc/profile.d/*.sh
/etc/profile.d/*.shSourced by /etc/profileFavourite drop-in location: package-owned files sit next to anything added
/etc/bash.bashrcInteractive non-login bashDebian/Ubuntu (compiled-in; Debian's /etc/profile also sources it for bash login shells)
/etc/bashrcSourced by the user's ~/.bashrcRHEL/Fedora family convention, not read by bash itself
~/.bash_profile, ~/.bash_login, ~/.profileLogin shells: only the first one found is readRHEL skeleton ships .bash_profile; Debian ships .profile, which sources ~/.bashrc
~/.bashrcInteractive non-login shellsDebian skeleton also sources ~/.bash_aliases if present
~/.bash_logoutWhen an interactive login shell exitsCan run cleanup, such as wiping history
BASH_ENV fileNon-interactive bash running a scriptOnly if the variable is set in the environment

Other shells and environment

FileNotes
/etc/zshenv, /etc/zprofile, /etc/zshrc, /etc/zlogin, /etc/zlogoutGlobal zsh files; Debian/Ubuntu place them in /etc/zsh/
~/.zshenv, ~/.zprofile, ~/.zshrc, ~/.zlogin, ~/.zlogoutUnder $ZDOTDIR if set; .zshenv runs for every zsh, even scripts
/etc/fish/config.fish, ~/.config/fish/config.fish, ~/.config/fish/conf.d/*.fishfish configuration
/etc/environmentRead by pam_env at login: KEY=value pairs, not a script
~/.pam_environmentOnly read if pam_env has user_readenv=1; deprecated since Linux-PAM 1.5.0
/etc/skel/Template copied into new home directories, so a change here spreads to every account created later

What it proves

  • That a command, alias, function or environment variable was configured to run or apply at shell start or exit, for one user or for all users.
  • When the file was last changed (from inode timestamps), which often dates the persistence step.
  • How history was configured, which explains gaps or missing timestamps.

It does not prove that the payload actually ran. That requires a shell session after the change: correlate with logins in wtmp, SSH logs and process or network evidence.

Key fields

Nothing is structured, so review content for these constructs:

ConstructWhy it matters
Commands that download, decode or launch (curl, wget, base64 -d, nohup, &, /dev/tcp/)Direct execution at every session start
alias sudo=..., alias ssh=..., functions named like real commandsCommand hijacking, for example capturing typed passwords
PROMPT_COMMAND (string or, in bash 5.1+, array)Runs before every prompt, so it fires constantly
trap '...' DEBUG or trap '...' EXITRuns around every command or at shell exit
export LD_PRELOAD=..., PATH with a writable directory firstLibrary or binary hijacking for child processes
HISTFILE, HISTSIZE, HISTCONTROL, HISTIGNORE, unset HISTFILEHistory suppression or shaping
source/. of an unusual pathPayload moved out of the obvious file

Timestamps

The files carry no internal timestamps. Use the inode: mtime for the last content change, ctime for the last metadata change, and birth time where the filesystem records it (ext4 crtime, XFS v5). An /etc/profile.d/ script with a birth time far from the OS install and package update times is worth explaining.

stat -c '%n  mtime=%y  ctime=%z  birth=%w' /mnt/evidence/etc/profile.d/* /mnt/evidence/home/*/.bashrc
debugfs -R 'stat /etc/profile.d/custom.sh' /dev/sdb1   # ext4 crtime from an unmounted image

Retention

The files persist until edited or deleted. Package upgrades can replace or leave aside (.dpkg-dist, .rpmnew) the package-owned files in /etc, while user dotfiles are never touched by the package manager.

Collection

UAC's files/shell/* artifacts collect user and system startup files for bash, zsh, fish and other shells, and its bodyfile gives the timestamps. Velociraptor's Linux.Sys.BashHistory accepts a TargetGlob; its description suggests /{root,home/*}/.*rc and .*_profile globs for dotfiles.

sudo ./uac -p ir_triage /mnt/usb/case42
tar -czf startup.tgz -C /mnt/evidence etc/profile etc/profile.d etc/bash.bashrc etc/bashrc \
  etc/zsh etc/environment etc/skel root/.bashrc root/.profile root/.bash_profile
find /mnt/evidence/home /mnt/evidence/root -maxdepth 2 -name '.*' -type f \
  \( -name '.*rc' -o -name '.*profile' -o -name '.*login' -o -name '.*logout' -o -name '.zshenv' \)

Parsing

No special parser is needed. Verify package-owned files against the package database, then read everything else:

# Debian/Ubuntu: files that differ from the package checksums
debsums -c -a 2>/dev/null | grep -E '^/etc/(profile|bash|zsh|skel)'
# RHEL/Fedora
rpm -Vf /etc/profile /etc/bashrc
# Everything in profile.d not owned by any package (on the live host or chroot)
for f in /etc/profile.d/*; do dpkg -S "$f" >/dev/null 2>&1 || echo "unowned: $f"; done
grep -nE 'curl|wget|base64|/dev/tcp|nohup|PROMPT_COMMAND|trap |LD_PRELOAD|alias (sudo|ssh|su)=|HISTFILE' \
  /mnt/evidence/etc/profile /mnt/evidence/etc/profile.d/* /mnt/evidence/home/*/.bashrc

Investigator tips

  • Check /root as carefully as /home: root's .bashrc fires every time an admin runs sudo -i.
  • Compare every home's dotfiles with /etc/skel; identical files are normal, small appended lines are not. Also check /etc/skel itself for tampering.
  • Files under /etc/profile.d/ that no package owns deserve a look, but many are legitimately added by admins or configuration management.
  • Look for payloads pushed to the end of long files or after many blank lines, and for sourced files hidden elsewhere (. /var/tmp/.x).
  • A ~/.bash_logout that clears history or deletes files is anti-forensics; pair with shell history.
  • Library preloading set through LD_PRELOAD in a startup file only affects that shell's children; system-wide preloading is covered by ld.so.preload.
  • Other autostart points (cron, systemd units, SSH authorized_keys options) are often used together; see hunting Linux persistence.

See also