v1
This commit is contained in:
@@ -0,0 +1,910 @@
|
||||
#!/bin/bash
|
||||
###############################################################################
|
||||
# perfctl / perfcc Malware Cleanup Script
|
||||
# Targets: LD_PRELOAD rootkit (libgcwrap.so), cryptominer, proxyjacking,
|
||||
# SSH backdoors, SUID backdoors, trojanized tools, cron persistence
|
||||
#
|
||||
# Usage:
|
||||
# ROOTFS=/path/to/mounted/rootfs ./clean.sh
|
||||
#
|
||||
# Cold-disk only. Intended for container rootfs scanning — OpenVZ 7
|
||||
# (e.g. /vz/root/<CTID>) or Incus (e.g. /var/lib/incus/storage-pools/
|
||||
# <pool>/containers/<name>/rootfs), or any extracted rootfs tarball.
|
||||
#
|
||||
# The script is non-destructive to user data (MySQL, nginx configs, node apps,
|
||||
# /home, /var/www, /var/backups). It only targets known perfctl IoCs.
|
||||
###############################################################################
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "${ROOTFS:-}" ]; then
|
||||
echo "ERROR: ROOTFS is not set. Refusing to run against live '/'." >&2
|
||||
echo "Usage: ROOTFS=/mnt/ct-cleanup $0" >&2
|
||||
exit 1
|
||||
fi
|
||||
R="$ROOTFS"
|
||||
LOG="/var/log/perfctl-cleanup-${ROOTFS//\//_}-$(date +%Y%m%d-%H%M%S).log"
|
||||
FORENSICS_DIR="${FORENSICS_DIR:-/root/forensics-$(date +%Y%m%d-%H%M%S)}"
|
||||
FOUND=0
|
||||
REMOVED=0
|
||||
ERRORS=0
|
||||
|
||||
# ---------- helpers ----------------------------------------------------------
|
||||
|
||||
log() { echo "[$(date '+%H:%M:%S')] $*" | tee -a "$LOG"; }
|
||||
warn() { echo "[$(date '+%H:%M:%S')] WARNING: $*" | tee -a "$LOG"; }
|
||||
ok() { echo "[$(date '+%H:%M:%S')] OK: $*" | tee -a "$LOG"; }
|
||||
fail() { echo "[$(date '+%H:%M:%S')] FAIL: $*" | tee -a "$LOG"; ERRORS=$((ERRORS+1)); }
|
||||
|
||||
remove_file() {
|
||||
local f="$R$1"
|
||||
[ -e "$f" ] || [ -L "$f" ] || return 0
|
||||
FOUND=$((FOUND+1))
|
||||
log " removing: $f"
|
||||
chattr -i "$f" 2>/dev/null || true
|
||||
rm -f "$f" && REMOVED=$((REMOVED+1)) || fail "could not remove $f"
|
||||
}
|
||||
|
||||
remove_dir() {
|
||||
local d="$R$1"
|
||||
[ -d "$d" ] || return 0
|
||||
FOUND=$((FOUND+1))
|
||||
log " removing dir: $d"
|
||||
chattr -R -i "$d" 2>/dev/null || true
|
||||
rm -rf "$d" && REMOVED=$((REMOVED+1)) || fail "could not remove $d"
|
||||
}
|
||||
|
||||
save_forensic() {
|
||||
local f="$R$1"
|
||||
[ -e "$f" ] || return 0
|
||||
mkdir -p "$FORENSICS_DIR"
|
||||
local base
|
||||
base=$(echo "$1" | tr '/' '_')
|
||||
cp -a "$f" "$FORENSICS_DIR/$base" 2>/dev/null && \
|
||||
log " saved forensic copy: $FORENSICS_DIR/$base" || true
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
echo "You are about to run cleanup for '${R:-/}' system."
|
||||
read -r -p "Confirm /y/n to proceed: " _confirm
|
||||
case "$_confirm" in
|
||||
y|Y|yes|YES) ;;
|
||||
*) echo "Aborted."; exit 1 ;;
|
||||
esac
|
||||
|
||||
log "=========================================="
|
||||
log "perfctl cleanup started"
|
||||
log "ROOTFS prefix: '${R:-/}'"
|
||||
log "Forensics dir: $FORENSICS_DIR"
|
||||
log "=========================================="
|
||||
|
||||
# ---------- 0. strip immutable flags ----------------------------------------
|
||||
|
||||
log ""
|
||||
log "=== PHASE 0: Strip immutable flags ==="
|
||||
for d in etc lib usr/lib bin usr/bin sbin usr/sbin tmp root var/spool; do
|
||||
[ -d "$R/$d" ] && chattr -R -i "$R/$d" 2>/dev/null || true
|
||||
done
|
||||
ok "immutable flags cleared"
|
||||
|
||||
# ---------- 1. save forensic samples ----------------------------------------
|
||||
|
||||
log ""
|
||||
log "=== PHASE 1: Save forensic samples ==="
|
||||
for f in \
|
||||
/etc/ld.so.preload \
|
||||
/lib/libgcwrap.so \
|
||||
/bin/perfcc \
|
||||
/bin/wizlmsh \
|
||||
/tmp/.perf.c/pctl \
|
||||
; do
|
||||
save_forensic "$f"
|
||||
done
|
||||
if [ -d "$R/tmp/.xdiag" ]; then
|
||||
mkdir -p "$FORENSICS_DIR"
|
||||
tar czf "$FORENSICS_DIR/_tmp_.xdiag.tgz" -C "$R" tmp/.xdiag 2>/dev/null && \
|
||||
log " saved forensic copy: $FORENSICS_DIR/_tmp_.xdiag.tgz" || true
|
||||
fi
|
||||
|
||||
# ---------- 2. remove rootkit + payload files --------------------------------
|
||||
|
||||
log ""
|
||||
log "=== PHASE 2: Remove rootkit, payload, and dropper files ==="
|
||||
|
||||
# ld.so.preload (the rootkit injector)
|
||||
remove_file /etc/ld.so.preload
|
||||
remove_file /etc/ld.so.preload_
|
||||
remove_file /etc/ld.so.preload.bak
|
||||
|
||||
# userland rootkit shared objects
|
||||
for so in libgcwrap.so libfsnldev.so libpprocps.so; do
|
||||
remove_file "/lib/$so"
|
||||
remove_file "/usr/lib/$so"
|
||||
done
|
||||
|
||||
# main malware installer
|
||||
remove_file /bin/perfcc
|
||||
remove_file /usr/bin/perfcc
|
||||
|
||||
# SUID backdoor
|
||||
remove_file /bin/wizlmsh
|
||||
remove_file /usr/bin/wizlmsh
|
||||
|
||||
# other known dropper/miner binaries
|
||||
remove_file /bin/kkbush
|
||||
remove_file /usr/bin/kkbush
|
||||
remove_file /tmp/.apid
|
||||
remove_file /tmp/gd.sh
|
||||
remove_file /tmp/ccrl
|
||||
remove_file /tmp/kubeupd
|
||||
remove_file /tmp/javax64
|
||||
remove_file /tmp/wttwe
|
||||
remove_file /dev/shm/libfsnldev.so
|
||||
|
||||
# ---------- 3. remove malware directories ------------------------------------
|
||||
|
||||
log ""
|
||||
log "=== PHASE 3: Remove malware directories ==="
|
||||
|
||||
remove_dir /tmp/.xdiag
|
||||
remove_dir /tmp/.perf.c
|
||||
remove_dir /tmp/.dmesg
|
||||
remove_dir /bin/wbin
|
||||
remove_dir /bin/.local
|
||||
remove_dir /bin/.atmp
|
||||
remove_dir /root/.config/cron
|
||||
remove_dir /root/.config/traffmonetizer
|
||||
|
||||
# ---------- 4. find and remove hidden miner binaries -------------------------
|
||||
|
||||
log ""
|
||||
log "=== PHASE 4: Scan for hidden miner binaries ==="
|
||||
|
||||
# statically-linked unpackaged ELFs in system dirs (miner signature)
|
||||
while IFS= read -r f; do
|
||||
[ -z "$f" ] && continue
|
||||
if file "$f" 2>/dev/null | grep -q "statically linked"; then
|
||||
if ! dpkg -S "$f" >/dev/null 2>&1; then
|
||||
log " SUSPECT STATIC BINARY: $f ($(stat -c%s "$f" 2>/dev/null) bytes)"
|
||||
save_forensic "${f#$R}"
|
||||
FOUND=$((FOUND+1))
|
||||
chattr -i "$f" 2>/dev/null || true
|
||||
rm -f "$f" && REMOVED=$((REMOVED+1)) && log " removed: $f" || fail "could not remove $f"
|
||||
fi
|
||||
fi
|
||||
done < <(find "$R/usr/bin" "$R/usr/sbin" "$R/bin" "$R/sbin" -xdev -type f 2>/dev/null)
|
||||
|
||||
# ---------- 5. remove proxyjacking container rootfs --------------------------
|
||||
|
||||
log ""
|
||||
log "=== PHASE 5: Remove proxyjacking leftovers ==="
|
||||
|
||||
# known proxyjacking rootfs paths (attacker renames to hide)
|
||||
remove_dir /var/-nonrpk
|
||||
remove_dir /var/.r.rpk
|
||||
|
||||
# Docker/containerd proxyjacking remnants
|
||||
for d in /bin/wbin /tmp/kubeupd; do
|
||||
remove_dir "$d"
|
||||
done
|
||||
|
||||
# scan /var/ and /dev/shm/ for ANY hidden files/dirs the attacker may have stashed
|
||||
log " scanning /var/ for hidden entries..."
|
||||
KNOWN_VAR_HIDDEN=".updated|.placeholder|.gitkeep|.gitignore|.prettierrc|.prettierignore|.dockerignore|.env"
|
||||
while IFS= read -r entry; do
|
||||
[ -z "$entry" ] && continue
|
||||
base=$(basename "$entry")
|
||||
# skip known-safe dotfiles
|
||||
if echo "$base" | grep -qE "^($KNOWN_VAR_HIDDEN)$"; then
|
||||
continue
|
||||
fi
|
||||
# auto-remove known perfctl proxyjacking dirs
|
||||
case "$base" in
|
||||
.r.rpk|.rpk|.-nonrpk|.nonrpk|.proxy*|.bitping*|.earnfm*|.speedshare*|.repocket*)
|
||||
log " removing known proxyjacking dir: $entry"
|
||||
chattr -R -i "$entry" 2>/dev/null || true
|
||||
rm -rf "$entry" && REMOVED=$((REMOVED+1)) || fail "could not remove $entry"
|
||||
FOUND=$((FOUND+1))
|
||||
;;
|
||||
*)
|
||||
warn "UNKNOWN HIDDEN ENTRY in /var/: $entry (review manually)"
|
||||
ls -la "$entry" 2>/dev/null | tee -a "$LOG"
|
||||
;;
|
||||
esac
|
||||
done < <(find "$R/var" -maxdepth 2 -name '.*' -not -name '.' -not -name '..' \
|
||||
-not -path '*/lib/*' -not -path '*/cache/*' -not -path '*/log/*' \
|
||||
-not -path '*/spool/*' -not -path '*/backups/*' -not -path '*/www/*' \
|
||||
2>/dev/null)
|
||||
|
||||
log " scanning /dev/shm/ for hidden entries..."
|
||||
while IFS= read -r entry; do
|
||||
[ -z "$entry" ] && continue
|
||||
base=$(basename "$entry")
|
||||
case "$base" in
|
||||
.ICE-unix|.X11-unix|.XIM-unix|.font-unix) continue ;; # standard X11
|
||||
esac
|
||||
warn "HIDDEN ENTRY in /dev/shm/: $entry (review manually)"
|
||||
ls -la "$entry" 2>/dev/null | tee -a "$LOG"
|
||||
# auto-remove if it matches known malware patterns
|
||||
if file "$entry" 2>/dev/null | grep -qE 'ELF|executable|shared object'; then
|
||||
log " removing suspicious binary: $entry"
|
||||
save_forensic "${entry#$R}"
|
||||
rm -rf "$entry" && REMOVED=$((REMOVED+1)) || fail "could not remove $entry"
|
||||
FOUND=$((FOUND+1))
|
||||
fi
|
||||
done < <(find "$R/dev/shm" -maxdepth 2 -name '.*' -not -name '.' -not -name '..' 2>/dev/null)
|
||||
|
||||
# ---------- 6. clean trojanized tools ---------------------------------------
|
||||
|
||||
log ""
|
||||
log "=== PHASE 6: Remove trojanized tool wrappers ==="
|
||||
|
||||
# perfctl drops SHC-compiled wrappers for top, ldd, lsof, crontab
|
||||
for f in top ldd lsof crontab htop strace mount; do
|
||||
for d in /bin/.local/bin /bin/.atmp; do
|
||||
remove_file "$d/$f"
|
||||
done
|
||||
done
|
||||
|
||||
# ---------- 7. clean .profile / shell RC hooks -------------------------------
|
||||
|
||||
log ""
|
||||
log "=== PHASE 7: Clean shell RC hooks ==="
|
||||
|
||||
for rc in \
|
||||
/root/.profile \
|
||||
/root/.bashrc \
|
||||
/root/.bash_profile \
|
||||
/etc/profile \
|
||||
/etc/bash.bashrc \
|
||||
; do
|
||||
f="$R$rc"
|
||||
[ -f "$f" ] || continue
|
||||
if grep -qE 'perfcc|FPROF|AAZHDE|perfctl|libgcwrap|\.perf\.c|\.xdiag' "$f" 2>/dev/null; then
|
||||
log " cleaning $rc"
|
||||
sed -i.bak '/perfcc\|FPROF\|AAZHDE\|perfctl\|libgcwrap\|\.perf\.c\|\.xdiag/d' "$f"
|
||||
rm -f "${f}.bak" 2>/dev/null
|
||||
FOUND=$((FOUND+1)); REMOVED=$((REMOVED+1))
|
||||
fi
|
||||
done
|
||||
|
||||
# ---------- 8. neuter 'news' / system-account SSH backdoor -------------------
|
||||
|
||||
log ""
|
||||
log "=== PHASE 8: Remove SSH backdoors on system accounts ==="
|
||||
|
||||
# trailing-space nologin binary (copy of dash used as backdoor shell)
|
||||
find "$R/usr/sbin" "$R/sbin" -maxdepth 1 -name 'nologin *' -print 2>/dev/null | while read -r f; do
|
||||
log " removing backdoor binary: $f"
|
||||
rm -f "$f" && REMOVED=$((REMOVED+1)) || fail "could not remove $f"
|
||||
FOUND=$((FOUND+1))
|
||||
done
|
||||
|
||||
# trailing-space entry in /etc/shells
|
||||
if [ -f "$R/etc/shells" ]; then
|
||||
if grep -qE '^/usr/sbin/nologin +$|^/sbin/nologin +$' "$R/etc/shells" 2>/dev/null; then
|
||||
log " removing trailing-space nologin from /etc/shells"
|
||||
sed -i -E '/^\/usr\/sbin\/nologin +$/d; /^\/sbin\/nologin +$/d' "$R/etc/shells"
|
||||
FOUND=$((FOUND+1)); REMOVED=$((REMOVED+1))
|
||||
fi
|
||||
fi
|
||||
|
||||
# trailing space in /etc/passwd news line
|
||||
if [ -f "$R/etc/passwd" ]; then
|
||||
if grep -qP '^news:.*nologin +$' "$R/etc/passwd" 2>/dev/null; then
|
||||
log " fixing news account trailing space in /etc/passwd"
|
||||
sed -i -E 's|(^news:.*:/usr/sbin/nologin) +$|\1|' "$R/etc/passwd"
|
||||
FOUND=$((FOUND+1)); REMOVED=$((REMOVED+1))
|
||||
fi
|
||||
fi
|
||||
|
||||
# remove attacker authorized_keys from system accounts
|
||||
SYS_USERS="news bin daemon sys lp mail games uucp proxy www-data backup list irc gnats nobody systemd-network"
|
||||
for u in $SYS_USERS; do
|
||||
home=$(awk -F: -v u="$u" '$1==u{print $6}' "$R/etc/passwd" 2>/dev/null)
|
||||
[ -n "$home" ] && [ -d "$R$home/.ssh" ] && {
|
||||
log " removing .ssh from system account: $u ($home)"
|
||||
save_forensic "$home/.ssh/authorized_keys"
|
||||
rm -rf "$R$home/.ssh"
|
||||
FOUND=$((FOUND+1)); REMOVED=$((REMOVED+1))
|
||||
}
|
||||
done
|
||||
|
||||
# ---------- 9. clean cron persistence ----------------------------------------
|
||||
|
||||
log ""
|
||||
log "=== PHASE 9: Clean cron persistence ==="
|
||||
|
||||
# named cron files
|
||||
for f in \
|
||||
/etc/cron.d/perfclean \
|
||||
/etc/cron.hourly/perfclean \
|
||||
/etc/cron.daily/perfclean \
|
||||
/etc/cron.weekly/perfclean \
|
||||
/etc/cron.monthly/perfclean \
|
||||
; do
|
||||
remove_file "$f"
|
||||
done
|
||||
|
||||
# clean root crontab entries
|
||||
if [ -f "$R/var/spool/cron/crontabs/root" ]; then
|
||||
if grep -qE 'perfcc|perfctl|libgcwrap|AAZHDE|FPROF|\.xdiag|\.perf\.c' "$R/var/spool/cron/crontabs/root" 2>/dev/null; then
|
||||
log " cleaning root crontab"
|
||||
chattr -i "$R/var/spool/cron/crontabs/root" 2>/dev/null || true
|
||||
sed -i '/perfcc\|perfctl\|libgcwrap\|AAZHDE\|FPROF\|\.xdiag\|\.perf\.c/d' "$R/var/spool/cron/crontabs/root"
|
||||
FOUND=$((FOUND+1)); REMOVED=$((REMOVED+1))
|
||||
fi
|
||||
fi
|
||||
|
||||
# remove system-user crontabs (shouldn't exist)
|
||||
if [ -d "$R/var/spool/cron/crontabs" ]; then
|
||||
for ct in "$R"/var/spool/cron/crontabs/*; do
|
||||
[ -f "$ct" ] || continue
|
||||
base=$(basename "$ct")
|
||||
[ "$base" = "root" ] && continue
|
||||
log " removing system-user crontab: $base"
|
||||
chattr -i "$ct" 2>/dev/null || true
|
||||
rm -f "$ct"
|
||||
FOUND=$((FOUND+1)); REMOVED=$((REMOVED+1))
|
||||
done
|
||||
fi
|
||||
|
||||
# ---------- 10. remove systemd persistence -----------------------------------
|
||||
|
||||
log ""
|
||||
log "=== PHASE 10: Remove systemd persistence ==="
|
||||
|
||||
SUSP_PATTERN='perfcc\|perfctl\|libgcwrap\|kubeupd\|kkbush\|AAZHDE\|ExecStart=/tmp'
|
||||
for sdir in "$R/etc/systemd" "$R/usr/lib/systemd" "$R/lib/systemd"; do
|
||||
[ -d "$sdir" ] || continue
|
||||
grep -rl "$SUSP_PATTERN" "$sdir" 2>/dev/null | while read -r u; do
|
||||
log " removing systemd unit: $u"
|
||||
base=$(basename "$u")
|
||||
find "$R/etc/systemd" -name "$base" -lname '*' -delete 2>/dev/null
|
||||
rm -f "$u"
|
||||
FOUND=$((FOUND+1)); REMOVED=$((REMOVED+1))
|
||||
done
|
||||
done
|
||||
|
||||
# ---------- 11. full filesystem IoC scan -------------------------------------
|
||||
|
||||
log ""
|
||||
log "=== PHASE 11: Full filesystem IoC scan ==="
|
||||
|
||||
REMAINING=$(find "$R/" -xdev \( \
|
||||
-name 'libgcwrap*' -o -name 'perfcc*' -o -name 'perfctl*' -o -name 'perfclean*' \
|
||||
-o -name 'libfsnldev*' -o -name 'libpprocps*' -o -name 'wizlmsh*' \
|
||||
-o -name 'javax64*' -o -name '.xdiag' -o -name '.perf.c' \
|
||||
-o -name 'kkbush*' -o -name 'kubeupd*' -o -name 'ld.so.preload*' \
|
||||
-o -name 'zipgrepjournalctl*' \
|
||||
\) 2>/dev/null || true)
|
||||
|
||||
if [ -n "$REMAINING" ]; then
|
||||
warn "IoC files still found after cleanup:"
|
||||
echo "$REMAINING" | while read -r f; do
|
||||
warn " $f"
|
||||
chattr -i "$f" 2>/dev/null || true
|
||||
rm -rf "$f" 2>/dev/null && log " force-removed: $f" || fail " STUCK: $f"
|
||||
done
|
||||
else
|
||||
ok "no IoC files remaining on disk"
|
||||
fi
|
||||
|
||||
# ---------- 12. grep sweep for residual strings ------------------------------
|
||||
|
||||
log ""
|
||||
log "=== PHASE 12: Grep sweep for residual rootkit strings ==="
|
||||
|
||||
GREP_HITS=$(grep -rlE 'libgcwrap|perfcc|perfctl|AAZHDE|FPROF|\.xdiag|\.perf\.c|wizlmsh|zipgrepjournalctl' \
|
||||
"$R/etc" "$R/root" "$R/var/spool" 2>/dev/null | \
|
||||
grep -v '\.log$\|\.tgz$\|forensics' || true)
|
||||
|
||||
if [ -n "$GREP_HITS" ]; then
|
||||
warn "files still containing rootkit strings (review manually):"
|
||||
echo "$GREP_HITS" | tee -a "$LOG"
|
||||
else
|
||||
ok "no residual rootkit strings in config/cron/home"
|
||||
fi
|
||||
|
||||
# ---------- 13. PAM / NSS hijack check --------------------------------------
|
||||
|
||||
log ""
|
||||
log "=== PHASE 13: PAM / NSS hijack check ==="
|
||||
|
||||
# PAM modules: flag any .so in security/ that isn't a pam_<name>.so from
|
||||
# libpam-modules* / libpam-runtime
|
||||
for secdir in "$R/lib/security" "$R"/lib/*/security "$R/usr/lib/security" "$R"/usr/lib/*/security; do
|
||||
[ -d "$secdir" ] || continue
|
||||
for so in "$secdir"/*.so; do
|
||||
[ -f "$so" ] || continue
|
||||
base=$(basename "$so")
|
||||
case "$base" in
|
||||
pam_*.so) ;;
|
||||
*)
|
||||
warn "non-standard file in PAM security dir: $so"
|
||||
save_forensic "${so#$R}"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
done
|
||||
|
||||
# pam.d configs referencing absolute paths (modules are normally bare names)
|
||||
if [ -d "$R/etc/pam.d" ]; then
|
||||
PAM_ABS_HITS=$(grep -rEn '^[^#]*\s/(tmp|opt|usr/local|home|var/tmp)/' "$R/etc/pam.d/" 2>/dev/null || true)
|
||||
if [ -n "$PAM_ABS_HITS" ]; then
|
||||
warn "pam.d references suspicious absolute paths:"
|
||||
echo "$PAM_ABS_HITS" | tee -a "$LOG"
|
||||
fi
|
||||
fi
|
||||
|
||||
# NSS modules: list libnss_*.so not matching a known package provider
|
||||
for libdir in "$R"/lib/*/ "$R"/usr/lib/*/ "$R/lib" "$R/usr/lib"; do
|
||||
[ -d "$libdir" ] || continue
|
||||
for so in "$libdir"/libnss_*.so*; do
|
||||
[ -f "$so" ] || continue
|
||||
base=$(basename "$so")
|
||||
case "$base" in
|
||||
libnss_compat.*|libnss_dns.*|libnss_files.*|libnss_hesiod.*|libnss_nis.*|libnss_nisplus.*|libnss_db.*|libnss_mdns*|libnss_myhostname.*|libnss_resolve.*|libnss_systemd.*|libnss_mymachines.*|libnss_sss.*|libnss_ldap.*|libnss_winbind.*|libnss_wins.*) ;;
|
||||
*)
|
||||
warn "non-standard NSS module: $so"
|
||||
save_forensic "${so#$R}"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
done
|
||||
ok "PAM/NSS check complete"
|
||||
|
||||
# ---------- 14. SSH server configuration tampering --------------------------
|
||||
|
||||
log ""
|
||||
log "=== PHASE 14: SSH server configuration tampering ==="
|
||||
|
||||
SSHD_FILES=""
|
||||
[ -f "$R/etc/ssh/sshd_config" ] && SSHD_FILES="$R/etc/ssh/sshd_config"
|
||||
if [ -d "$R/etc/ssh/sshd_config.d" ]; then
|
||||
for f in "$R"/etc/ssh/sshd_config.d/*.conf; do
|
||||
[ -f "$f" ] && SSHD_FILES="$SSHD_FILES $f"
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -n "$SSHD_FILES" ]; then
|
||||
# dangerous directives
|
||||
SSH_HITS=$(grep -HnE '^\s*(PermitRootLogin\s+yes|PasswordAuthentication\s+yes|PermitEmptyPasswords\s+yes|ForceCommand|AuthorizedKeysCommand|AuthorizedKeysFile\s+(/tmp|/opt|/home|/var/tmp)|Subsystem\s+.*(/tmp|/opt|/dev/shm))' $SSHD_FILES 2>/dev/null || true)
|
||||
if [ -n "$SSH_HITS" ]; then
|
||||
warn "sshd_config contains suspicious directives:"
|
||||
echo "$SSH_HITS" | tee -a "$LOG"
|
||||
fi
|
||||
|
||||
# top-level Match blocks (can override per-user policy)
|
||||
MATCH_HITS=$(grep -HnE '^\s*Match\s+' $SSHD_FILES 2>/dev/null || true)
|
||||
if [ -n "$MATCH_HITS" ]; then
|
||||
log " sshd_config Match blocks (review):"
|
||||
echo "$MATCH_HITS" | tee -a "$LOG"
|
||||
fi
|
||||
|
||||
# trailing-space paths in sshd_config (same backdoor trick as nologin )
|
||||
TS_HITS=$(grep -HnE ' +$' $SSHD_FILES 2>/dev/null || true)
|
||||
if [ -n "$TS_HITS" ]; then
|
||||
warn "sshd_config lines with trailing whitespace (possible backdoor):"
|
||||
echo "$TS_HITS" | tee -a "$LOG"
|
||||
fi
|
||||
fi
|
||||
ok "sshd_config check complete"
|
||||
|
||||
# ---------- 15. sudoers tampering -------------------------------------------
|
||||
|
||||
log ""
|
||||
log "=== PHASE 15: sudoers tampering ==="
|
||||
|
||||
SUDO_FILES=""
|
||||
[ -f "$R/etc/sudoers" ] && SUDO_FILES="$R/etc/sudoers"
|
||||
if [ -d "$R/etc/sudoers.d" ]; then
|
||||
for f in "$R"/etc/sudoers.d/*; do
|
||||
[ -f "$f" ] || continue
|
||||
case "$(basename "$f")" in
|
||||
README|*.dpkg-*|*.ucf-*) continue ;;
|
||||
esac
|
||||
SUDO_FILES="$SUDO_FILES $f"
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -n "$SUDO_FILES" ]; then
|
||||
NOPASSWD_HITS=$(grep -HnE '^[^#]*NOPASSWD:' $SUDO_FILES 2>/dev/null || true)
|
||||
if [ -n "$NOPASSWD_HITS" ]; then
|
||||
warn "sudoers NOPASSWD rules (review):"
|
||||
echo "$NOPASSWD_HITS" | tee -a "$LOG"
|
||||
fi
|
||||
|
||||
DANGEROUS_HITS=$(grep -HnE '^[^#]*(Defaults\s+!authenticate|Defaults\s+targetpw|runas_default)' $SUDO_FILES 2>/dev/null || true)
|
||||
if [ -n "$DANGEROUS_HITS" ]; then
|
||||
warn "sudoers dangerous defaults:"
|
||||
echo "$DANGEROUS_HITS" | tee -a "$LOG"
|
||||
fi
|
||||
|
||||
# include directives pointing outside sudoers.d
|
||||
INC_HITS=$(grep -HnE '^\s*[#@]include(dir)?\s+' $SUDO_FILES 2>/dev/null | grep -vE '/etc/sudoers\.d' || true)
|
||||
if [ -n "$INC_HITS" ]; then
|
||||
warn "sudoers include directives outside /etc/sudoers.d:"
|
||||
echo "$INC_HITS" | tee -a "$LOG"
|
||||
fi
|
||||
fi
|
||||
ok "sudoers check complete"
|
||||
|
||||
# ---------- 16. account hardening audit -------------------------------------
|
||||
|
||||
log ""
|
||||
log "=== PHASE 16: Account hardening audit ==="
|
||||
|
||||
if [ -f "$R/etc/passwd" ]; then
|
||||
# UID 0 accounts (only root should exist)
|
||||
UID0=$(awk -F: '$3==0 && $1!="root" {print}' "$R/etc/passwd" 2>/dev/null || true)
|
||||
if [ -n "$UID0" ]; then
|
||||
warn "non-root UID 0 accounts:"
|
||||
echo "$UID0" | tee -a "$LOG"
|
||||
fi
|
||||
|
||||
# accounts with real login shells (excluding system defaults)
|
||||
LOGIN_SHELLS=$(awk -F: '$7 ~ /(bash|zsh|ksh|fish|sh)$/ && $1!="root" {print}' "$R/etc/passwd" 2>/dev/null || true)
|
||||
if [ -n "$LOGIN_SHELLS" ]; then
|
||||
log " accounts with real login shells (review):"
|
||||
echo "$LOGIN_SHELLS" | tee -a "$LOG"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "$R/etc/shadow" ]; then
|
||||
# empty-password accounts
|
||||
EMPTY_PW=$(awk -F: '$2=="" {print $1}' "$R/etc/shadow" 2>/dev/null || true)
|
||||
if [ -n "$EMPTY_PW" ]; then
|
||||
warn "accounts with empty password fields in /etc/shadow:"
|
||||
echo "$EMPTY_PW" | tee -a "$LOG"
|
||||
fi
|
||||
|
||||
# weak/unusual hash prefixes (strong: $6$/$y$/$7$)
|
||||
WEAK_PW=$(awk -F: '$2 ~ /^\$[135]\$/ {print $1":"substr($2,1,3)}' "$R/etc/shadow" 2>/dev/null || true)
|
||||
if [ -n "$WEAK_PW" ]; then
|
||||
warn "accounts with weak password hashes (MD5/SHA1/Blowfish):"
|
||||
echo "$WEAK_PW" | tee -a "$LOG"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "$R/etc/group" ]; then
|
||||
for grp in sudo wheel adm docker lxd incus-admin; do
|
||||
members=$(awk -F: -v g="$grp" '$1==g {print $4}' "$R/etc/group" 2>/dev/null)
|
||||
if [ -n "$members" ]; then
|
||||
log " group $grp members: $members"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# authorized_keys for ALL users (Phase 8 only handled system users)
|
||||
if [ -f "$R/etc/passwd" ]; then
|
||||
while IFS=: read -r user _ uid _ _ home _; do
|
||||
[ "$uid" -ge 1000 ] || [ "$user" = "root" ] || continue
|
||||
[ -n "$home" ] && [ -d "$R$home/.ssh" ] || continue
|
||||
for akf in authorized_keys authorized_keys2; do
|
||||
ak="$R$home/.ssh/$akf"
|
||||
[ -f "$ak" ] || continue
|
||||
keycount=$(grep -cE '^(ssh-|ecdsa-|sk-)' "$ak" 2>/dev/null || echo 0)
|
||||
log " $user ($home/.ssh/$akf): $keycount keys"
|
||||
# flag forced commands and restrictive/exfiltration options
|
||||
forced=$(grep -En '^(command=|no-pty|no-agent-forwarding|permitopen=|ProxyCommand)' "$ak" 2>/dev/null || true)
|
||||
if [ -n "$forced" ]; then
|
||||
warn " $user has authorized_keys with forced commands/options:"
|
||||
echo "$forced" | tee -a "$LOG"
|
||||
fi
|
||||
done
|
||||
# ~/.ssh/config ProxyCommand is suspicious on a server account
|
||||
if [ -f "$R$home/.ssh/config" ]; then
|
||||
proxy=$(grep -HnE '^\s*(ProxyCommand|LocalCommand|PermitLocalCommand)' "$R$home/.ssh/config" 2>/dev/null || true)
|
||||
if [ -n "$proxy" ]; then
|
||||
warn "$user ~/.ssh/config contains proxy/local-command directives:"
|
||||
echo "$proxy" | tee -a "$LOG"
|
||||
fi
|
||||
fi
|
||||
done < "$R/etc/passwd"
|
||||
fi
|
||||
ok "account audit complete"
|
||||
|
||||
# ---------- 17. other persistence vectors -----------------------------------
|
||||
|
||||
log ""
|
||||
log "=== PHASE 17: Other persistence vectors ==="
|
||||
|
||||
PERSIST_TARGETS=""
|
||||
[ -f "$R/etc/rc.local" ] && PERSIST_TARGETS="$PERSIST_TARGETS $R/etc/rc.local"
|
||||
for f in \
|
||||
"$R"/etc/profile.d/*.sh \
|
||||
"$R"/etc/xdg/autostart/*.desktop \
|
||||
"$R"/etc/systemd/system/*.timer \
|
||||
"$R"/usr/lib/systemd/system/*.timer \
|
||||
"$R"/lib/systemd/system/*.timer \
|
||||
"$R"/etc/systemd/system/*.service.d/*.conf \
|
||||
"$R"/usr/share/dbus-1/system-services/*.service \
|
||||
"$R"/etc/polkit-1/rules.d/*.rules \
|
||||
"$R"/etc/cron.allow \
|
||||
"$R"/etc/cron.deny \
|
||||
; do
|
||||
[ -f "$f" ] && PERSIST_TARGETS="$PERSIST_TARGETS $f"
|
||||
done
|
||||
|
||||
if [ -n "$PERSIST_TARGETS" ]; then
|
||||
# auto-remove any file matching SUSP_PATTERN (BRE — matches Phase 10 convention)
|
||||
for f in $PERSIST_TARGETS; do
|
||||
if grep -q "$SUSP_PATTERN" "$f" 2>/dev/null; then
|
||||
log " removing persistence file matching IoC pattern: $f"
|
||||
save_forensic "${f#$R}"
|
||||
chattr -i "$f" 2>/dev/null || true
|
||||
rm -f "$f" && REMOVED=$((REMOVED+1)) || fail "could not remove $f"
|
||||
FOUND=$((FOUND+1))
|
||||
fi
|
||||
done
|
||||
|
||||
# list timers (they're easy to miss in Phase 10's content-grep sweep)
|
||||
for t in "$R"/etc/systemd/system/*.timer "$R"/usr/lib/systemd/system/*.timer "$R"/lib/systemd/system/*.timer; do
|
||||
[ -f "$t" ] || continue
|
||||
log " systemd timer present: ${t#$R}"
|
||||
done
|
||||
|
||||
# flag any non-shipped init.d script
|
||||
if [ -d "$R/etc/init.d" ]; then
|
||||
for f in "$R"/etc/init.d/*; do
|
||||
[ -f "$f" ] || continue
|
||||
base=$(basename "$f")
|
||||
case "$base" in
|
||||
README|rc|rcS|skeleton|hwclock.sh|single|sendsigs|umountfs|umountnfs.sh|umountroot|checkfs.sh|checkroot.sh|checkroot-bootclean.sh|mountkernfs.sh|mountdevsubfs.sh|mountall.sh|mountall-bootclean.sh|mountnfs.sh|mountnfs-bootclean.sh|procps|hostname.sh|networking|ssh|cron|rsyslog|dbus|mysql|nginx|apache2|postfix|postgresql|redis-server|memcached|php*-fpm) continue ;;
|
||||
esac
|
||||
log " unexpected init.d script (review): $f"
|
||||
done
|
||||
fi
|
||||
fi
|
||||
ok "persistence vector scan complete"
|
||||
|
||||
# ---------- 18. file capability audit ---------------------------------------
|
||||
|
||||
log ""
|
||||
log "=== PHASE 18: File capability audit ==="
|
||||
|
||||
if command -v getcap >/dev/null 2>&1; then
|
||||
CAP_HITS=$(getcap -r "$R/usr" "$R/bin" "$R/sbin" 2>/dev/null || true)
|
||||
if [ -n "$CAP_HITS" ]; then
|
||||
while IFS= read -r line; do
|
||||
[ -z "$line" ] && continue
|
||||
f="${line%% =*}"
|
||||
base=$(basename "$f")
|
||||
case "$base" in
|
||||
ping|ping6|mtr-packet|arping|traceroute6.iputils|clockdiff|systemd-detect-virt|suexec) continue ;;
|
||||
esac
|
||||
warn "file capability (review): $line"
|
||||
# auto-flag catastrophic caps
|
||||
if echo "$line" | grep -qE 'cap_sys_admin|cap_dac_read_search|cap_dac_override|cap_setuid|cap_setgid|cap_sys_module|cap_sys_ptrace'; then
|
||||
warn " ^ catastrophic capability — likely backdoor"
|
||||
fi
|
||||
done <<< "$CAP_HITS"
|
||||
fi
|
||||
ok "capability audit complete"
|
||||
else
|
||||
log " getcap not installed on host — skipping"
|
||||
fi
|
||||
|
||||
# ---------- 19. network-level IoC (config files only) -----------------------
|
||||
|
||||
log ""
|
||||
log "=== PHASE 19: Network-level IoC ==="
|
||||
|
||||
# /etc/hosts: anything non-loopback and non-self
|
||||
if [ -f "$R/etc/hosts" ]; then
|
||||
HOSTS_ODD=$(grep -vE '^\s*#|^\s*$|^\s*(127\.|::1|::ffff:127\.|fe80:|ff[0-9a-f]{2}:)' "$R/etc/hosts" 2>/dev/null || true)
|
||||
if [ -n "$HOSTS_ODD" ]; then
|
||||
log " /etc/hosts non-loopback entries (review):"
|
||||
echo "$HOSTS_ODD" | tee -a "$LOG"
|
||||
fi
|
||||
fi
|
||||
|
||||
# nameservers outside known-safe set
|
||||
NS_FILES=""
|
||||
[ -f "$R/etc/resolv.conf" ] && NS_FILES="$R/etc/resolv.conf"
|
||||
[ -f "$R/etc/systemd/resolved.conf" ] && NS_FILES="$NS_FILES $R/etc/systemd/resolved.conf"
|
||||
if [ -d "$R/etc/systemd/resolved.conf.d" ]; then
|
||||
for f in "$R"/etc/systemd/resolved.conf.d/*.conf; do
|
||||
[ -f "$f" ] && NS_FILES="$NS_FILES $f"
|
||||
done
|
||||
fi
|
||||
if [ -n "$NS_FILES" ]; then
|
||||
NS_HITS=$(grep -HnE '^\s*(nameserver|DNS=)' $NS_FILES 2>/dev/null | \
|
||||
grep -vE '(1\.1\.1\.1|1\.0\.0\.1|8\.8\.8\.8|8\.8\.4\.4|9\.9\.9\.9|149\.112\.112\.112|208\.67\.222\.222|208\.67\.220\.220|127\.0\.0\.[0-9]+|::1|fe80:|169\.254\.|192\.168\.|10\.|172\.1[6-9]\.|172\.2[0-9]\.|172\.3[01]\.)' || true)
|
||||
if [ -n "$NS_HITS" ]; then
|
||||
warn "non-standard DNS nameservers configured:"
|
||||
echo "$NS_HITS" | tee -a "$LOG"
|
||||
fi
|
||||
fi
|
||||
|
||||
# C2/pool string sweep across network-relevant config
|
||||
NET_CONFIG_DIRS=""
|
||||
for d in "$R/etc/hosts" "$R/etc/hosts.allow" "$R/etc/hosts.deny" \
|
||||
"$R/etc/NetworkManager" "$R/etc/systemd/network" \
|
||||
"$R/etc/iptables" "$R/etc/nftables.conf" \
|
||||
"$R/etc/network" "$R/etc/netplan"; do
|
||||
[ -e "$d" ] && NET_CONFIG_DIRS="$NET_CONFIG_DIRS $d"
|
||||
done
|
||||
if [ -n "$NET_CONFIG_DIRS" ]; then
|
||||
C2_HITS=$(grep -rlEi 'solscan|xmrpool|nanopool|monerohash|supportxmr|minexmr|moneroocean|c3pool|hashvault|:(3333|5555|7777|14444|14433|9000)\b' $NET_CONFIG_DIRS 2>/dev/null || true)
|
||||
if [ -n "$C2_HITS" ]; then
|
||||
warn "network config files containing mining-pool / C2 strings:"
|
||||
echo "$C2_HITS" | tee -a "$LOG"
|
||||
fi
|
||||
fi
|
||||
ok "network IoC scan complete"
|
||||
|
||||
# ---------- 20. anti-forensic / timestamp anomalies -------------------------
|
||||
|
||||
log ""
|
||||
log "=== PHASE 20: Anti-forensic anomalies ==="
|
||||
|
||||
# files with mtime > 1h in the future (clock-skew tolerant)
|
||||
FUTURE_BOUNDARY=$(date -u -d '+1 hour' '+%Y-%m-%d %H:%M:%S' 2>/dev/null || date -u -v+1H '+%Y-%m-%d %H:%M:%S' 2>/dev/null)
|
||||
FUTURE_FILES=""
|
||||
[ -n "$FUTURE_BOUNDARY" ] && FUTURE_FILES=$(find "$R/etc" "$R/usr/bin" "$R/usr/sbin" "$R/bin" "$R/sbin" \
|
||||
-xdev -newermt "$FUTURE_BOUNDARY" 2>/dev/null | head -50 || true)
|
||||
if [ -n "$FUTURE_FILES" ]; then
|
||||
warn "files with future mtime (anti-forensic timestamp tampering):"
|
||||
echo "$FUTURE_FILES" | tee -a "$LOG"
|
||||
fi
|
||||
|
||||
# history clearing
|
||||
for h in "$R/root/.bash_history" "$R/root/.zsh_history"; do
|
||||
[ -e "$h" ] || continue
|
||||
if [ -L "$h" ]; then
|
||||
target=$(readlink "$h")
|
||||
case "$target" in
|
||||
/dev/null|/dev/zero) warn "shell history symlinked to $target: $h" ;;
|
||||
esac
|
||||
elif [ ! -s "$h" ]; then
|
||||
warn "shell history file is empty: $h"
|
||||
fi
|
||||
done
|
||||
if [ -d "$R/home" ]; then
|
||||
for userhome in "$R"/home/*/; do
|
||||
[ -d "$userhome" ] || continue
|
||||
for h in "$userhome.bash_history" "$userhome.zsh_history"; do
|
||||
[ -e "$h" ] || continue
|
||||
if [ -L "$h" ]; then
|
||||
target=$(readlink "$h")
|
||||
case "$target" in
|
||||
/dev/null|/dev/zero) warn "shell history symlinked to $target: $h" ;;
|
||||
esac
|
||||
elif [ ! -s "$h" ]; then
|
||||
warn "shell history file is empty: $h"
|
||||
fi
|
||||
done
|
||||
done
|
||||
fi
|
||||
|
||||
# unusual xattrs on system binaries
|
||||
if command -v getfattr >/dev/null 2>&1; then
|
||||
XATTR_HITS=$(getfattr -R -m '^user\.' --absolute-names "$R/etc" "$R/usr/bin" "$R/bin" 2>/dev/null | grep -B1 '^user\.' || true)
|
||||
if [ -n "$XATTR_HITS" ]; then
|
||||
warn "system files with user.* xattrs (review — can hide data):"
|
||||
echo "$XATTR_HITS" | head -40 | tee -a "$LOG"
|
||||
fi
|
||||
fi
|
||||
ok "anti-forensic scan complete"
|
||||
|
||||
# ---------- 21. Docker-in-container artifacts -------------------------------
|
||||
|
||||
log ""
|
||||
log "=== PHASE 21: Docker-in-container artifacts ==="
|
||||
|
||||
if [ -d "$R/var/lib/docker" ] || [ -d "$R/var/lib/containerd" ]; then
|
||||
warn "container runtime dirs present inside this container — unusual"
|
||||
|
||||
if [ -d "$R/var/lib/docker/containers" ]; then
|
||||
for cfg in "$R"/var/lib/docker/containers/*/config.v2.json; do
|
||||
[ -f "$cfg" ] || continue
|
||||
img=$(grep -oE '"Image":"[^"]+"' "$cfg" 2>/dev/null | head -1)
|
||||
log " docker container config: $cfg image=$img"
|
||||
if echo "$img" | grep -qiE 'xmrig|bitping|repocket|traffmonetiz|peer2profit|earnapp|honeygain|packetstream|mining|miner'; then
|
||||
warn " ^ known proxyjacking/mining image"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -f "$R/etc/docker/daemon.json" ]; then
|
||||
if grep -qE '"hosts"\s*:\s*\[[^]]*tcp://' "$R/etc/docker/daemon.json" 2>/dev/null; then
|
||||
warn "docker daemon.json exposes TCP socket:"
|
||||
grep -nE '"hosts"' "$R/etc/docker/daemon.json" | tee -a "$LOG"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
log " no container runtime artifacts present"
|
||||
fi
|
||||
ok "container-runtime scan complete"
|
||||
|
||||
# ---------- 22. optional third-party scanners (gated by env) ----------------
|
||||
|
||||
log ""
|
||||
log "=== PHASE 22: Optional third-party scanners ==="
|
||||
|
||||
if [ "${RUN_CHKROOTKIT:-0}" = "1" ] && command -v chkrootkit >/dev/null 2>&1; then
|
||||
log " running chkrootkit against $R (may take several minutes)..."
|
||||
chkrootkit -r "$R" 2>&1 | tee -a "$LOG" || true
|
||||
fi
|
||||
|
||||
if [ "${RUN_RKHUNTER:-0}" = "1" ] && command -v rkhunter >/dev/null 2>&1; then
|
||||
log " running rkhunter against $R (may take several minutes)..."
|
||||
rkhunter --rootdir "$R" --check --sk --nocolors 2>&1 | tee -a "$LOG" || true
|
||||
fi
|
||||
|
||||
if [ -n "${YARA_RULES:-}" ] && [ -f "$YARA_RULES" ] && command -v yara >/dev/null 2>&1; then
|
||||
log " running yara with rules $YARA_RULES..."
|
||||
yara -r "$YARA_RULES" "$R/usr" "$R/bin" "$R/lib" "$R/tmp" "$R/opt" 2>&1 | tee -a "$LOG" || true
|
||||
fi
|
||||
|
||||
if [ "${RUN_CLAMAV:-0}" = "1" ] && command -v clamscan >/dev/null 2>&1; then
|
||||
log " running clamscan against $R (slow)..."
|
||||
clamscan -ri "$R" --exclude-dir='^'"$R"'/(proc|sys|dev)' 2>&1 | tee -a "$LOG" || true
|
||||
fi
|
||||
|
||||
if [ "${RUN_CHKROOTKIT:-0}" != "1" ] && [ "${RUN_RKHUNTER:-0}" != "1" ] \
|
||||
&& [ -z "${YARA_RULES:-}" ] && [ "${RUN_CLAMAV:-0}" != "1" ]; then
|
||||
log " no third-party scanners requested (set RUN_CHKROOTKIT=1, RUN_RKHUNTER=1,"
|
||||
log " YARA_RULES=/path/rules.yar, or RUN_CLAMAV=1 to enable)"
|
||||
fi
|
||||
ok "third-party scanner phase complete"
|
||||
|
||||
# ---------- 23. debsums package verification (cold-disk) --------------------
|
||||
|
||||
if command -v debsums >/dev/null 2>&1; then
|
||||
log ""
|
||||
log "=== PHASE 23: Package integrity verification (debsums) ==="
|
||||
DEBSUMS_MISMATCHES=$(debsums --root="$R" -c 2>&1 || true)
|
||||
if [ -n "$DEBSUMS_MISMATCHES" ]; then
|
||||
warn "debsums found mismatched files:"
|
||||
echo "$DEBSUMS_MISMATCHES" | tee -a "$LOG"
|
||||
else
|
||||
ok "all package files match expected checksums"
|
||||
fi
|
||||
else
|
||||
log ""
|
||||
log "=== PHASE 23: debsums not installed on host — skipping ==="
|
||||
fi
|
||||
|
||||
# ---------- 24. SUID/SGID audit (cold-disk) ---------------------------------
|
||||
|
||||
log ""
|
||||
log "=== PHASE 24: SUID/SGID binary audit ==="
|
||||
|
||||
find "$R" -xdev \( -perm -4000 -o -perm -2000 \) -type f 2>/dev/null | while read -r f; do
|
||||
case "$(basename "$f")" in
|
||||
mount.cifs|mount.nfs*|unix_chkpwd|su|sudo|mount|umount|passwd|chsh|chfn|gpasswd|newgrp|pkexec|ping|ping6|fusermount|fusermount3|ssh-keysign|at|expiry|crontab|chage) ;;
|
||||
*) warn "SUID/SGID binary (review): $f ($(ls -la "$f" 2>/dev/null))" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
ok "SUID/SGID audit complete"
|
||||
|
||||
# ---------- summary ----------------------------------------------------------
|
||||
|
||||
log ""
|
||||
log "=========================================="
|
||||
log "CLEANUP COMPLETE"
|
||||
log " IoC items found: $FOUND"
|
||||
log " IoC items removed: $REMOVED"
|
||||
log " Errors: $ERRORS"
|
||||
log " Log saved to: $LOG"
|
||||
log " Forensics saved: $FORENSICS_DIR"
|
||||
log "=========================================="
|
||||
|
||||
if [ "$ERRORS" -gt 0 ]; then
|
||||
warn "some items could not be removed — review log"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log ""
|
||||
log "RECOMMENDED NEXT STEPS:"
|
||||
log " 1. Rotate ALL credentials for this container (SSH keys, DB passwords,"
|
||||
log " API tokens) — the rootkit's TTY keylogger + PAM hook captured everything"
|
||||
log " 2. Check $FORENSICS_DIR for saved malware samples"
|
||||
log " 3. Submit libgcwrap.so to VirusTotal for attribution"
|
||||
log " 4. Re-run this script against the same rootfs to verify cleanup"
|
||||
log " 5. Before starting the container again, confirm no persistence remains"
|
||||
|
||||
exit 0
|
||||
Reference in New Issue
Block a user