#!/usr/bin/env bash # php2shell-check.sh — read-only PHP2Shell (CVE-2026-109618 / CVE-2026-109619) evidence collector # https://scan.theadminstack.com/php2shell/ # # What it does: finds PHP binaries (CLI, FPM, LiteSpeed lsphp, cPanel EA-PHP, Remi SCL, Plesk), # Apache mod_php modules, their package versions and the php.ini settings that matter, AND which of # them are actually in use (running processes, Apache workers with mod_php loaded, enabled systemd # units, cron jobs, PATH default). Run it with sudo so it can see other users' processes. Then prints # a plain-text block. Paste that block into the page; the verdict is calculated in your browser. # # What it does NOT do: change anything, send anything anywhere, or need root (root only helps it # read FPM pool files). Review it before running: less php2shell-check.sh set -u export LC_ALL=C INI_KEYS="session.upload_progress.enabled memory_limit expose_php fatal_error_backtraces file_uploads post_max_size" say() { printf '%s\n' "$*"; } say "#php2shell-check v2 bash" say "[host]" say "hostname=$(hostname 2>/dev/null || uname -n)" if [ -r /etc/os-release ]; then # shellcheck disable=SC1091 . /etc/os-release say "os_id=${ID:-unknown}" say "os_version=${VERSION_ID:-unknown}" say "os_name=${PRETTY_NAME:-unknown}" fi say "kernel=$(uname -sr)" say "collected_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)" say "usage_scan=2" if [ "$(id -u)" = 0 ]; then say "elevated=1"; else say "elevated=0"; fi # ---- usage evidence: what actually runs PHP on this host ---------------------------------------- RUN_EXES=""; APACHE_MAPS="" for d in /proc/[0-9]*; do e=$(readlink "$d/exe" 2>/dev/null) || continue e=${e% (deleted)} case "$(basename "$e")" in php*|lsphp*) RUN_EXES="$RUN_EXES $e" ;; httpd|apache2|httpd.worker|httpd.event) m=$(grep -oE '/[^ ]*libphp[^ ]*\.so' "$d/maps" 2>/dev/null | head -1); [ -n "$m" ] && APACHE_MAPS="$APACHE_MAPS $m" ;; esac done PATH_PHP=$(command -v php 2>/dev/null); PATH_PHP_REAL=$(readlink -f "$PATH_PHP" 2>/dev/null) CRON_TEXT=$(cat /etc/crontab /etc/cron.d/* /var/spool/cron/* /var/spool/cron/crontabs/* 2>/dev/null) ENABLED_UNITS=$(systemctl list-unit-files --state=enabled --no-legend 2>/dev/null | awk '{print $1}' | grep -E 'php|lsws|litespeed|httpd|apache2' | tr '\n' ' ') [ -n "$ENABLED_UNITS" ] && say "enabled_units=${ENABLED_UNITS% }" usage_for() { # $1 path → prints "rolesdetails" local real roles="" det="" n real=$(readlink -f "$1" 2>/dev/null || echo "$1") n=$(printf '%s\n' "$RUN_EXES" | grep -cxF "$real") if [ "$n" -gt 0 ]; then roles="$roles,running-process"; det="$det; $n running process(es)"; fi if printf '%s\n' "$APACHE_MAPS" | grep -qxF "$real"; then roles="$roles,apache-module"; det="$det; loaded by running Apache workers"; fi if [ -n "$PATH_PHP_REAL" ] && [ "$real" = "$PATH_PHP_REAL" ]; then roles="$roles,path-default"; det="$det; what 'php' runs on PATH"; fi if [ -n "$CRON_TEXT" ] && printf '%s' "$CRON_TEXT" | grep -qE "(^|[[:space:]/])($(basename "$1")|$(basename "$real"))([[:space:]]|$)"; then roles="$roles,scheduled-task"; det="$det; referenced by cron"; fi printf '%s\t%s' "${roles#,}" "${det#; }" } # FPM pool overrides for the settings that matter (php_admin_* beats php.ini) pool_hits=$(grep -rhsE 'php_(admin_)?(value|flag)\[(session\.upload_progress\.enabled|memory_limit|fatal_error_backtraces)\]' \ /etc/php/*/fpm/pool.d /etc/php-fpm.d /etc/opt/remi/*/php-fpm.d /opt/cpanel/ea-php*/root/etc/php-fpm.d 2>/dev/null | sed 's/^[[:space:]]*//' | sort -u | tr '\n' ';') [ -n "$pool_hits" ] && say "fpm_pool_overrides=${pool_hits}" pkg_for() { # $1 = absolute path → "managernameversion" local p name ver p=$(readlink -f "$1" 2>/dev/null || echo "$1") if command -v dpkg-query >/dev/null 2>&1; then name=$(dpkg -S "$p" 2>/dev/null | head -1 | cut -d: -f1) if [ -n "$name" ]; then ver=$(dpkg-query -W -f='${Version}' "$name" 2>/dev/null) printf 'dpkg\t%s\t%s' "$name" "$ver"; return fi fi if command -v rpm >/dev/null 2>&1; then local q q=$(rpm -qf --qf '%{NAME}\t%{VERSION}-%{RELEASE}\n' "$p" 2>/dev/null | head -1) if [ -n "$q" ] && ! printf '%s' "$q" | grep -q 'not owned'; then printf 'rpm\t%s' "$q"; return; fi fi if command -v apk >/dev/null 2>&1; then local o o=$(apk info -W "$p" 2>/dev/null | sed -n 's/.* is owned by //p' | head -1) [ -n "$o" ] && { printf 'apk\t%s\t%s' "${o%-*-*}" "${o#"${o%-*-*}"-}"; return; } fi printf '\t\t' } emit_install() { # $1 path, $2 kind (cli|fpm|lsphp|apache-module) local bin="$1" kind="$2" ver sapi info say "[php]" say "path=${bin}" say "kind=${kind}" IFS=$'\t' read -r uroles udet <<<"$(usage_for "$bin")" say "in_use=${uroles}" [ -n "$udet" ] && say "usage_detail=${udet}" case "$kind" in cli) ver=$("$bin" -n -r 'echo PHP_VERSION;' 2>/dev/null) say "php_version=${ver}" say "sapi=cli" "$bin" -r 'foreach (explode(" ", getenv("P2S_KEYS")) as $k) { $v = ini_get($k); if ($v !== false) echo "ini.$k=$v\n"; }' 2>/dev/null ;; fpm|lsphp) ver=$("$bin" -v 2>/dev/null | head -1 | sed -nE 's/^PHP ([0-9][^ ]*).*/\1/p') sapi=$("$bin" -v 2>/dev/null | head -1 | sed -nE 's/.*\(([a-z0-9-]+)\).*/\1/p') say "php_version=${ver}" say "sapi=${sapi:-$kind}" info=$("$bin" -i 2>/dev/null) for k in $INI_KEYS; do printf '%s\n' "$info" | awk -F' => ' -v k="$k" '$1==k {print "ini." k "=" $2; exit}' done if pgrep -f "$(basename "$bin")" >/dev/null 2>&1; then say "running=1"; else say "running=0"; fi ;; apache-module) say "sapi=apache2handler" ver=$(strings "$bin" 2>/dev/null | grep -m1 -E '^X-Powered-By: PHP/' | sed 's#X-Powered-By: PHP/##') [ -z "$ver" ] && ver=$(strings "$bin" 2>/dev/null | grep -m1 -E '^[78]\.[0-9]+\.[0-9]+' ) [ -n "$ver" ] && say "php_version=${ver}" ;; esac IFS=$'\t' read -r mgr name pver <<<"$(pkg_for "$bin")" if [ -n "${mgr:-}" ]; then say "pkg_manager=${mgr}"; say "pkg_name=${name}"; say "pkg_version=${pver}" fi } export P2S_KEYS="$INI_KEYS" seen="" add() { # de-duplicate by resolved path local real; real=$(readlink -f "$1" 2>/dev/null || echo "$1") case " $seen " in *" $real "*) return 1;; esac seen="$seen $real"; return 0 } found=0 # CLI binaries for b in $(command -v php 2>/dev/null) /usr/bin/php[0-9]* /usr/local/bin/php /usr/bin/php \ /opt/remi/php*/root/usr/bin/php /opt/cpanel/ea-php*/root/usr/bin/php /opt/plesk/php/*/bin/php /opt/alt/php*/usr/bin/php; do [ -x "$b" ] && [ -f "$b" ] && add "$b" && { emit_install "$b" cli; found=$((found+1)); } done # FPM binaries for b in /usr/sbin/php-fpm* /usr/sbin/php*-fpm /usr/local/sbin/php-fpm /opt/remi/php*/root/usr/sbin/php-fpm \ /opt/cpanel/ea-php*/root/usr/sbin/php-fpm /opt/plesk/php/*/sbin/php-fpm; do [ -x "$b" ] && [ -f "$b" ] && add "$b" && { emit_install "$b" fpm; found=$((found+1)); } done # LiteSpeed for b in /usr/local/lsws/lsphp*/bin/lsphp /usr/local/lsws/fcgi-bin/lsphp*; do [ -x "$b" ] && [ -f "$b" ] && add "$b" && { emit_install "$b" lsphp; found=$((found+1)); } done # Apache mod_php for m in /usr/lib/apache2/modules/libphp*.so /usr/lib64/httpd/modules/libphp*.so /usr/lib/httpd/modules/libphp*.so; do [ -f "$m" ] && add "$m" && { emit_install "$m" apache-module; found=$((found+1)); } done say "#installs_found=${found}" [ "$(id -u)" != 0 ] && say "#warning=Not run as root: other users' processes (php-fpm, apache) may be invisible, so usage is incomplete. Re-run with sudo." [ "$found" -eq 0 ] && say "#note=No PHP found in standard locations. If PHP runs in containers, use the Docker check." say "#end"