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
Tools
Compare all tools- VelociraptorPlatform · open source
- debsumsCLI · open source
- rpmCLI · built into Linux
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
| File | When it runs | Family notes |
|---|---|---|
/etc/profile | Login shells (also sh login shells) | All distributions; normally sources /etc/profile.d/*.sh |
/etc/profile.d/*.sh | Sourced by /etc/profile | Favourite drop-in location: package-owned files sit next to anything added |
/etc/bash.bashrc | Interactive non-login bash | Debian/Ubuntu (compiled-in; Debian's /etc/profile also sources it for bash login shells) |
/etc/bashrc | Sourced by the user's ~/.bashrc | RHEL/Fedora family convention, not read by bash itself |
~/.bash_profile, ~/.bash_login, ~/.profile | Login shells: only the first one found is read | RHEL skeleton ships .bash_profile; Debian ships .profile, which sources ~/.bashrc |
~/.bashrc | Interactive non-login shells | Debian skeleton also sources ~/.bash_aliases if present |
~/.bash_logout | When an interactive login shell exits | Can run cleanup, such as wiping history |
BASH_ENV file | Non-interactive bash running a script | Only if the variable is set in the environment |
Other shells and environment
| File | Notes |
|---|---|
/etc/zshenv, /etc/zprofile, /etc/zshrc, /etc/zlogin, /etc/zlogout | Global zsh files; Debian/Ubuntu place them in /etc/zsh/ |
~/.zshenv, ~/.zprofile, ~/.zshrc, ~/.zlogin, ~/.zlogout | Under $ZDOTDIR if set; .zshenv runs for every zsh, even scripts |
/etc/fish/config.fish, ~/.config/fish/config.fish, ~/.config/fish/conf.d/*.fish | fish configuration |
/etc/environment | Read by pam_env at login: KEY=value pairs, not a script |
~/.pam_environment | Only 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:
| Construct | Why 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 commands | Command 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 '...' EXIT | Runs around every command or at shell exit |
export LD_PRELOAD=..., PATH with a writable directory first | Library or binary hijacking for child processes |
HISTFILE, HISTSIZE, HISTCONTROL, HISTIGNORE, unset HISTFILE | History suppression or shaping |
source/. of an unusual path | Payload 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
/rootas carefully as/home: root's.bashrcfires every time an admin runssudo -i. - Compare every home's dotfiles with
/etc/skel; identical files are normal, small appended lines are not. Also check/etc/skelitself 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_logoutthat clears history or deletes files is anti-forensics; pair with shell history. - Library preloading set through
LD_PRELOADin 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_keysoptions) are often used together; see hunting Linux persistence.