Find backdoors an attacker left behind on your server

Updated November 2026

Attackers who get into a server rarely leave with one way back in. Before you trust a cleaned server again, check every place on this list where a backdoor (a hidden way back in) can hide. If the attacker had root, the honest answer is still to rebuild: this checklist then tells you what to look for in your backups and on the image.

Do this first.

  1. Save evidence before you change anything: copy logs and any file you plan to remove into a folder such as /root/evidence, or better, to another machine.
  2. Keep one console or SSH session open while you remove keys, users or PAM changes, so a mistake cannot lock you out.
  3. If you find signs of a rootkit (an /etc/ld.so.preload file, changed system binaries or an unknown kernel module), stop cleaning and plan a rebuild.

How to use this checklist

Commands work on Debian, Ubuntu and RHEL-family systems unless marked. Run them as root or with sudo. Timestamps help: note when the intrusion probably began, and look closely at anything changed since then. Attackers can fake a file’s modification time, but the change time (ctime, shown by ls -lc and find -ctime) is much harder to forge. A compromised root user can still make tools lie, so treat a clean result as “nothing found”, not as proof.

Users and login history

awk -F: '$3 == 0 {print $1}' /etc/passwd                    # only "root" should print
awk -F: '$7 !~ /(nologin|false|sync)$/ {print $1, $3, $7}' /etc/passwd   # accounts that can log in
sudo awk -F: '$2 == "" {print $1}' /etc/shadow               # accounts with no password
ls -lc /etc/passwd /etc/shadow /etc/group                    # when they last changed
getent group sudo wheel                                      # admin group members

# Recent logins (Debian 13: "last" is gone; install the wtmpdb package and use "wtmpdb last")
last -Faiw | head -30
sudo journalctl -t sshd -t sshd-session --since "-30d" | grep -E 'Accepted|session opened'

Look for unfamiliar accounts, system accounts (like www-data or postgres) given a shell, and logins from unexpected addresses or at odd hours.

Sudo rights

sudo grep -vE '^\s*(#|$)' /etc/sudoers
sudo ls -la /etc/sudoers.d/
sudo grep -rn 'NOPASSWD' /etc/sudoers /etc/sudoers.d/
sudo visudo -c                                               # syntax check before and after edits

A new file in /etc/sudoers.d/ giving a user ALL=(ALL) NOPASSWD: ALL is a classic backdoor. Always edit with visudo; a syntax error can lock everyone out of sudo.

SSH keys and SSH settings

sudo find / -xdev \( -name authorized_keys -o -name authorized_keys2 \) -exec ls -lc {} \; -exec cat {} \;
sudo sshd -T | grep -Ei 'authorizedkeys|permitrootlogin|passwordauthentication|pubkeyauthentication'
ls -la /etc/ssh/sshd_config.d/

Remove every key you cannot match to a person and device. Check whether AuthorizedKeysFile or AuthorizedKeysCommand was pointed somewhere unusual, and whether root login was re-enabled. See SSH hardening.

Cron and at jobs

for u in $(cut -d: -f1 /etc/passwd); do sudo crontab -l -u "$u" 2>/dev/null | sed "s/^/$u: /"; done
ls -la /etc/crontab /etc/anacrontab /etc/cron.d/ /etc/cron.hourly/ /etc/cron.daily/ /etc/cron.weekly/ /etc/cron.monthly/
sudo ls -la /var/spool/cron/crontabs/    # Debian/Ubuntu user crontabs
sudo ls -la /var/spool/cron/             # RHEL-family user crontabs
sudo atq                                 # one-off "at" jobs, if at is installed

Red flags: jobs that run every minute, download with curl or wget and pipe to a shell, decode base64, or run files from /tmp, /var/tmp or /dev/shm.

systemd services and timers

systemctl list-unit-files --type=service --state=enabled
systemctl list-timers --all
systemd-delta --type=extended,overridden          # drop-ins that change existing units
sudo find /etc/systemd /usr/lib/systemd /run/systemd -type f -ctime -30 -ls
sudo find /root /home -path '*/.config/systemd/*' -type f -ls   # per-user units

# Unit files no package installed (some will be yours; review each)
# Debian/Ubuntu:
sudo find /etc/systemd/system -type f -name '*.service' -exec sh -c 'dpkg -S "$1" >/dev/null 2>&1 || echo "$1"' _ {} \;
# RHEL-family:
sudo find /etc/systemd/system -type f -name '*.service' -exec sh -c 'rpm -qf "$1" >/dev/null 2>&1 || echo "$1"' _ {} \;

MITRE ATT&CK notes that malicious units often run programs from /tmp or /dev/shm. Read the ExecStart= line of anything unfamiliar with systemctl cat NAME. Kinsing, for example, registered a service called bot.

Startup and shell scripts

