#!/usr/bin/env bash # php2shell-docker.sh — read-only PHP2Shell check for running containers (and optionally images) # https://scan.theadminstack.com/php2shell/ # # For every running container that has PHP, prints PHP_VERSION, the distro package (if PHP came # from apt/dnf/apk rather than the official php:* image) and the php.ini settings that matter. # Uses `docker exec` with read-only commands. Nothing is changed or sent anywhere. # # bash php2shell-docker.sh # running containers # bash php2shell-docker.sh --images # also local images that contain PHP (starts a throwaway # # container per image with --network none, removed after) # Works with podman too: DOCKER=podman bash php2shell-docker.sh set -u DOCKER="${DOCKER:-docker}" command -v "$DOCKER" >/dev/null 2>&1 || { echo "#error=$DOCKER not found"; exit 1; } # Runs inside the container (POSIX sh, busybox-safe) read -r -d '' INNER <<'SH' P=$(command -v php 2>/dev/null || command -v php83 2>/dev/null || command -v php82 2>/dev/null || command -v php84 2>/dev/null || command -v php85 2>/dev/null) [ -z "$P" ] && { echo "#nophp=1"; exit 0; } if [ -r /etc/os-release ]; then . /etc/os-release; echo "os_id=${ID:-unknown}"; echo "os_version=${VERSION_ID:-unknown}"; fi echo "[php]" echo "path=$P" V=$("$P" -n -r 'echo PHP_VERSION;' 2>/dev/null); echo "php_version=$V" if [ -n "${PHP_VERSION:-}" ]; then echo "runtime=docker"; echo "image_php_version=$PHP_VERSION"; fi echo "sapi=cli" "$P" -r 'foreach (["session.upload_progress.enabled","memory_limit","expose_php","fatal_error_backtraces","file_uploads","post_max_size"] as $k) { $v = ini_get($k); if ($v !== false) echo "ini.$k=$v\n"; }' 2>/dev/null RP=$(readlink -f "$P" 2>/dev/null || echo "$P") if command -v dpkg >/dev/null 2>&1; then N=$(dpkg -S "$RP" 2>/dev/null | head -1 | cut -d: -f1) [ -n "$N" ] && { echo "pkg_manager=dpkg"; echo "pkg_name=$N"; echo "pkg_version=$(dpkg-query -W -f='${Version}' "$N" 2>/dev/null)"; } elif command -v rpm >/dev/null 2>&1; then Q=$(rpm -qf --qf '%{NAME} %{VERSION}-%{RELEASE}' "$RP" 2>/dev/null) case "$Q" in *"not owned"*|"") ;; *) echo "pkg_manager=rpm"; echo "pkg_name=${Q%% *}"; echo "pkg_version=${Q#* }";; esac elif command -v apk >/dev/null 2>&1; then O=$(apk info -W "$RP" 2>/dev/null | sed -n 's/.* is owned by //p' | head -1) [ -n "$O" ] && { echo "pkg_manager=apk"; echo "pkg_name=$O"; } fi SH echo "#php2shell-check v2 docker" echo "#usage: running containers are reported in_use=container; --images entries are not running (in_use empty)" n=0 for id in $("$DOCKER" ps -q); do name=$("$DOCKER" inspect -f '{{.Name}}' "$id" | sed 's#^/##') image=$("$DOCKER" inspect -f '{{.Config.Image}}' "$id") res=$("$DOCKER" exec "$id" sh -c "$INNER" 2>/dev/null) || continue printf '%s' "$res" | grep -q '^#nophp=1' && continue [ -z "$res" ] && continue echo "[host]"; echo "hostname=container:${name}"; echo "image=${image}"; echo "usage_scan=2"; echo "elevated=1" printf '%s\n' "$res" | sed -n '/^os_/p' printf '%s\n' "$res" | sed -n '/^\[php\]/,$p' echo "in_use=container"; echo "usage_detail=Running container ${name}" n=$((n+1)) done if [ "${1:-}" = "--images" ]; then for img in $("$DOCKER" images --format '{{.Repository}}:{{.Tag}}' | grep -v '' | sort -u); do res=$("$DOCKER" run --rm --network none --entrypoint sh "$img" -c "$INNER" 2>/dev/null) || continue printf '%s' "$res" | grep -q '^#nophp=1' && continue [ -z "$res" ] && continue echo "[host]"; echo "hostname=image:${img}"; echo "image=${img}"; echo "usage_scan=2"; echo "elevated=1" printf '%s\n' "$res" | sed -n '/^os_/p' printf '%s\n' "$res" | sed -n '/^\[php\]/,$p' echo "in_use="; echo "usage_detail=Local image only: not running. Redeploys from it would be vulnerable if it is." n=$((n+1)) done fi echo "#containers_with_php=${n}" echo "#end"