#!/usr/bin/env bash
#
# php2shell-check.sh
#
# Defensive check for CVE-2026-109618 / CVE-2026-109619 ("PHP2Shell").
# This script does NOT attempt to exploit anything. It only:
#   1. Reports the PHP version(s) running on this host and whether they
#      are patched.
#   2. Looks for common signs of a web shell / compromise in your web
#      root (new or recently modified PHP files, suspicious functions).
#   3. Scans logs for process-spawning behaviour from your web server
#      user, which is what a successful exploit normally leaves behind.
#
# Run it on the server itself (via SSH), not against a remote URL.
# Read-only: it does not delete, quarantine, or modify anything — it
# only prints findings for you to review.
#
# Usage:
#   chmod +x php2shell-check.sh
#   ./php2shell-check.sh [web_root] [since_date]
#
#   web_root    Path to scan for suspicious PHP files. Default: /var/www
#   since_date  Only flag files modified on/after this date (YYYY-MM-DD).
#               Default: 2026-10-20 (the day PHP2Shell leaked).
#
# Fixed PHP versions: 8.5.12, 8.4.27, 8.3.36, 8.2.35
# PHP 8.1 and older: no official fix — upgrade or get a vendor backport.

set -uo pipefail

WEB_ROOT="${1:-/var/www}"
SINCE_DATE="${2:-2026-10-20}"

RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
BOLD='\033[1m'
NC='\033[0m'

ok()   { echo -e "${GREEN}[OK]${NC}   $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
bad()  { echo -e "${RED}[FLAG]${NC} $*"; }
info() { echo -e "${BLUE}[i]${NC}    $*"; }
hdr()  { echo; echo -e "${BOLD}== $* ==${NC}"; }

FLAGS=0

# ---------------------------------------------------------------------
hdr "1. PHP version check"
# ---------------------------------------------------------------------

version_is_patched() {
  # args: major.minor patch  -> 0 = patched, 1 = vulnerable, 2 = unknown branch
  local branch="$1" patch="$2"
  case "$branch" in
    8.5) [ "$patch" -ge 12 ] && return 0 || return 1 ;;
    8.4) [ "$patch" -ge 27 ] && return 0 || return 1 ;;
    8.3) [ "$patch" -ge 36 ] && return 0 || return 1 ;;
    8.2) [ "$patch" -ge 35 ] && return 0 || return 1 ;;
    *)   return 2 ;;
  esac
}

