#!/usr/bin/env bash # php2shell-check.sh - looks for traces of PHP2Shell abuse (CVE-2026-109618, CVE-2026-109619) # https://nokkela.blog/en/content/php2shell-php-rce.html # # SPDX-License-Identifier: BSD-2-Clause # Copyright (c) 2026 Nokkela-IT-Concept GmbH # Status and spinner functions from lxc-to-go, Copyright (c) Daniel Plominski (BSD-2-Clause). # # Redistribution and use in source and binary forms, with or without modification, are permitted # provided that the following conditions are met: # 1. Redistributions of source code must retain the above copyright notice, this list of # conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright notice, this list of # conditions and the following disclaimer in the documentation and/or other materials provided # with the distribution. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND # FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER # IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT # OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # Read-only check for Debian and Ubuntu hosts running PHP from dpkg packages (distribution or # Ondrej Sury) behind Apache mod_php, PHP-FPM or php-cgi. Nothing on the system is changed; the # only file written is the report. # # The console shows fixed texts, counts and report line numbers only, so a whole run (command # line and output) can go into a chat as it is. Paths, processes, addresses and log lines go to # the report under /opt/php2shell-check/reports (directory 0700, report 0600). # # sudo bash php2shell-check.sh lookback from 2026-10-17 # sudo SINCE=2026-08-01 bash php2shell-check.sh longer lookback # # Signals from the public advisories (2026-10-22): memory exhaustion while PHP sets up a request # ("in Unknown on line 0"), crashed Apache/PHP workers and core dumps, session errors and # sessions created by upload progress alone, published scanner user agents and source addresses, # processes of the web user that are no web server, programs running from /tmp or memory, # script files written by the web user, persistence (cron, SSH keys, systemd, accounts), # modified package files. A successful exploit can leave no log entry at all; the process and # file checks carry the weight. set -u umask 077 export LC_ALL=C SCRIPT_VERSION=1.0.0 SINCE=${SINCE:-2026-10-17} REPORT_DIR=/opt/php2shell-check/reports # PHP source package and fixed version per release (USN-8948-1/-2, DSA-6574-1, DLA-4848-1; # all 2026-10-21) declare -A FIX_SRC=([stonking]=php8.5 [resolute]=php8.5 [noble]=php8.3 [jammy]=php8.1 [focal]=php7.4 [bionic]=php7.2 [xenial]=php7.0 [trixie]=php8.4 [bookworm]=php8.2) declare -A FIX_VER=([stonking]=8.5.9-0ubuntu2.1 [resolute]=8.5.4-0ubuntu1.5 [noble]=8.3.6-0ubuntu0.24.04.13 [jammy]=8.1.2-1ubuntu2.28 [focal]=7.4.3-4ubuntu2.29+esm5 [bionic]=7.2.24-0ubuntu0.18.04.17+esm14 [xenial]=7.0.33-0ubuntu0.16.04.16+esm22 [trixie]=8.4.27-1~deb13u1 [bookworm]=8.2.35-1~deb12u1) # Fixed versions of the builds by Ondrej Sury (PPA ondrej/php, packages.sury.org; 2026-10-21/22) declare -A SURY_FIX=([php8.5]=8.5.12 [php8.4]=8.4.27 [php8.3]=8.3.36 [php8.2]=8.2.35 [php8.1]=8.1.34-9 [php8.0]=1:8.0.30-25 [php7.4]=1:7.4.33-31 [php7.3]=7.3.33-35 [php7.2]=7.2.34-66 [php7.1]=7.1.33-80 [php7.0]=7.0.33-92 [php5.6]=5.6.40-105) # Published indicators (Amazon, Sophos, ELLIO; 2026-10-22): scanner user agents, canary strings # and source addresses. Amazon advises to investigate these addresses, not to block on them. IOC_RE='php2shell-(scan|verify|test)|JACKALPOC|P2SMARK|PHP2SHELL_OK' IOC_RE+='|(^|[^0-9.])(103\.216\.220\.92|31\.223\.184\.166|37\.19\.200\.5|134\.209\.159\.64' IOC_RE+='|103\.114\.163\.188|95\.217\.98\.37|45\.198\.224\.169|176\.65\.132\.128' IOC_RE+='|92\.118\.112\.100)([^0-9.]|$)' # Process names of web servers and PHP workers WEB_COMM_RE='^(apache2|httpd|nginx|php-fpm.*|php-cgi.*|lsphp.*|frankenphp)$' # Names of files a web server would execute or that change how it executes files SCRIPT_NAMES=(\( -iname '*.php' -o -iname '*.php[0-9]' -o -iname '*.pht' -o -iname '*.phtml' -o -iname '*.phar' -o -iname '*.phps' -o -iname '*.php.*' -o -name .htaccess -o -name .user.ini \)) #// FUNCTION: status lines in the lxc-to-go format (Daniel Plominski, BSD-2-Clause) _ok() { printf '[\033[1;32m OK \033[0m] %s\n' "'$*'"; } _failed() { printf '[\033[1;31mFAILED\033[0m] %s\n' "'$*'"; } _soft() { printf '[\033[1;33mFAILED\033[0m] %s\n' "'$*'"; } info() { printf '[\033[1;36m INFO \033[0m] %s\n' "$*"; } header() { printf '\n\033[1;35m### %s\033[0m\n' "$*"; } #// FUNCTION: spinner (lxc-to-go 1.0, plus exit code of the job); draws on /dev/tty only spinner() { local pid=$1 spinstr='|/-\' temp out=/dev/null if { true >/dev/tty; } 2>/dev/null; then out=/dev/tty; fi while kill -0 "$pid" 2>/dev/null; do temp=${spinstr#?} printf ' [%c] ' "$spinstr" >"$out" spinstr=$temp${spinstr%"$temp"} sleep 0.1 printf '\b\b\b\b\b\b' >"$out" done printf ' \b\b\b\b\b\b' >"$out" wait "$pid" } #// FUNCTION: run one check with spinner; each output line is a finding and goes to the report # run_check RED=0 YELLOW=0 FIRST_LINE= run_check() { local level=$1 clean=$2 found=$3 fn=$4 out n from to where out=$TMPD/$fn ("$fn" >"$out" 2>/dev/null) & spinner $! n=$(grep -c . "$out") if [ "$n" -eq 0 ]; then if [ "$level" = info ]; then info "$clean"; else _ok "$clean"; fi return 0 fi found=${found//@N@/$n} from=$(($(wc -l <"$REPORT") + 3)) { printf '\n## %s\n' "$found"; grep . "$out"; } >>"$REPORT" to=$(wc -l <"$REPORT") where="report lines $from-$to" [ "$from" -eq "$to" ] && where="report line $from" case $level in hard) RED=$((RED + 1)); _failed "$found ($where)" ;; soft) YELLOW=$((YELLOW + 1)); _soft "$found ($where)" ;; *) info "$found ($where)" ;; esac [ -n "$FIRST_LINE" ] || FIRST_LINE=$((from - 1)) } #// FUNCTION: uids of the users running web servers or PHP workers, plus www-data find_web_uids() { local d comm uid { for d in /proc/[0-9]*; do comm=$(cat "$d/comm" 2>/dev/null) || continue [[ $comm =~ $WEB_COMM_RE ]] || continue uid=$(awk '/^Uid:/ {print $2; exit}' "$d/status" 2>/dev/null) [ -n "$uid" ] && [ "$uid" != 0 ] && echo "$uid" done id -u www-data 2>/dev/null } | sort -un | tr '\n' ' ' } #// FUNCTION: document roots of Apache and nginx plus the usual places, one per line collect_webroots() { local d { printf '%s\n' /var/www /srv/www /srv/http /usr/share/nginx/html grep -rhiE '^[[:space:]]*DocumentRoot[[:space:]]+' /etc/apache2 /etc/httpd 2>/dev/null | awk '{print $2}' | tr -d '"' grep -rhE '^[[:space:]]*root[[:space:]]+[^;]+;' /etc/nginx 2>/dev/null | awk '{print $2}' | tr -d '";' } | while read -r d; do [ -d "$d" ] && readlink -f "$d" done | sort -u } #// FUNCTION: absolute paths on stdin plus their rotations, changed since SINCE, one per line recent_with_rotations() { local f g while read -r f; do case $f in /*) ;; *) continue ;; esac for g in "$f" "$f".*; do [ -n "$(find "$g" -maxdepth 0 -type f -newermt "$SINCE" 2>/dev/null)" ] && echo "$g" done done | sort -u } #// FUNCTION: error logs of Apache, nginx and PHP changed since SINCE, one per line collect_logs() { { find /var/log/apache2 /var/log/httpd /var/log/nginx -maxdepth 1 -type f -name '*error*' 2>/dev/null find /var/log -maxdepth 1 -type f -name 'php*.log*' 2>/dev/null # error logs named in the Apache and PHP configuration grep -rhiE '^[[:space:]]*ErrorLog[[:space:]]+[^|[:space:]]' /etc/apache2 /etc/httpd 2>/dev/null | awk '{print $2}' | sed -e 's/"//g' -e 's|\${APACHE_LOG_DIR}|/var/log/apache2|' grep -hE '^[[:space:]]*(php_admin_value\[error_log\]|php_value\[error_log\]|error_log)[[:space:]]*=' \ /etc/php/*/*/php.ini /etc/php/*/fpm/pool.d/*.conf 2>/dev/null | sed -e 's/^[^=]*=[[:space:]]*//' -e 's/"//g' } | recent_with_rotations } #// FUNCTION: access logs of Apache and nginx changed since SINCE, one per line collect_access_logs() { { find /var/log/apache2 /var/log/httpd /var/log/nginx -maxdepth 1 -type f -name '*access*' 2>/dev/null # access logs named in the Apache and nginx configuration grep -rhiE '^[[:space:]]*(CustomLog|TransferLog)[[:space:]]+[^|[:space:]]' /etc/apache2 /etc/httpd 2>/dev/null | awk '{print $2}' | sed -e 's/"//g' -e 's|\${APACHE_LOG_DIR}|/var/log/apache2|' grep -rhE '^[[:space:]]*access_log[[:space:]]+/' /etc/nginx 2>/dev/null | awk '{print $2}' | tr -d ';' } | recent_with_rotations } #// FUNCTION: installed packages built from a versioned PHP source, as "package|version|source" php_pkgs() { dpkg-query -W -f='${db:Status-Abbrev}|${Package}|${Version}|${source:Package}\n' 2>/dev/null | awk -F'|' '$1 ~ /^ii/ && $4 ~ /^php[0-9]+\.[0-9]+$/ {print $2 "|" $3 "|" $4}' } #// FUNCTION: file is shipped by a package and still as shipped (dpkg knows its checksum) pkg_pristine() { local owner owner=$(dpkg -S "$1" 2>/dev/null | grep -v '^diversion ' | awk -F': ' -v f="$1" '$NF == f {split($1, a, ", "); print a[1]; exit}') [ -n "$owner" ] && dpkg -s "$owner" >/dev/null 2>&1 || return 1 ! dpkg -V "$owner" 2>/dev/null | awk '{print $NF}' | grep -qxF "$1" } #// FUNCTION: package version built by Ondrej Sury (PPA ondrej/php or packages.sury.org) is_sury() { [[ $1 == *sury* || $1 =~ \+0~20[0-9]{6} ]] } #// FUNCTION: one finding for a PHP package below its fixed version, nothing when it is fixed pkg_finding() { local pkg=$1 ver=$2 src=$3 fix if is_sury "$ver"; then fix=${SURY_FIX[$src]:-} if [ -z "$fix" ]; then echo "$pkg $ver: Sury build without a known fixed version" elif ! dpkg --compare-versions "$ver" ge "$fix"; then echo "$pkg $ver: Sury build below $fix" fi elif [ -n "$PHP_SRC" ] && [ "$src" = "$PHP_SRC" ]; then dpkg --compare-versions "$ver" ge "$PHP_FIX" || echo "$pkg $ver: below $PHP_FIX" else echo "$pkg $ver: PHP version without a known fixed version for this release" fi } #// CHECK: PHP packages below the fixed version or without a known fix chk_patch() { local pkg ver src php_pkgs | while IFS='|' read -r pkg ver src; do pkg_finding "$pkg" "$ver" "$src" done } #// CHECK: processes that still map replaced PHP files (updated, but not restarted) chk_oldcode() { local m pid for m in /proc/[0-9]*/maps; do grep -qsE '/[^ ]*php[^ ]* \(deleted\)$' "$m" || continue pid=${m#/proc/} pid=${pid%/maps} echo "pid=$pid comm=$(cat "/proc/$pid/comm" 2>/dev/null)" done } #// CHECK: PHP failed while setting up a request (memory exhaustion, oversized input) chk_log_init() { xargs -r -d '\n' zgrep -H -n -E 'in Unknown on line 0' <"$TMPD/logs" | cut -c1-300 } #// CHECK: Apache or PHP workers that died on a signal chk_log_crash() { xargs -r -d '\n' zgrep -H -n -E 'AH00052: child pid [0-9]+ exit signal|exited on signal (4|6|7|8|11)\b' \ <"$TMPD/logs" | cut -c1-300 if command -v journalctl >/dev/null; then journalctl -k -q --no-pager --since "$SINCE" 2>/dev/null | grep -E '(apache2|httpd|php[^[ ]*)\[[0-9]+\]: segfault' | cut -c1-300 fi find /var/lib/systemd/coredump /var/crash -type f -newermt "$SINCE" \ \( -iname '*php*' -o -iname '*apache2*' -o -iname '*httpd*' \) -printf 'core dump: %p\n' 2>/dev/null } #// CHECK: session errors and sessions created by upload progress alone (CVE-2026-109619 path) chk_log_session() { xargs -r -d '\n' zgrep -H -n -E 'Failed to decode session object|Session object destruction failed' \ <"$TMPD/logs" | cut -c1-300 find /var/lib/php/sessions /tmp -maxdepth 4 -type f -name 'sess_*' -newerct "$SINCE" \ -exec grep -l -m1 '^upload_progress_' {} + 2>/dev/null | sed 's/^/upload-progress session: /' } #// CHECK: published indicators: scanner user agents and source addresses, known payload name chk_iocs() { xargs -r -d '\n' zgrep -H -n -i -E "$IOC_RE" <"$TMPD/access" | cut -c1-300 find /tmp /var/tmp /dev/shm -maxdepth 4 -type f -name nfs -printf 'known payload name: %p\n' 2>/dev/null } #// CHECK: processes of the web users that are no web server or PHP worker chk_webuser_procs() { local d pid uid comm ppid socks args for d in /proc/[0-9]*; do pid=${d#/proc/} uid=$(awk '/^Uid:/ {print $2; exit}' "$d/status" 2>/dev/null) || continue case " $WEB_UIDS " in *" $uid "*) ;; *) continue ;; esac comm=$(cat "$d/comm" 2>/dev/null) || continue [[ $comm =~ $WEB_COMM_RE ]] && continue ppid=$(awk '/^PPid:/ {print $2; exit}' "$d/status" 2>/dev/null) socks=$(find "$d/fd" -lname 'socket:*' 2>/dev/null | wc -l) args=$(tr '\0' ' ' <"$d/cmdline" 2>/dev/null | cut -c1-200) echo "pid=$pid uid=$uid comm=$comm parent=$(cat "/proc/$ppid/comm" 2>/dev/null) sockets=$socks cmd=$args" done } #// CHECK: programs running from temp directories, memory, a web root or a deleted file chk_strange_exe() { local d exe r hit for d in /proc/[0-9]*; do exe=$(readlink "$d/exe" 2>/dev/null) || continue hit=0 case $exe in /tmp/* | /var/tmp/* | /dev/shm/* | /memfd:*) hit=1 ;; *" (deleted)") case $exe in /usr/* | /lib/* | /lib64/* | /bin/* | /sbin/* | /opt/* | /snap/*) ;; *) hit=1 ;; esac ;; esac for r in "${WEBROOTS[@]}"; do case $exe in "$r"/*) hit=1 ;; esac done [ "$hit" = 1 ] || continue echo "pid=${d#/proc/} uid=$(awk '/^Uid:/ {print $2; exit}' "$d/status" 2>/dev/null) exe=$exe" \ "cmd=$(tr '\0' ' ' <"$d/cmdline" 2>/dev/null | cut -c1-200)" done } #// CHECK: script files in the web roots written by a web user since SINCE chk_web_scripts_webuser() { [ ${#WEBROOTS[@]} -gt 0 ] || return 0 find "${WEBROOTS[@]}" -type f "${SCRIPT_NAMES[@]}" -newerct "$SINCE" \( "${UID_EXPR[@]}" \) \ -printf '%CY-%Cm-%Cd %CH:%CM uid=%U %p\n' 2>/dev/null | sort -u } #// CHECK: other script files in the web roots changed since SINCE (deployments, updates) chk_web_scripts_other() { [ ${#WEBROOTS[@]} -gt 0 ] || return 0 find "${WEBROOTS[@]}" -type f "${SCRIPT_NAMES[@]}" -newerct "$SINCE" ! \( "${UID_EXPR[@]}" \) \ -printf '%CY-%Cm-%Cd %CH:%CM uid=%U %p\n' 2>/dev/null | sort -u } #// CHECK: files of a web user in the temp directories since SINCE chk_tmp_webuser() { find /tmp /var/tmp /dev/shm -type f -newerct "$SINCE" \( "${UID_EXPR[@]}" \) \ -printf '%CY-%Cm-%Cd %CH:%CM uid=%U %p\n' 2>/dev/null } #// CHECK: persistence (cron, SSH keys, shells of web users, uid 0, preload, changed system files) chk_persistence() { local u name home shell f t for u in $WEB_UIDS; do IFS=: read -r name _ _ _ _ home shell < <(getent passwd "$u") [ -n "${name:-}" ] || continue [ -s "/var/spool/cron/crontabs/$name" ] && echo "crontab of the web user: /var/spool/cron/crontabs/$name" for f in "$home/.ssh/authorized_keys" "$home/.ssh/authorized_keys2"; do [ -e "$f" ] && echo "SSH keys of the web user: $f" done case $shell in */nologin | */false | '') ;; *) echo "web user $name has the login shell $shell" ;; esac done awk -F: '$3 == 0 && $1 != "root" {print "further account with uid 0: " $1}' /etc/passwd [ -e /etc/ld.so.preload ] && echo "/etc/ld.so.preload present" find /etc/crontab /etc/cron.d /etc/cron.hourly /etc/cron.daily /etc/cron.weekly /etc/cron.monthly \ /var/spool/cron /etc/systemd/system /etc/sudoers /etc/sudoers.d /etc/passwd /etc/shadow \ /etc/group /etc/rc.local /etc/profile.d /etc/ld.so.conf.d /root/.ssh /root/.bashrc \ /root/.profile /root/.config/systemd /home/*/.ssh /home/*/.bashrc /home/*/.profile \ /home/*/.config/systemd \( -type f -o -type l \) -newerct "$SINCE" \ -printf '%CY-%Cm-%Cd %CH:%CM %p\n' 2>/dev/null | while read -r _ _ f; do pkg_pristine "$f" && continue # enabling a unit a package ships unchanged (wants/ links) is no persistence if [ -L "$f" ]; then t=$(readlink "$f") case $t in /*) ;; *) t=$(dirname "$f")/$t ;; esac pkg_pristine "$t" && continue fi echo "changed since $SINCE: $f" done } #// CHECK: package files whose checksum differs from the package (configuration files excluded) chk_dpkg_verify() { dpkg --verify 2>/dev/null | awk 'substr($1, 3, 1) == "5" && $2 != "c"' } #// CHECK: successful SSH logins since SINCE, for comparison with your own chk_logins() { journalctl -q --no-pager --since "$SINCE" -t sshd 2>/dev/null | grep -E ': Accepted ' | cut -c1-200 } usage() { cat <&2; exit 1 ;; esac if [ "$(id -u)" != 0 ]; then echo "[ERROR] This script must run as root (sudo)" 1>&2 exit 1 fi if ! command -v dpkg-query >/dev/null; then echo "[ERROR] This script supports Debian and Ubuntu (dpkg) only" 1>&2 exit 1 fi if ! date -d "$SINCE" >/dev/null 2>&1; then echo "[ERROR] SINCE is not a date: $SINCE" 1>&2 exit 1 fi install -d -m 0700 "$REPORT_DIR" || exit 1 REPORT=$REPORT_DIR/report-$(date +%Y%m%d-%H%M%S).txt TMPD=$(mktemp -d) || exit 1 trap 'rm -rf "$TMPD"' EXIT # shellcheck disable=SC1091 . /etc/os-release PHP_SRC=${FIX_SRC[${VERSION_CODENAME:-none}]:-} PHP_FIX=${FIX_VER[${VERSION_CODENAME:-none}]:-} WEB_UIDS=$(find_web_uids) UID_EXPR=() for f in $WEB_UIDS; do [ ${#UID_EXPR[@]} -eq 0 ] || UID_EXPR+=(-o) UID_EXPR+=(-uid "$f") done [ ${#UID_EXPR[@]} -gt 0 ] || UID_EXPR=(-false) mapfile -t WEBROOTS < <(collect_webroots) collect_logs >"$TMPD/logs" collect_access_logs >"$TMPD/access" { echo "php2shell-check $SCRIPT_VERSION (CVE-2026-109618, CVE-2026-109619)" echo "Host: $(hostname)" echo "Run: $(date '+%Y-%m-%d %H:%M:%S %z')" echo "Since: $SINCE" echo "System: ${PRETTY_NAME:-unknown}" echo "Web users (uid): ${WEB_UIDS% }" echo "Web roots:" [ ${#WEBROOTS[@]} -eq 0 ] || printf ' %s\n' "${WEBROOTS[@]}" echo "Error logs:" sed 's/^/ /' "$TMPD/logs" echo "Access logs:" sed 's/^/ /' "$TMPD/access" } >"$REPORT" header "php2shell-check $SCRIPT_VERSION (CVE-2026-109618, CVE-2026-109619), since $SINCE" info "Platform ${PRETTY_NAME:-unknown}" info "Web users: $(wc -w <<<"$WEB_UIDS"), web roots: ${#WEBROOTS[@]}," \ "error logs: $(grep -c . "$TMPD/logs"), access logs: $(grep -c . "$TMPD/access")" header "PHP packages" n_all=$(php_pkgs | grep -c .) if [ "$n_all" -eq 0 ]; then info "No PHP packages installed (PHP inside containers is not checked here)" else run_check hard "All $n_all PHP packages at a fixed version" \ "PHP packages below the fixed version or without a known fix: @N@" chk_patch fi run_check hard "Running processes use the current PHP code" \ "Processes still running old PHP code, restart the service: @N@" chk_oldcode header "Attack attempts in the logs" run_check soft "No errors while setting up requests (Unknown on line 0)" \ "Errors while setting up requests (Unknown on line 0): @N@" chk_log_init run_check soft "No crashed Apache or PHP workers, no core dumps" \ "Crashed Apache or PHP workers, core dumps: @N@" chk_log_crash run_check soft "No session errors or upload-progress sessions" \ "Session errors and upload-progress sessions: @N@" chk_log_session run_check soft "No published PHP2Shell indicators (scanners, source IPs, payload)" \ "Published PHP2Shell indicators (scanners, source IPs, payload): @N@" chk_iocs header "Traces of a compromise" run_check soft "No foreign processes under the web user" \ "Foreign processes under the web user: @N@" chk_webuser_procs run_check soft "No programs running from /tmp, /dev/shm, memory or a web root" \ "Programs running from /tmp, /dev/shm, memory or a web root: @N@" chk_strange_exe run_check soft "No script files written by the web user" \ "Script files written by the web user: @N@" chk_web_scripts_webuser run_check info "No other changed script files" \ "Other changed script files (deployment?): @N@" chk_web_scripts_other run_check soft "No files of the web user in /tmp, /var/tmp, /dev/shm" \ "Files of the web user in /tmp, /var/tmp, /dev/shm: @N@" chk_tmp_webuser run_check soft "No signs of persistence (cron, SSH, systemd, accounts)" \ "Signs of persistence (cron, SSH, systemd, accounts): @N@" chk_persistence run_check soft "Package files unchanged (dpkg --verify)" \ "Package files differing from their package: @N@" chk_dpkg_verify if command -v journalctl >/dev/null; then run_check info "No SSH logins in the journal" \ "SSH logins in the journal, compare with your own: @N@" chk_logins else info "No journal available, SSH logins not checked" fi header "Result" if [ $((RED + YELLOW)) -eq 0 ]; then _ok "No traces found" elif [ "$RED" -gt 0 ]; then _failed "Red: $RED, yellow: $YELLOW" else _soft "Yellow: $YELLOW, review and assess" fi command -v docker >/dev/null && info "Docker present: PHP inside containers is not checked by this script" info "Report: $REPORT" [ -z "$FIRST_LINE" ] || info "View all findings: tail -n +$FIRST_LINE $REPORT" [ "$RED" -gt 0 ] && exit 2 [ "$YELLOW" -gt 0 ] && exit 1 exit 0 } # PHP2SHELL_CHECK_LIB=1 loads the functions without running the check (used by the test bed) [ "${PHP2SHELL_CHECK_LIB:-}" = 1 ] || main "$@"