LINUX PERSISTENCE DETECTION
A triage reference of WHERE attackers keep a foothold on Linux, and how to inspect each location read-only. It is for hunting and investigation (where to look), NOT for planting persistence. Commands assume root, bash and a systemd host; paths are given for Debian/Ubuntu and RHEL/Rocky/Alma. Before you start: collect volatile data first (see the Linux Forensics sheet) and work on a copy or a read-only mount when you can. File times can be forged with touch; ctime (find -cmin/-ctime) cannot be set by touch, so compare both. dpkg -S <file> / rpm -qf <file> tell you whether a package owns a file; "not owned" is the first question for every hit below.
1. SYSTEMD SERVICES (ATT&CK T1543.002)#
Main unit paths, highest priority first (systemd.unit(5)):
/etc/systemd/system/ admin units and <unit>.d/ drop-ins
/run/systemd/system/ runtime units (gone after reboot)
/usr/local/lib/systemd/system/ local installs (usually no package)
/usr/lib/systemd/system/ package units
User units: ~/.config/systemd/user/, /etc/systemd/user/,
/usr/lib/systemd/user/ -- they run in the user's session.
Inspect:
systemd-analyze unit-paths # every directory this host reads
systemctl list-unit-files --state=enabled
systemctl list-units --type=service --state=running
systemctl cat sshd.service # unit file plus all drop-ins
systemd-delta --type=extended,overridden # edits to packaged units only;
# it never lists a NEW unit
find /etc/systemd /usr/local/lib/systemd /run/systemd -type f -ctime -30 \
2>/dev/null # changed in the last 30 days
find /home/*/.config/systemd /root/.config/systemd -type f 2>/dev/null
ls /var/lib/systemd/linger/ # users whose units start at boot
Flags: ExecStart/ExecStartPre in /tmp, /dev/shm, /var/tmp or a home dir;
sh -c with curl, wget or base64; a drop-in that replaces ExecStart of a
system service; Restart=always on an unknown unit; a user listed in linger.
2. SYSTEMD TIMERS AND GENERATORS (ATT&CK T1053.006)#
Timers live in the same unit paths as services (*.timer).
Generators (no ATT&CK sub-technique of their own) run early at every boot
and at each daemon-reload, before units load (systemd.generator(7)):
/etc/systemd/system-generators/ /usr/local/lib/systemd/system-generators/
/usr/lib/systemd/system-generators/ (and the matching user-generators/)
/run/systemd/system-generators/ (tmpfs, highest priority, live host only)
Inspect:
systemctl list-timers --all # next/last run and the unit fired
systemctl cat <name>.timer
ls -la /etc/systemd/system-generators /run/systemd/system-generators \
/usr/local/lib/systemd/system-generators 2>/dev/null
ls /run/systemd/generator /run/systemd/generator.late 2>/dev/null # output
Flags: a timer whose service runs from a writable path; any file in the /etc
or /usr/local generator dirs (rare on a normal host); generated units that no
package explains.
3. CRON, ANACRON AND AT (ATT&CK T1053.003, T1053.002)#
System: /etc/crontab, /etc/cron.d/, /etc/cron.{hourly,daily,weekly,monthly}/
/etc/anacrontab (RHEL; Debian if anacron is installed)
Users: /var/spool/cron/crontabs/<user> (Debian/Ubuntu)
/var/spool/cron/<user> (RHEL/Rocky/Alma)
at jobs: /var/spool/cron/atjobs/ (Debian), /var/spool/at/ (RHEL)
Access: /etc/cron.allow, /etc/cron.deny, /etc/at.allow, /etc/at.deny
Inspect:
cat /etc/crontab /etc/cron.d/* 2>/dev/null | grep -v '^#'
ls -la /var/spool/cron/crontabs /var/spool/cron 2>/dev/null
for u in $(cut -d: -f1 /etc/passwd); do crontab -l -u "$u" 2>/dev/null \
| sed "s|^|$u: |"; done # every user's crontab
ls -la /etc/cron.hourly /etc/cron.daily # scripts run by run-parts
atq # pending at jobs
at -c <job> # full script of one job
Flags: curl/wget piped to sh; @reboot lines; jobs in /tmp or a home dir;
cron files not owned by root; a crontab for a service account.
More syntax: the Crontab sheet.
4. SHELL STARTUP FILES (ATT&CK T1546.004)#
Login shells: /etc/profile, /etc/profile.d/*.sh, ~/.bash_profile,
~/.bash_login, ~/.profile (bash reads the first one found)
Interactive: ~/.bashrc, /etc/bash.bashrc (Debian), /etc/bashrc (RHEL)
Others: ~/.bash_logout, /etc/zsh/ or /etc/zshrc, ~/.zshrc,
/etc/environment (read by pam_env, not by the shell)
New accounts: /etc/skel/ is copied into every new home directory
Inspect:
ls -la --time-style=full-iso /etc/profile.d/
grep -nE 'curl|wget|base64|/dev/tcp|nohup|LD_PRELOAD|alias (sudo|ssh)=' \
/etc/profile /etc/profile.d/*.sh /etc/bash.bashrc /etc/bashrc \
/root/.bashrc /root/.profile /home/*/.bashrc /home/*/.profile 2>/dev/null
tail -n 5 /home/*/.bashrc /root/.bashrc # additions are usually appended
grep -nP '[^\t -~]' /root/.bashrc /home/*/.bashrc # non-ASCII, controls
Flags: an alias that wraps sudo or ssh (password capture); /dev/tcp
connections; background jobs started from a profile; changes in /etc/skel.
5. SSH KEYS AND SSHD SETTINGS (ATT&CK T1098.004)#
Keys: ~/.ssh/authorized_keys and ~/.ssh/authorized_keys2 (upstream and
Debian default; RHEL 9 reads authorized_keys only)
Login: ~/.ssh/rc (or, if it is absent, /etc/ssh/sshrc) runs at every
SSH login, before the user's shell (sshd(8), SSHRC)
Config: /etc/ssh/sshd_config, /etc/ssh/sshd_config.d/*.conf
Inspect:
find / -xdev \( -name authorized_keys -o -name authorized_keys2 \) \
-type f -ls 2>/dev/null
sshd -T | grep -iE 'authorizedkeys|permitrootlogin|permituserenv'
# effective config, all includes
ls -la /etc/ssh/sshrc /root/.ssh/rc /home/*/.ssh/rc 2>/dev/null
grep -nE '(^|,)(command|environment|from)=' /root/.ssh/authorized_keys
ssh-keygen -lf /root/.ssh/authorized_keys # fingerprints to compare
Flags: keys for root or service accounts that no one can explain; a changed
AuthorizedKeysFile or AuthorizedKeysCommand; PermitUserEnvironment yes (lets
~/.ssh/environment and environment= set LD_PRELOAD); any sshrc or rc file.
6. ACCOUNTS AND SUDO (ATT&CK T1136.001, T1548.003)#
Files: /etc/passwd, /etc/shadow, /etc/group, /etc/sudoers, /etc/sudoers.d/
Inspect:
awk -F: '$3 == 0 {print $1}' /etc/passwd # only root expected
awk -F: '$7 !~ /(nologin|false)$/ {print $1, $3, $7}' /etc/passwd
getent group sudo wheel # admin groups (Debian / RHEL)
grep -rn 'NOPASSWD' /etc/sudoers /etc/sudoers.d/ 2>/dev/null
visudo -c # parses sudoers + every include
ls -la --time-style=full-iso /etc/passwd /etc/shadow /etc/sudoers.d/
passwd -S -a # Debian: status of all accounts
passwd -S root # RHEL: one account at a time
Flags: a second UID 0 account; a system account (UID below 1000) with a
login shell; NOPASSWD ALL for a user; a sudoers.d file no package owns.
7. PAM MODULES (ATT&CK T1556.003)#
Config: /etc/pam.d/ (common-* on Debian, system-auth/password-auth on RHEL)
Modules: /usr/lib/<arch>-linux-gnu/security/ (Debian), /usr/lib64/security/
Inspect:
grep -rnE 'pam_exec|pam_python|pam_script' /etc/pam.d/
grep -rnE '^[^#].*\.so' /etc/pam.d/ | grep -vE 'pam_[a-z0-9_]+\.so'
for f in /usr/lib64/security/*.so /usr/lib/*-linux-gnu/security/*.so; do
[ -e "$f" ] || continue; { dpkg -S "$f" || rpm -qf "$f"; } >/dev/null \
2>&1 || echo "NOT OWNED: $f"; done
Flags: pam_exec.so running a script; a module that no package owns; a
module path outside the security directory; pam_permit.so in an auth stack
where it does not belong.
8. DYNAMIC LINKER HIJACKING (ATT&CK T1574.006)#
Files: /etc/ld.so.preload (normally absent), /etc/ld.so.conf.d/*.conf Inspect: ls -la /etc/ld.so.preload && cat /etc/ld.so.preload cat /etc/ld.so.conf /etc/ld.so.conf.d/*.conf grep -l LD_PRELOAD /proc/[0-9]*/environ 2>/dev/null # processes using it grep -rn LD_PRELOAD /etc/environment /etc/profile* /etc/systemd 2>/dev/null ldd /usr/bin/bash # compare with a clean host Flags: any /etc/ld.so.preload on a server; a library in /tmp, /dev/shm, /usr/local or a home dir; a new ld.so.conf.d entry. A rootkit that hooks libc can hide the file from ls and cat -- check from a rescue system.
9. KERNEL MODULES (ATT&CK T1547.006)#
Load at boot: /etc/modules-load.d/, /usr/lib/modules-load.d/,
/etc/modules (Debian, legacy but still read)
Options: /etc/modprobe.d/ -- an "install <mod> <command>" line runs a
command whenever that module loads (modprobe.d(5))
Inspect:
lsmod # loaded modules
cat /proc/sys/kernel/tainted # 4096 out-of-tree, 8192 unsigned
grep -rn '' /etc/modules-load.d/ /etc/modules 2>/dev/null
grep -rnE '^\s*install' /etc/modprobe.d/ /usr/lib/modprobe.d/ 2>/dev/null
modinfo -F filename <module> # where the .ko file lives
find /lib/modules/$(uname -r) -name '*.ko*' -ctime -30
Flags: a module file outside /lib/modules/<release>/ (kernel/, updates/,
extra/); an unknown name in modules-load.d; an install line that runs a
script. A loaded rootkit can hide from lsmod: use memory forensics (see the
Volatility sheet).
10. UDEV RULES (ATT&CK T1546.017)#
Rules: /etc/udev/rules.d/, /run/udev/rules.d/, /usr/local/lib/udev/rules.d/,
/usr/lib/udev/rules.d/ (udev(7))
Inspect:
ls -la --time-style=full-iso /etc/udev/rules.d/ /run/udev/rules.d/
grep -rnE 'RUN\+?=|PROGRAM=|IMPORT\{program\}|SYSTEMD_WANTS' \
/etc/udev/rules.d/ /run/udev/rules.d/ /usr/local/lib/udev 2>/dev/null
udevadm verify # syntax check (systemd 254+)
Flags: RUN+= starting a shell script or a binary outside /usr/lib/udev;
a rule on a device that always exists (e.g. KERNEL=="random"), so it runs
at every boot. udev kills what RUN starts once the event is handled, so a
rule that hands off to at, systemd-run, nohup or a SYSTEMD_WANTS unit is a
strong sign.
11. RC SCRIPTS AND SYSV INIT (ATT&CK T1037.004)#
Files: /etc/rc.local (Debian), /etc/rc.d/rc.local (RHEL; /etc/rc.local is
a link to it) -- run at boot only when executable
(systemd-rc-local-generator(8)); /etc/init.d/, /etc/rc?.d/
Inspect:
ls -la /etc/rc.local /etc/rc.d/rc.local 2>/dev/null
systemctl status rc-local.service # did it run at the last boot?
ls -la --time-style=full-iso /etc/init.d/
for f in /etc/init.d/*; do [ -e "$f" ] || continue; dpkg -S "$f" \
>/dev/null 2>&1 \
|| rpm -qf "$f" >/dev/null 2>&1 || echo "NOT OWNED: $f"; done
Flags: an executable rc.local with network commands; init scripts no
package owns. systemd-sysv-generator turns each one into a unit at boot
(/run/systemd/generator.late/<name>.service); it is deprecated upstream.
12. XDG AUTOSTART (DESKTOPS) (ATT&CK T1547.013)#
Files: ~/.config/autostart/*.desktop, /etc/xdg/autostart/*.desktop
(freedesktop Desktop Application Autostart spec)
Inspect:
ls -la /etc/xdg/autostart/ /home/*/.config/autostart/ 2>/dev/null
grep -HnE '^(Exec|TryExec|Hidden|X-GNOME-Autostart-enabled)=' \
/etc/xdg/autostart/*.desktop /home/*/.config/autostart/*.desktop
Flags: Exec= pointing to a script in a home dir or /tmp; a user entry with
the same name as a system one (it overrides it). Hidden=true disables an
entry, so a user file can also switch off a security agent's autostart.
13. PACKAGE MANAGER HOOKS (ATT&CK T1546.016)#
APT: /etc/apt/apt.conf.d/ -- DPkg::Pre-Invoke, DPkg::Post-Invoke,
APT::Update::Pre-Invoke / Post-Invoke run commands (apt.conf(5))
dpkg: /var/lib/dpkg/info/*.postinst, *.prerm (maintainer scripts)
DNF: /etc/dnf/plugins/, post-transaction-actions.d/*.action
RPM: scripts stored in the rpm database
Inspect:
grep -rnE 'Invoke|Pre-Install-Pkgs' /etc/apt/apt.conf.d/ 2>/dev/null
ls -lt /var/lib/dpkg/info/*.postinst | head # newest first
rpm -q --scripts <package> # pre/post install scripts
ls -la /etc/dnf/plugins/ /etc/yum.repos.d/ /etc/apt/sources.list.d/
Flags: a hook that calls curl, wget or a script in /tmp; a repository that
nobody added; a .deb or .rpm installed outside change windows (check
/var/log/dpkg.log or dnf history).
14. LOGIN MESSAGE SCRIPTS (DEBIAN/UBUNTU)#
Files: /etc/update-motd.d/* -- Debian's pam_motd runs them as root with
run-parts at each SSH or console login ("noupdate" skips it). RHEL's
pam_motd only prints /etc/motd and /etc/motd.d/. No ATT&CK sub-technique.
Inspect:
ls -la --time-style=full-iso /etc/update-motd.d/
grep -n 'pam_motd' /etc/pam.d/sshd /etc/pam.d/login
grep -nvE '^\s*(#|$)' /etc/update-motd.d/*
Flags: a script that does anything but print text, especially downloads,
user creation or background jobs.
15. PYTHON STARTUP HOOKS (ATT&CK T1546.018)#
Files: *.pth files in site-packages run any line starting with "import"
(deprecated in Python 3.15 but still run); sitecustomize.py and
usercustomize.py are imported at every start (Python site docs)
Inspect:
python3 -c 'import site, sys; print(site.getsitepackages(), sys.path)'
find / -xdev -name '*.pth' -path '*-packages*' -exec grep -Hn '^import' \
{} + 2>/dev/null
find / -xdev \( -name sitecustomize.py -o -name usercustomize.py \) \
2>/dev/null
Flags: a .pth import line that runs exec, base64 or a network call; a
sitecustomize.py that no package owns.
16. SETUID AND FILE CAPABILITIES (ATT&CK T1548.001)#
Inspect: find / -xdev -perm -4000 -type f 2>/dev/null # setuid find / -xdev -perm -2000 -type f 2>/dev/null # setgid getcap -r / 2>/dev/null # file capabilities find /tmp /var/tmp /dev/shm /home -perm /6000 -type f 2>/dev/null Flags: setuid copies of bash, sh, python or find; cap_setuid or cap_dac_override on an interpreter; any setuid file in a writable dir.
17. CHANGED SYSTEM FILES (ATT&CK T1554)#
Inspect (compare installed files with the package database): dpkg --verify # Debian: "??5??????" = new hash debsums -c # Debian: changed files (debsums) rpm -Va # RHEL: S size, 5 hash, T time, P caps rpm -Va | grep -v ' c /' # skip expected config edits Flags: a hash change (5) on /usr/bin, /usr/sbin or library files such as sshd, sudo, login, ls, ps or ss. Changed config files (c) are often normal; read them anyway, always for /etc/ssh, /etc/sudoers and /etc/pam.d. The database itself can be tampered with, so compare against a known-good host or install media when it matters.
FAST TRIAGE PASS#
1. Units: the find -ctime line (new units), list-unit-files --state=enabled, list-timers --all, systemd-delta (edited packaged units), plus every ~/.config/systemd and /var/lib/systemd/linger. 2. Cron: /etc/cron*, the user spool and atq. 3. Keys and accounts: every authorized_keys, UID 0, sudoers NOPASSWD. 4. Loader and kernel: /etc/ld.so.preload, lsmod, modprobe.d install lines. 5. Hooks: PAM, udev RUN, apt/dnf hooks, update-motd.d, .pth files. 6. Integrity: dpkg --verify or rpm -Va, then setuid and capabilities. 7. Order hits by ctime and match them with the incident window and with auth logs (see the Linux Forensics and Auditd sheets). Collect at scale: osquery tables crontab, systemd_units, authorized_keys, kernel_modules, suid_bin; Velociraptor artifacts Linux.Sys.Crontab, Linux.Sys.Services, Linux.Ssh.AuthorizedKeys, Linux.Sys.SUID (see the Osquery and Velociraptor sheets). Paths and options checked against the systemd, udev, OpenSSH, bash, glibc, kmod, Linux-PAM, sudo, apt, dpkg and rpm manual pages, the Python site docs and the freedesktop autostart spec; technique IDs per MITRE ATT&CK Enterprise v20. Commands tested on Debian 13 and Rocky Linux 9.
LINUX PERSISTENCE DETECTION
===========================
A triage reference of WHERE attackers keep a foothold on Linux, and how to
inspect each location read-only. It is for hunting and investigation (where
to look), NOT for planting persistence. Commands assume root, bash and a
systemd host; paths are given for Debian/Ubuntu and RHEL/Rocky/Alma.
Before you start: collect volatile data first (see the Linux Forensics sheet)
and work on a copy or a read-only mount when you can. File times can be
forged with touch; ctime (find -cmin/-ctime) cannot be set by touch, so
compare both. dpkg -S <file> / rpm -qf <file> tell you whether a package owns
a file; "not owned" is the first question for every hit below.
1. SYSTEMD SERVICES (ATT&CK T1543.002)
---------------------------------------
Main unit paths, highest priority first (systemd.unit(5)):
/etc/systemd/system/ admin units and <unit>.d/ drop-ins
/run/systemd/system/ runtime units (gone after reboot)
/usr/local/lib/systemd/system/ local installs (usually no package)
/usr/lib/systemd/system/ package units
User units: ~/.config/systemd/user/, /etc/systemd/user/,
/usr/lib/systemd/user/ -- they run in the user's session.
Inspect:
systemd-analyze unit-paths # every directory this host reads
systemctl list-unit-files --state=enabled
systemctl list-units --type=service --state=running
systemctl cat sshd.service # unit file plus all drop-ins
systemd-delta --type=extended,overridden # edits to packaged units only;
# it never lists a NEW unit
find /etc/systemd /usr/local/lib/systemd /run/systemd -type f -ctime -30 \
2>/dev/null # changed in the last 30 days
find /home/*/.config/systemd /root/.config/systemd -type f 2>/dev/null
ls /var/lib/systemd/linger/ # users whose units start at boot
Flags: ExecStart/ExecStartPre in /tmp, /dev/shm, /var/tmp or a home dir;
sh -c with curl, wget or base64; a drop-in that replaces ExecStart of a
system service; Restart=always on an unknown unit; a user listed in linger.
2. SYSTEMD TIMERS AND GENERATORS (ATT&CK T1053.006)
----------------------------------------------------
Timers live in the same unit paths as services (*.timer).
Generators (no ATT&CK sub-technique of their own) run early at every boot
and at each daemon-reload, before units load (systemd.generator(7)):
/etc/systemd/system-generators/ /usr/local/lib/systemd/system-generators/
/usr/lib/systemd/system-generators/ (and the matching user-generators/)
/run/systemd/system-generators/ (tmpfs, highest priority, live host only)
Inspect:
systemctl list-timers --all # next/last run and the unit fired
systemctl cat <name>.timer
ls -la /etc/systemd/system-generators /run/systemd/system-generators \
/usr/local/lib/systemd/system-generators 2>/dev/null
ls /run/systemd/generator /run/systemd/generator.late 2>/dev/null # output
Flags: a timer whose service runs from a writable path; any file in the /etc
or /usr/local generator dirs (rare on a normal host); generated units that no
package explains.
3. CRON, ANACRON AND AT (ATT&CK T1053.003, T1053.002)
------------------------------------------------------
System: /etc/crontab, /etc/cron.d/, /etc/cron.{hourly,daily,weekly,monthly}/
/etc/anacrontab (RHEL; Debian if anacron is installed)
Users: /var/spool/cron/crontabs/<user> (Debian/Ubuntu)
/var/spool/cron/<user> (RHEL/Rocky/Alma)
at jobs: /var/spool/cron/atjobs/ (Debian), /var/spool/at/ (RHEL)
Access: /etc/cron.allow, /etc/cron.deny, /etc/at.allow, /etc/at.deny
Inspect:
cat /etc/crontab /etc/cron.d/* 2>/dev/null | grep -v '^#'
ls -la /var/spool/cron/crontabs /var/spool/cron 2>/dev/null
for u in $(cut -d: -f1 /etc/passwd); do crontab -l -u "$u" 2>/dev/null \
| sed "s|^|$u: |"; done # every user's crontab
ls -la /etc/cron.hourly /etc/cron.daily # scripts run by run-parts
atq # pending at jobs
at -c <job> # full script of one job
Flags: curl/wget piped to sh; @reboot lines; jobs in /tmp or a home dir;
cron files not owned by root; a crontab for a service account.
More syntax: the Crontab sheet.
4. SHELL STARTUP FILES (ATT&CK T1546.004)
------------------------------------------
Login shells: /etc/profile, /etc/profile.d/*.sh, ~/.bash_profile,
~/.bash_login, ~/.profile (bash reads the first one found)
Interactive: ~/.bashrc, /etc/bash.bashrc (Debian), /etc/bashrc (RHEL)
Others: ~/.bash_logout, /etc/zsh/ or /etc/zshrc, ~/.zshrc,
/etc/environment (read by pam_env, not by the shell)
New accounts: /etc/skel/ is copied into every new home directory
Inspect:
ls -la --time-style=full-iso /etc/profile.d/
grep -nE 'curl|wget|base64|/dev/tcp|nohup|LD_PRELOAD|alias (sudo|ssh)=' \
/etc/profile /etc/profile.d/*.sh /etc/bash.bashrc /etc/bashrc \
/root/.bashrc /root/.profile /home/*/.bashrc /home/*/.profile 2>/dev/null
tail -n 5 /home/*/.bashrc /root/.bashrc # additions are usually appended
grep -nP '[^\t -~]' /root/.bashrc /home/*/.bashrc # non-ASCII, controls
Flags: an alias that wraps sudo or ssh (password capture); /dev/tcp
connections; background jobs started from a profile; changes in /etc/skel.
5. SSH KEYS AND SSHD SETTINGS (ATT&CK T1098.004)
-------------------------------------------------
Keys: ~/.ssh/authorized_keys and ~/.ssh/authorized_keys2 (upstream and
Debian default; RHEL 9 reads authorized_keys only)
Login: ~/.ssh/rc (or, if it is absent, /etc/ssh/sshrc) runs at every
SSH login, before the user's shell (sshd(8), SSHRC)
Config: /etc/ssh/sshd_config, /etc/ssh/sshd_config.d/*.conf
Inspect:
find / -xdev \( -name authorized_keys -o -name authorized_keys2 \) \
-type f -ls 2>/dev/null
sshd -T | grep -iE 'authorizedkeys|permitrootlogin|permituserenv'
# effective config, all includes
ls -la /etc/ssh/sshrc /root/.ssh/rc /home/*/.ssh/rc 2>/dev/null
grep -nE '(^|,)(command|environment|from)=' /root/.ssh/authorized_keys
ssh-keygen -lf /root/.ssh/authorized_keys # fingerprints to compare
Flags: keys for root or service accounts that no one can explain; a changed
AuthorizedKeysFile or AuthorizedKeysCommand; PermitUserEnvironment yes (lets
~/.ssh/environment and environment= set LD_PRELOAD); any sshrc or rc file.
6. ACCOUNTS AND SUDO (ATT&CK T1136.001, T1548.003)
---------------------------------------------------
Files: /etc/passwd, /etc/shadow, /etc/group, /etc/sudoers, /etc/sudoers.d/
Inspect:
awk -F: '$3 == 0 {print $1}' /etc/passwd # only root expected
awk -F: '$7 !~ /(nologin|false)$/ {print $1, $3, $7}' /etc/passwd
getent group sudo wheel # admin groups (Debian / RHEL)
grep -rn 'NOPASSWD' /etc/sudoers /etc/sudoers.d/ 2>/dev/null
visudo -c # parses sudoers + every include
ls -la --time-style=full-iso /etc/passwd /etc/shadow /etc/sudoers.d/
passwd -S -a # Debian: status of all accounts
passwd -S root # RHEL: one account at a time
Flags: a second UID 0 account; a system account (UID below 1000) with a
login shell; NOPASSWD ALL for a user; a sudoers.d file no package owns.
7. PAM MODULES (ATT&CK T1556.003)
----------------------------------
Config: /etc/pam.d/ (common-* on Debian, system-auth/password-auth on RHEL)
Modules: /usr/lib/<arch>-linux-gnu/security/ (Debian), /usr/lib64/security/
Inspect:
grep -rnE 'pam_exec|pam_python|pam_script' /etc/pam.d/
grep -rnE '^[^#].*\.so' /etc/pam.d/ | grep -vE 'pam_[a-z0-9_]+\.so'
for f in /usr/lib64/security/*.so /usr/lib/*-linux-gnu/security/*.so; do
[ -e "$f" ] || continue; { dpkg -S "$f" || rpm -qf "$f"; } >/dev/null \
2>&1 || echo "NOT OWNED: $f"; done
Flags: pam_exec.so running a script; a module that no package owns; a
module path outside the security directory; pam_permit.so in an auth stack
where it does not belong.
8. DYNAMIC LINKER HIJACKING (ATT&CK T1574.006)
-----------------------------------------------
Files: /etc/ld.so.preload (normally absent), /etc/ld.so.conf.d/*.conf
Inspect:
ls -la /etc/ld.so.preload && cat /etc/ld.so.preload
cat /etc/ld.so.conf /etc/ld.so.conf.d/*.conf
grep -l LD_PRELOAD /proc/[0-9]*/environ 2>/dev/null # processes using it
grep -rn LD_PRELOAD /etc/environment /etc/profile* /etc/systemd 2>/dev/null
ldd /usr/bin/bash # compare with a clean host
Flags: any /etc/ld.so.preload on a server; a library in /tmp, /dev/shm,
/usr/local or a home dir; a new ld.so.conf.d entry. A rootkit that hooks
libc can hide the file from ls and cat -- check from a rescue system.
9. KERNEL MODULES (ATT&CK T1547.006)
-------------------------------------
Load at boot: /etc/modules-load.d/, /usr/lib/modules-load.d/,
/etc/modules (Debian, legacy but still read)
Options: /etc/modprobe.d/ -- an "install <mod> <command>" line runs a
command whenever that module loads (modprobe.d(5))
Inspect:
lsmod # loaded modules
cat /proc/sys/kernel/tainted # 4096 out-of-tree, 8192 unsigned
grep -rn '' /etc/modules-load.d/ /etc/modules 2>/dev/null
grep -rnE '^\s*install' /etc/modprobe.d/ /usr/lib/modprobe.d/ 2>/dev/null
modinfo -F filename <module> # where the .ko file lives
find /lib/modules/$(uname -r) -name '*.ko*' -ctime -30
Flags: a module file outside /lib/modules/<release>/ (kernel/, updates/,
extra/); an unknown name in modules-load.d; an install line that runs a
script. A loaded rootkit can hide from lsmod: use memory forensics (see the
Volatility sheet).
10. UDEV RULES (ATT&CK T1546.017)
----------------------------------
Rules: /etc/udev/rules.d/, /run/udev/rules.d/, /usr/local/lib/udev/rules.d/,
/usr/lib/udev/rules.d/ (udev(7))
Inspect:
ls -la --time-style=full-iso /etc/udev/rules.d/ /run/udev/rules.d/
grep -rnE 'RUN\+?=|PROGRAM=|IMPORT\{program\}|SYSTEMD_WANTS' \
/etc/udev/rules.d/ /run/udev/rules.d/ /usr/local/lib/udev 2>/dev/null
udevadm verify # syntax check (systemd 254+)
Flags: RUN+= starting a shell script or a binary outside /usr/lib/udev;
a rule on a device that always exists (e.g. KERNEL=="random"), so it runs
at every boot. udev kills what RUN starts once the event is handled, so a
rule that hands off to at, systemd-run, nohup or a SYSTEMD_WANTS unit is a
strong sign.
11. RC SCRIPTS AND SYSV INIT (ATT&CK T1037.004)
------------------------------------------------
Files: /etc/rc.local (Debian), /etc/rc.d/rc.local (RHEL; /etc/rc.local is
a link to it) -- run at boot only when executable
(systemd-rc-local-generator(8)); /etc/init.d/, /etc/rc?.d/
Inspect:
ls -la /etc/rc.local /etc/rc.d/rc.local 2>/dev/null
systemctl status rc-local.service # did it run at the last boot?
ls -la --time-style=full-iso /etc/init.d/
for f in /etc/init.d/*; do [ -e "$f" ] || continue; dpkg -S "$f" \
>/dev/null 2>&1 \
|| rpm -qf "$f" >/dev/null 2>&1 || echo "NOT OWNED: $f"; done
Flags: an executable rc.local with network commands; init scripts no
package owns. systemd-sysv-generator turns each one into a unit at boot
(/run/systemd/generator.late/<name>.service); it is deprecated upstream.
12. XDG AUTOSTART (DESKTOPS) (ATT&CK T1547.013)
------------------------------------------------
Files: ~/.config/autostart/*.desktop, /etc/xdg/autostart/*.desktop
(freedesktop Desktop Application Autostart spec)
Inspect:
ls -la /etc/xdg/autostart/ /home/*/.config/autostart/ 2>/dev/null
grep -HnE '^(Exec|TryExec|Hidden|X-GNOME-Autostart-enabled)=' \
/etc/xdg/autostart/*.desktop /home/*/.config/autostart/*.desktop
Flags: Exec= pointing to a script in a home dir or /tmp; a user entry with
the same name as a system one (it overrides it). Hidden=true disables an
entry, so a user file can also switch off a security agent's autostart.
13. PACKAGE MANAGER HOOKS (ATT&CK T1546.016)
---------------------------------------------
APT: /etc/apt/apt.conf.d/ -- DPkg::Pre-Invoke, DPkg::Post-Invoke,
APT::Update::Pre-Invoke / Post-Invoke run commands (apt.conf(5))
dpkg: /var/lib/dpkg/info/*.postinst, *.prerm (maintainer scripts)
DNF: /etc/dnf/plugins/, post-transaction-actions.d/*.action
RPM: scripts stored in the rpm database
Inspect:
grep -rnE 'Invoke|Pre-Install-Pkgs' /etc/apt/apt.conf.d/ 2>/dev/null
ls -lt /var/lib/dpkg/info/*.postinst | head # newest first
rpm -q --scripts <package> # pre/post install scripts
ls -la /etc/dnf/plugins/ /etc/yum.repos.d/ /etc/apt/sources.list.d/
Flags: a hook that calls curl, wget or a script in /tmp; a repository that
nobody added; a .deb or .rpm installed outside change windows (check
/var/log/dpkg.log or dnf history).
14. LOGIN MESSAGE SCRIPTS (DEBIAN/UBUNTU)
-----------------------------------------
Files: /etc/update-motd.d/* -- Debian's pam_motd runs them as root with
run-parts at each SSH or console login ("noupdate" skips it). RHEL's
pam_motd only prints /etc/motd and /etc/motd.d/. No ATT&CK sub-technique.
Inspect:
ls -la --time-style=full-iso /etc/update-motd.d/
grep -n 'pam_motd' /etc/pam.d/sshd /etc/pam.d/login
grep -nvE '^\s*(#|$)' /etc/update-motd.d/*
Flags: a script that does anything but print text, especially downloads,
user creation or background jobs.
15. PYTHON STARTUP HOOKS (ATT&CK T1546.018)
--------------------------------------------
Files: *.pth files in site-packages run any line starting with "import"
(deprecated in Python 3.15 but still run); sitecustomize.py and
usercustomize.py are imported at every start (Python site docs)
Inspect:
python3 -c 'import site, sys; print(site.getsitepackages(), sys.path)'
find / -xdev -name '*.pth' -path '*-packages*' -exec grep -Hn '^import' \
{} + 2>/dev/null
find / -xdev \( -name sitecustomize.py -o -name usercustomize.py \) \
2>/dev/null
Flags: a .pth import line that runs exec, base64 or a network call; a
sitecustomize.py that no package owns.
16. SETUID AND FILE CAPABILITIES (ATT&CK T1548.001)
----------------------------------------------------
Inspect:
find / -xdev -perm -4000 -type f 2>/dev/null # setuid
find / -xdev -perm -2000 -type f 2>/dev/null # setgid
getcap -r / 2>/dev/null # file capabilities
find /tmp /var/tmp /dev/shm /home -perm /6000 -type f 2>/dev/null
Flags: setuid copies of bash, sh, python or find; cap_setuid or
cap_dac_override on an interpreter; any setuid file in a writable dir.
17. CHANGED SYSTEM FILES (ATT&CK T1554)
----------------------------------------
Inspect (compare installed files with the package database):
dpkg --verify # Debian: "??5??????" = new hash
debsums -c # Debian: changed files (debsums)
rpm -Va # RHEL: S size, 5 hash, T time, P caps
rpm -Va | grep -v ' c /' # skip expected config edits
Flags: a hash change (5) on /usr/bin, /usr/sbin or library files such as
sshd, sudo, login, ls, ps or ss. Changed config files (c) are often
normal; read them anyway, always for /etc/ssh, /etc/sudoers and
/etc/pam.d. The database itself can be tampered with, so
compare against a known-good host or install media when it matters.
FAST TRIAGE PASS
----------------
1. Units: the find -ctime line (new units), list-unit-files --state=enabled,
list-timers --all, systemd-delta (edited packaged units), plus every
~/.config/systemd and /var/lib/systemd/linger.
2. Cron: /etc/cron*, the user spool and atq.
3. Keys and accounts: every authorized_keys, UID 0, sudoers NOPASSWD.
4. Loader and kernel: /etc/ld.so.preload, lsmod, modprobe.d install lines.
5. Hooks: PAM, udev RUN, apt/dnf hooks, update-motd.d, .pth files.
6. Integrity: dpkg --verify or rpm -Va, then setuid and capabilities.
7. Order hits by ctime and match them with the incident window and with
auth logs (see the Linux Forensics and Auditd sheets).
Collect at scale: osquery tables crontab, systemd_units, authorized_keys,
kernel_modules, suid_bin; Velociraptor artifacts Linux.Sys.Crontab,
Linux.Sys.Services, Linux.Ssh.AuthorizedKeys, Linux.Sys.SUID (see the
Osquery and Velociraptor sheets).
Paths and options checked against the systemd, udev, OpenSSH, bash, glibc,
kmod, Linux-PAM, sudo, apt, dpkg and rpm manual pages, the Python site docs
and the freedesktop autostart spec; technique IDs per MITRE ATT&CK
Enterprise v20. Commands tested on Debian 13 and Rocky Linux 9.
Defensive reference on CyberRamen. Offensive / red-team sheets live on OffensiveRamen.com.