check_binary() {
  local bin="$1"
  command -v "$bin" >/dev/null 2>&1 || return
  local verout ver branch patch
  verout=$("$bin" -v 2>/dev/null | head -1)
  ver=$(echo "$verout" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
  [ -z "$ver" ] && { warn "$bin: could not parse version from: $verout"; return; }
  branch="${ver%.*}"
  patch="${ver##*.}"
  if version_is_patched "$branch" "$patch"; then
    ok "$bin: PHP $ver — patched"
  else
    local rc=$?
    if [ "$rc" = "2" ]; then
      warn "$bin: PHP $ver — branch is end-of-life (no official fix). Upgrade to 8.4 or 8.5."
      FLAGS=$((FLAGS+1))
    else
      bad "$bin: PHP $ver — VULNERABLE to PHP2Shell, update now"
      FLAGS=$((FLAGS+1))
    fi
  fi
}

for b in php php8.1 php8.2 php8.3 php8.4 php8.5 php-cgi; do
  check_binary "$b"
done

# FPM pools, since php -v can differ from what FPM actually runs
hdr "1b. PHP-FPM pool versions"
FOUND_FPM=0
for fpm in /usr/sbin/php-fpm* /usr/sbin/php*-fpm; do
  [ -x "$fpm" ] || continue
  FOUND_FPM=1
  verout=$("$fpm" -v 2>/dev/null | head -1)
  ver=$(echo "$verout" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
  [ -z "$ver" ] && continue
  branch="${ver%.*}"; patch="${ver##*.}"
  if version_is_patched "$branch" "$patch"; then
    ok "$(basename "$fpm"): PHP $ver — patched"
  else
    bad "$(basename "$fpm"): PHP $ver — VULNERABLE, update and restart this pool"
    FLAGS=$((FLAGS+1))
  fi
done
[ "$FOUND_FPM" = 0 ] && info "No php-fpm binaries found in /usr/sbin (not using FPM, or installed elsewhere)."

# Warn if any running process still maps the old (deleted) PHP binary/library
hdr "1c. Processes still running an old (pre-patch) PHP binary"
if command -v grep >/dev/null 2>&1 && [ -d /proc ]; then
  DELETED=$(sudo -n grep -lE ' /(usr|lib|opt).*php.*\(deleted\)$' /proc/[0-9]*/maps 2>/dev/null)
  if [ -n "$DELETED" ]; then
    bad "Some processes still map an old/deleted PHP binary or library — restart PHP-FPM / Apache / FrankenPHP:"
    echo "$DELETED" | sed 's/^/        /'
    FLAGS=$((FLAGS+1))
  else
    ok "No processes found mapping a deleted PHP binary (or check needs sudo — rerun as root to be sure)."
  fi
fi

# ---------------------------------------------------------------------
hdr "2. Recently modified/created PHP files under: $WEB_ROOT"
# ---------------------------------------------------------------------
if [ ! -d "$WEB_ROOT" ]; then
  warn "Path $WEB_ROOT does not exist — pass your actual web root as the first argument."
else
  info "Flagging .php files modified on/after $SINCE_DATE (adjust with: $0 /path YYYY-MM-DD)"
  RECENT=$(find "$WEB_ROOT" -type f -name '*.php' -newermt "$SINCE_DATE" 2>/dev/null)
  if [ -z "$RECENT" ]; then
    ok "No .php files modified since $SINCE_DATE."
  else
    COUNT=$(echo "$RECENT" | wc -l)
    warn "$COUNT .php file(s) modified since $SINCE_DATE. Review each — this is common but not proof of compromise:"
    echo "$RECENT" | sed 's/^/        /' | head -50
    [ "$COUNT" -gt 50 ] && info "(showing first 50 of $COUNT)"
    FLAGS=$((FLAGS+1))
  fi

  echo
  info "Checking for PHP files inside upload directories (should normally contain none):"
  UPLOAD_PHP=$(find "$WEB_ROOT" -type d \( -iname 'uploads' -o -iname 'upload' -o -iname 'files' \) \
               -exec find {} -type f -name '*.php' \; 2>/dev/null)
  if [ -z "$UPLOAD_PHP" ]; then
    ok "No PHP files found inside upload directories."
  else
    bad "PHP file(s) found inside an uploads directory — review immediately:"
    echo "$UPLOAD_PHP" | sed 's/^/        /'
    FLAGS=$((FLAGS+1))
  fi

  echo
  info "Scanning for common web-shell patterns in PHP files (heuristic, expect some false positives):"
  SUSPECT=$(grep -rlE 'eval\s*\(\s*(base64_decode|gzinflate|str_rot13|\$_(POST|GET|REQUEST))|assert\s*\(\s*\$_(POST|GET|REQUEST)|shell_exec\s*\(\s*\$_(POST|GET|REQUEST)|passthru\s*\(\s*\$_(POST|GET|REQUEST)' \
          --include='*.php' "$WEB_ROOT" 2>/dev/null)
  if [ -z "$SUSPECT" ]; then
    ok "No obvious web-shell patterns found."
  else
    bad "Files matching common web-shell patterns — review these by hand, do not trust automatically:"
    echo "$SUSPECT" | sed 's/^/        /' | head -30
    FLAGS=$((FLAGS+1))
  fi
fi

# ---------------------------------------------------------------------
hdr "3. Web server / PHP-FPM user spawning shells or tools"
# ---------------------------------------------------------------------
WEB_USERS="www-data|apache|httpd|nginx|nobody"
info "Checking process list for $WEB_USERS spawning shells, interpreters, or download tools:"
PS_HITS=$(ps -eo user,pid,ppid,cmd 2>/dev/null | grep -E "^($WEB_USERS)" | \
          grep -E '\b(sh|bash|dash|zsh|python[0-9.]*|perl|nc|ncat|curl|wget|certutil)\b')
if [ -z "$PS_HITS" ]; then
  ok "No current processes match (this only sees what's running right now, not history)."
else
  bad "Web-server-user process(es) running a shell/interpreter/downloader — investigate now:"
  echo "$PS_HITS" | sed 's/^/        /'
  FLAGS=$((FLAGS+1))
fi

echo
info "Checking available logs for the same pattern (last 7 days, where readable):"
for log in /var/log/auth.log /var/log/secure /var/log/syslog; do
  [ -r "$log" ] || continue
  HITS=$(grep -E "($WEB_USERS).*\b(sh|bash|nc|wget|curl)\b" "$log" 2>/dev/null | tail -20)
  if [ -n "$HITS" ]; then
    warn "Possible matches in $log (review manually, log formats vary):"
    echo "$HITS" | sed 's/^/        /'
    FLAGS=$((FLAGS+1))
  fi
done

# ---------------------------------------------------------------------
hdr "4. PHP error logs — out-of-memory / fatal errors since $SINCE_DATE"
# ---------------------------------------------------------------------
for log in /var/log/php*-fpm.log /var/log/php*/error.log /var/log/apache2/error.log /var/log/httpd/error_log; do
  [ -r "$log" ] 2>/dev/null || continue
  OOM_HITS=$(grep -iE 'allowed memory size|out of memory' "$log" 2>/dev/null | tail -20)
  if [ -n "$OOM_HITS" ]; then
    warn "Out-of-memory entries found in $log — one of the two exploit paths involves OOM, review timing:"
    echo "$OOM_HITS" | sed 's/^/        /'
  fi
done

# ---------------------------------------------------------------------
hdr "Summary"
# ---------------------------------------------------------------------
if [ "$FLAGS" -eq 0 ]; then
  ok "No PHP2Shell indicators found by this script. This is NOT a guarantee — it's a quick triage pass."
  info "Still patch any unpatched PHP version shown above regardless of these results."
else
  bad "$FLAGS item(s) need your review. None of these confirm compromise on their own —"
  echo "        check them by hand, and get incident-response help if you find a real web shell."
fi

echo
info "This script only checks for symptoms. The only real fix is upgrading PHP to"
info "8.5.12 / 8.4.27 / 8.3.36 / 8.2.35 (or later) and restarting every PHP process."