ls -l /etc/rc.local /etc/rc.d/rc.local 2>/dev/null      # Debian / RHEL locations
systemctl status rc-local.service
ls -la /etc/profile.d/ /etc/init.d/ /etc/update-motd.d/ 2>/dev/null
sudo grep -nE 'curl|wget|base64|/dev/tcp|LD_PRELOAD' /etc/profile /etc/profile.d/* /etc/bash.bashrc \
  /etc/bashrc /etc/environment /root/.bashrc /root/.profile /home/*/.bashrc /home/*/.profile 2>/dev/null

Login scripts run every time someone logs in, which makes them a quiet place for a reverse shell.

Preloaded libraries

Any library listed in /etc/ld.so.preload is loaded into every program on the system. The ld.so manual calls this “usually undesirable” and “typically employed only as an emergency remedy”. Rootkits such as the one used by Kinsing use it to hide processes and files.

ls -l /etc/ld.so.preload; cat /etc/ld.so.preload    # normally: No such file or directory
lsattr /etc/ld.so.preload                           # "i" = made immutable
sudo grep -l LD_PRELOAD /proc/[0-9]*/environ 2>/dev/null   # processes started with LD_PRELOAD

Because the rootkit can hide the file from normal tools, confirm with a static binary such as busybox cat (Debian/Ubuntu package busybox-static) or from rescue media.

PAM authentication modules

MITRE ATT&CK describes attackers patching pam_unix.so to accept a secret password for any user, or adding modules that record passwords.

# Debian/Ubuntu
sudo dpkg -V libpam-modules libpam-modules-bin libpam-runtime
for m in $(grep -rhoE 'pam_[A-Za-z0-9_]+\.so' /etc/pam.d | sort -u); do
  dpkg -S "*/$m" >/dev/null 2>&1 || echo "Not from a package: $m"; done

# RHEL-family
sudo rpm -V pam
for m in $(grep -rhoE 'pam_[A-Za-z0-9_]+\.so' /etc/pam.d | sort -u); do
  rpm -qf "/usr/lib64/security/$m" >/dev/null 2>&1 || echo "Not from a package: $m"; done

# Both: modules loaded by full path, and recently changed PAM files
grep -rnE '^[^#]*[[:space:]]/[^[:space:]]+\.so' /etc/pam.d
ls -lct /etc/pam.d | head

Any output from the verify commands (other than config files you edited) means the module is not what the package installed. Reinstall the package, and treat the server as fully compromised.

SUID files and changed binaries

A copy of a shell with the SUID bit set lets any local user become root.

sudo find / -xdev \( -perm -4000 -o -perm -2000 \) -type f -exec ls -lc {} \;

# SUID files no package installed
# Debian/Ubuntu:
sudo find / -xdev -perm -4000 -type f -exec sh -c 'dpkg -S "$1" >/dev/null 2>&1 || echo "$1"' _ {} \;
# RHEL-family:
sudo find / -xdev -perm -4000 -type f -exec sh -c 'rpm -qf "$1" >/dev/null 2>&1 || echo "$1"' _ {} \;

On Debian-based systems a packaged file can appear in this list when its path differs only by the merged /usr layout; check those with dpkg -S and the file name. Then verify all system binaries: sudo dpkg --verify or sudo rpm -Va, as explained in Malware scan.

Kernel modules

lsmod
cat /proc/sys/kernel/tainted                         # 0 = kernel not tainted
sudo dmesg | grep -iE 'taint|module verification failed'
ls -la /etc/modules /etc/modules-load.d/ /usr/lib/modules-load.d/ 2>/dev/null

# Loaded modules whose file no package installed
# Debian/Ubuntu (use rpm -qf instead of dpkg -S on RHEL-family):
for m in $(lsmod | awk 'NR>1 {print $1}'); do f=$(modinfo -n "$m" 2>/dev/null)
  [ -n "$f" ] && { dpkg -S "$f" >/dev/null 2>&1 || echo "Not from a package: $m $f"; }; done

Vendor drivers built with DKMS (graphics, ZFS, VirtualBox) also taint the kernel and show up here; anything else needs an explanation. A kernel rootkit can hide from lsmod entirely, which is one more reason to inspect a suspected server from rescue media.

Web shells

On web servers, check for web shells: PHP or other scripts that the application did not ship, especially in upload directories, and for files newer than your last deployment:

sudo find /var/www -type f -ctime -30 -name '*.php' -ls
sudo find /var/www -path '*/uploads/*' -name '*.php*'

The full procedure is on Website hacked.

After you have checked

When to call a professional

If you found modified system binaries or PAM modules, a preload library, an unknown kernel module, or you cannot work out how the attacker got in, bring in an incident responder or your host. At that point the server needs rebuilding from known-good media, and a professional can help you find out what else the attacker reached.

Sources