psf-php2shell-mitigate(8)

Script: /opt/psf-security-patches/php/php2shell-mitigate.sh. Tool page: PHP2Shell. Source. With the package installed: man psf-php2shell-mitigate.

PHP2Shell mitigation for PHP-FPM 7.0 through 8.5  (v5)
CVE-2026-109618 / CVE-2026-109619, and CVE-2026-115560 (session upload
progress, announced 23 Oct 2026, fix due in the next regular PHP release)

If this software is helpful, consider a donation to our 501c3 nonprofit:
https://privacysafe.foundation

This script was released ahead of patches for two 0day vulns in php.

Usage (as root):
  ./php2shell-mitigate.sh [command] [--php X.Y] [--force] [--ignore-env]

  check            environment report only; changes nothing, root optional
                   (exit 0 = ok, 1 = hard blocker, 2 = needs --ignore-env)
  apply (default)  drop-in + AppArmor profile, safe-load, per FPM version
  enforce          switch profile(s) to enforce; refuses if complain mode
                   logged unhandled accesses (override with --force)
  complain         switch profile(s) back to complain
  status           mitigation state and recent AppArmor events
  hunt             read-only: scan logs for signs of exploitation or the
                   memory-corruption crashes these bugs cause, plus the
                   request, source-address, proof-file and live process
                   indicators from the 24 October 2026 phpot honeypot
                   report (https://github.com/0x4D31/honeypot-data/blob/main/reports/php2shell/2026-10-24-confirmed-rce.md):
                   both request fingerprint forms, leak-sized
                   responses, TCP peers of flagged processes, auditd events
                   (see php/detect/) and nginx's "upstream sent too big
                   header" error. It says so when pre-script OOM lines carry
                   no client address, so repeats cannot be counted. Changes
                   nothing, root optional. Exit 0 = clean, 1 = findings.
  cleanup          remove the ini drop-in once patched (--force to skip check).
                   Keeps session.upload_progress off until the build is
                   also fixed for CVE-2026-115560 (--force drops that too)
  version          print the script version and exit
  remove           remove the AppArmor profile entirely: unload it, delete
                   the profile, local override and disable symlink, verify.
                   Leaves the ini drop-in (that's cleanup). No patch needed.

  --php X.Y        act on one version only (default: every installed
                   version in SUPPORTED_VERS)
  --ignore-env     proceed past soft environment blockers (live unpatched
                   mod_php, live sites only on skipped versions). Hard
                   blockers (non-Debian, no service manager, no AppArmor,
                   not root, no supported FPM) can't be overridden.

Every command first checks, in order: OS (Debian, Ubuntu, Trisquel and
popular derivatives), PHP-FPM versions and their package source, web
servers (Apache/nginx and what PHP they use), then system (root, init
system, container, AppArmor, tools). It reports, then refuses to start only
if it can't work safely. Derivatives and non-systemd init (sysvinit/OpenRC)
are supported. Each version's patch state is reported from three signals:
upstream version, known distro backport builds, and the package changelog.

Env overrides:
  PHP_VER=X.Y                        same as --php X.Y (an exported PHP_VER therefore
                                     limits the run to that version)
  SUPPORTED_VERS="7.0 ... 8.5"       versions this script will touch
  FIXED_BUILDS="7.4.33-1+deb11u9"    extra fixed distro builds (backports)
  PATCHED_X_Y=<version>              first fixed upstream release for branch
                                     X.Y, e.g. PATCHED_8_4=8.4.27. Defaults:
                                     8.2.35 8.3.36 8.4.27 8.5.12 (21 Oct 2026).
                                     7.0-8.1 get no upstream fix: only a
                                     distro/PPA changelog listing both CVEs
                                     counts as patched.
  PATCHED_115560_X_Y=<version>       first release fixing CVE-2026-115560 for
                                     branch X.Y, e.g. PATCHED_115560_8_4=8.4.28.
                                     No defaults yet: until PHP ships it, only a
                                     changelog naming the CVE counts.
  GUARD_HINT=0                       don't point to the third-party extension
                                     for PHP this script can't cover
  WEB_ROOTS="/var/www /srv/www"      read-only docroots; hunt also looks here
                                     for proof-style files
  HUNT_IPS="15.204.59.70 162.35.99.94"
                                     source addresses reported in confirmed
                                     exploitation (2026-10-24); hunt flags
                                     them in web logs. Aging indicator: a hit
                                     means the address touched you, not what
                                     it did. HUNT_IPS="" turns it off
  HUNT_ATTEMPT_IPS="104.28.208.84 144.48.4.242"
                                     addresses the same report names as
                                     sending exploit-shaped requests with no
                                     shell seen: attempt indicators, not
                                     confirmed RCE. Shared or proxy addresses
                                     are possible. HUNT_ATTEMPT_IPS="" off
  COOKIE_MAX=4096                    hunt flags a PHPSESSID value this many
                                     characters long in a log that records
                                     response cookies (0 = off). The leak in
                                     the phpot report was 819,200 bytes decoded
  RESP_BYTES_MAX=500000              hunt flags a POST whose logged response
                                     is this many bytes (0 = off). Only logs
                                     that count headers can show it: Apache
                                     %O, the Debian/Ubuntu default
  PROOF_SINCE=2026-10-01             hunt's proof-file scan ignores older files
  HUNT_PROC_ALLOW='rotatelogs|cronolog|logger'
                                     regex on the command line: hunt's live
                                     process check ignores matches (Apache
                                     piped-log helpers are normal)
  WRITABLE_PATHS="..."               rwk paths (uploads, cache)

Debian/Ubuntu/Trisquel layout (distro packages, ondrej/sury PPA). Each version gets
its own drop-in, profile, local override and backups; a failure on one
version doesn't stop the others.
     PSF_LOCK_WAIT    seconds a run that changes the host waits for another one that is already
                      changing it (default 30), then stops with exit 2; the lock is STATE_DIR/.lock

The Security Patches software and this documentation are dedicated to the public domain under CC0 1.0 Universal. Other pages on privacysafe.app/docs are CC BY-SA 4.0 unless the page says otherwise.

The PrivacySafe Ecosystem

Shop at Ivy Cyber

Ivy Cyber offers PrivacySafe products and services in the USA and has partnerships with 3NSoft in Canada and Kuip Limited in the European Union. Enterprise services and consulting are available at ivycyber.com

PrivacySafe Foundation is a 501(c)(3) nonprofit public charity that supports the development of the PrivacySafe applications, along with the components they rely upon: 3NWeb protocols, filesystem, documentation, and reference implementations.

PrivacySafe Foundation

Donations to PrivacySafe Foundation support education, research, documentation, and public code. Purchases from Ivy Cyber support PrivacySafe products, services, hosting, and technical support. Together, we're building a better web.