Skip to content
emirwtzPublic

About

A lightweight Python CLI to assess PHP2Shell (CVE-2026-109618 & CVE-2026-109619) exposure via version checks.

Topics

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

PHP2Shell Audit

Check PHP version evidence. Prioritize updates. Verify the running deployment.

A small Python command-line tool for assessing potential exposure to PHP2Shell (CVE-2026-109618 and CVE-2026-109619). It checks an HTTP endpoint, a local PHP executable, or a version supplied offline, and produces readable or JSON output.

This is a version-based assessment, not an exploit detector. It does not send exploit payloads, attempt command execution, or determine whether a server has already been compromised. Use it on systems you own or are authorized to assess.

Rules last reviewed: October 23, 2026. The rule set is embedded and does not update automatically. Review new vendor advisories before relying on old results.

Contents

Understanding PHP2Shell

PHP2Shell is the public name associated with two vulnerabilities in PHP's runtime. PHP announced fixes on October 21, 2026. Its advisory identifies web-serving installations as at risk, including Apache with mod_php, PHP-FPM, CGI, FrankenPHP, and LiteSpeed. Actual attack conditions depend on the runtime and configuration; a version match alone cannot establish exploitability. PHP security announcement.

CVE-2026-109618: out-of-memory corruption

An allocation failure can interrupt work while PHP's internal request data is incompletely initialized. Cleanup can subsequently use that invalid state, causing memory corruption and potentially remote code execution. PHP's advisory describes a demonstrated mod_php path before application code runs. Other runtimes have different failure behavior, so their exposure cannot be reduced to the same assumption. The fix changes how the engine handles memory exhaustion. PHP's OOM advisory.

CVE-2026-109619: stale global state

A long-lived PHP worker can retain references to memory belonging to a finished request. If that memory is freed without all references being reset, a later request handled by the same worker may access stale data before initialization replaces it. This use-after-free condition can support remote code execution. The affected internal state and reachable operations determine the specific conditions. Hiding PHP warnings does not remove the underlying problem. PHP's stale-state advisory.

Why this matters operationally

Successful exploitation can give an attacker the permissions of the PHP process, potentially exposing application secrets, altering content, or disrupting the service. It does not automatically mean root access. AWS describes code execution, access to process-readable data, and denial of service as possible outcomes. AWS security bulletin.

Treat the PHP runtime as a deployment dependency: an application or CMS update does not establish that the underlying interpreter was updated. Record the runtime package and the actual worker deployment separately.

Affected and fixed versions

The following table describes upstream release thresholds for both CVEs:

PHP branch Below the fixed threshold First fixed upstream release
8.2 8.2.0–8.2.34 8.2.35
8.3 8.3.0–8.3.35 8.3.36
8.4 8.4.0–8.4.26 8.4.27
8.5 8.5.0–8.5.11 8.5.12

Source: PHP security announcement. Use a supported vendor release containing the fixes; these are minimum thresholds, not an instruction to downgrade a newer installation.

PHP 8.1 and older are upstream end-of-life. Vendors may supply backported fixes; PHP also links limited source backports for 7.4, 8.0, and 8.1. A custom build or a higher legacy version number is not enough to verify one of those fixes. PHP's legacy-version guidance.

The checker retains the supplied baseline's historical cutoffs for PHP 5.4–5.6, 7.0–7.4, 8.0, and 8.1. A version at or below one of those cutoffs is flagged; a higher legacy patch returns UNKNOWN, never an inferred OK. Branches absent from the rules also return UNKNOWN.

Distribution backports need package evidence. A distribution can ship a fix while retaining an older upstream-looking version. Conversely, a custom version string does not prove a fix. Check the complete package release and your vendor's advisory. This tool has no package-vendor database.

Features

  • Python 3.10+ with no third-party runtime dependencies.
  • HTTP HEAD inspection, with GET fallback only for status 405 or 501.
  • Local PHP inspection through php -v, without invoking a command shell.
  • Offline assessment of a supplied version with --php-version.
  • HTTPS certificate verification using Python's default trust configuration.
  • No redirect following, body analysis, directory discovery, or target crawling.
  • Separate evidence for all detected PHP versions and HTTP attempts.
  • Stable exit codes, structured JSON, and a recommended next step.
  • Conservative handling of unknown branches, prereleases, and custom suffixes.

Installation

Download or clone this repository, then open a terminal in its root directory. For direct use, nothing needs to be installed beyond Python:

python --version
python phpcheck.py --help
python -m php2shell_audit --version

Use python3 on systems where that is the Python 3 command. Windows installations may use py -3. Local mode additionally requires a trusted PHP executable; URL and offline modes do not require PHP.

Optional installation into a virtual environment adds a console command:

python -m venv .venv
# Linux/macOS:
source .venv/bin/activate
# Windows PowerShell instead:
# .\.venv\Scripts\Activate.ps1

python -m pip install .
php2shell-audit --help

Installation may download the setuptools build backend. Running directly from the repository needs no package installation or network access.

Usage

Inspect a website

python phpcheck.py https://example.com
python phpcheck.py https://example.com --timeout 5 --json

Replace example.com with an authorized endpoint. Pick an ordinary, read-only page: even a normal GET or HEAD can execute application logic or create logs. The checker requests only the supplied URL.

Inspect the local CLI runtime

python phpcheck.py --local
python phpcheck.py --local --php /usr/bin/php8.4
python phpcheck.py --local --php "C:\PHP\php.exe" --json

--php accepts an executable path, not a shell command or extra arguments. Only select an executable you trust. The checker invokes it with -v; it does not change the PHP configuration or suppress normal startup processing.

Assess an inventory version without making requests

python phpcheck.py --php-version 8.4.26
python phpcheck.py --php-version 8.4.27 --json
python phpcheck.py --php-version 8.4.27RC1

These return VULNERABLE, OK, and UNKNOWN, respectively. Offline mode checks only the supplied string and is useful when reviewing inventory records. It does not confirm that the recorded version is deployed.

Command reference

Argument Purpose
URL Inspect one absolute HTTP or HTTPS URL
--local Inspect a PHP executable on this machine
--php PATH Choose the executable; valid only with --local
--php-version VERSION Assess a version offline
--timeout SECONDS Default 10; must be finite, greater than 0, and no more than 60
--json Write one JSON report to standard output
--version Display the checker version
-h, --help Display help

Choose exactly one URL, --local, or --php-version. Invalid combinations are rejected before a request or subprocess starts.

How checks work

  1. Validate the selected mode and its arguments.
  2. Collect version evidence from HTTP headers, local stdout, or the supplied string.
  3. Assess every distinct version against the embedded branch rules.
  4. Select the most concerning result: VULNERABLE takes priority over UNKNOWN, which takes priority over OK.
  5. Print the report and return its exit code.

In URL mode the tool reads only Server and X-Powered-By. Duplicate header fields are retained. A rejected HEAD with status 405 or 501 triggers one GET; evidence from both responses remains in the report. A later failure cannot erase an earlier vulnerable version. Without vulnerable evidence, incomplete collection produces UNKNOWN.

Redirect responses are inspected as received; their destination is never visited. HTTP errors such as 403 can still contain version evidence, so an HTTP error status is not itself a vulnerability verdict. Missing PHP headers mean UNKNOWN. Response bodies are closed without being read for analysis, although the server may already have transmitted some body bytes.

Connections go directly to the target; environment proxy settings are not used. The timeout applies to blocking socket operations, not an end-to-end deadline. DNS resolution may exceed it, and a fallback request gets its own operations. In local mode it limits the PHP subprocess. Use an external job deadline if your automation requires a strict total runtime limit.

Interpreting results

Result Exit code Meaning Next action
OK 0 A plain version meets the fixed threshold in a reviewed branch Verify the actual serving workers and package provenance
VULNERABLE 1 Version evidence falls in a flagged upstream range Confirm vendor patch status and prioritize remediation
Argument error 2 Invalid input or incompatible options Fix the invocation
UNKNOWN 3 Evidence is absent, incomplete, or cannot establish a fixed version Investigate locally or with the provider

An interrupted run returns 130 when interrupted during checking/reporting.

OK means a version comparison passed. It does not certify the host as secure. VULNERABLE means a version matches the rules, not that exploitation succeeded. UNKNOWN must never be converted into a passing security check.

Examples of the conservative rules:

Evidence Result Why
8.3.35 VULNERABLE Below 8.3.36
8.3.36 OK Meets the reviewed upstream threshold
8.3.37 OK Higher plain patch in the same reviewed branch
8.3.36RC1 UNKNOWN Prerelease at the fixed boundary
8.3.36-custom UNKNOWN Custom build requires verification
8.3.35-vendor1 VULNERABLE Older base; a backport may make this a false positive
7.4.34 UNKNOWN No inferred fixed release for an EOL branch
8.6.0 UNKNOWN Branch absent from the reviewed rules

A header advertising a higher plain patch is treated as version evidence; the checker does not query a release catalog to prove that the release exists.

JSON and automation

python phpcheck.py --php-version 8.4.26 --json > scan-result.json

Selected fields from that report (the full report contains additional fields):

{
  "schema_version": 1,
  "patch_status": "vulnerable",
  "php_version": "8.4.26",
  "exit_code": 1,
  "rules_as_of": "2026-10-23",
  "cves": ["CVE-2026-109618", "CVE-2026-109619"],
  "findings": [{
    "version": "8.4.26",
    "status": "vulnerable",
    "reason": "Below the upstream fix; a vendor backport requires verification.",
    "minimum_fixed_version": "8.4.27",
    "upstream_eol": false
  }]
}

Additional fields include the UTC check time, mode, target, collection error, HTTP request evidence, warnings, next step, and advisory URL. The original fields patch_status, php_version, reason, and exit_code remain available for simple integrations. Unknown command arguments produce a usage error on stderr, not a JSON report, even with --json.

Capture exit codes immediately. This POSIX-shell example also works under set -e because the check appears in an if condition:

if python phpcheck.py --local --json > scan-result.json; then
  code=0
else
  code=$?
fi
case "$code" in
  0) echo "Version threshold passed; verify the serving deployment." ;;
  1) echo "Flagged version: remediation review required." ;;
  3) echo "Inconclusive: manual verification required." ;;
  *) echo "Check failed with exit code $code." ;;
esac
exit "$code"

In PowerShell, capture $LASTEXITCODE after running the command. Do not use || true to turn unknown or failed checks into successful security gates. Reports contain the supplied URL, including its query, and selected response headers; avoid secret-bearing URLs and handle reports as deployment information.

What to do after a vulnerable result

Use this operational checklist to turn a version finding into a verified update.

1. Identify the actual deployment

Record the hostname, service owner, environment, runtime integration, complete package version, and deployment source. Check all relevant containers, replicas, virtual hosts, pools, and scheduled jobs. A single URL may reach only one member of a load-balanced service.

Compare the result with the runtime handling web requests. php -v checks the CLI binary selected by your shell or --php; it does not inspect an Apache module or discover PHP-FPM pools. Use administrative tooling or hosting support to identify those. Avoid publishing a diagnostic phpinfo() page to the Internet.

2. Establish whether a vendor fix is already present

Match the full installed package release to the distribution or hosting provider's advisory for both CVEs. Save the advisory reference and package evidence. Do not dismiss a finding merely because the version has a distribution suffix, and do not remove a verified vendor patch just to make a banner comparison pass.

If a verified backport remains flagged, document the evidence in your own exception process with an owner and review date. This tool intentionally does not offer an override that silently changes such findings to OK.

3. Update using the deployment's supported channel

Use the applicable package manager, approved image, hosting control panel, or source-build procedure. Confirm that the selected release includes both fixes. For containerized services, update the source image and redeploy every affected replica; changing a running container alone leaves the next deployment exposed.

Plan a maintained branch for legacy applications. Keep backups and a rollback procedure, but ensure rollback will not silently restore the vulnerable runtime. Test extensions and application compatibility in the same deployment layout.

4. Replace or restart running workers

An updated file on disk is not proof that old workers stopped using the previous runtime. Apply the service-specific restart or replacement procedure and verify all affected instances. Check deployment status, process start times, image identifiers, or package records through your administrative tooling.

5. Recheck and record completion

Rerun the appropriate local or offline assessment using verified runtime data. Optionally compare the external banner as supporting evidence. Smoke-test the application, inspect error rates, and retain the package version, check time, change record, and evidence that all workers were replaced. Hidden headers may still produce UNKNOWN after a successful patch; package evidence matters more.

6. Review whether incident response is needed

Patching resolves a software flaw; it cannot establish whether an earlier intrusion occurred. Review the exposure period and available telemetry. Escalate unexpected activity to your security or hosting team.

When patching cannot happen immediately

Reduce access to the affected service and arrange an emergency update with the owner or provider. If you cannot adequately restrict exposure, consider a maintenance window or temporarily withdrawing the service.

For the OOM issue, PHP describes administrative memory-limit changes and frontend request constraints as partial measures. Disabling PHP's memory limit can move the availability risk to the host; allocation failures remain relevant. Do not apply that change without the accompanying resource-isolation guidance. OOM mitigation details.

For stale state, PHP documents disabling upload-progress tracking and, on PHP 8.5, fatal-error backtraces for specific paths. These require configuration that takes effect at the right initialization stage and worker restarts. They do not cover every variant. Review the full applicability before changing production settings. Stale-state mitigation details.

Provider WAF protections can add a temporary layer; verify that the relevant rules apply to your traffic and that the origin cannot bypass that layer. Cloudflare describes its PHP2Shell protection deployment in its provider advisory. Keep the runtime update as the completion requirement. A mitigation-only change does not alter this checker's version result.

If compromise is suspected

The following are investigation leads, not PHP2Shell-specific proof:

  • Unexpected files or modifications under the application/web root.
  • New scheduled tasks, accounts, processes, or unexplained outbound connections.
  • Unusual PHP worker failures or request/error patterns around the exposure period.
  • Unexpected access to credentials, storage, databases, or administrative interfaces.

Involve your incident-response team or hosting provider. Isolate affected workloads as appropriate, preserve relevant logs and snapshots, and avoid destroying evidence during cleanup. Determine the scope before returning the service to normal operation. If credentials may have been exposed, revoke or rotate them from a trusted system and review their downstream use. A clean rebuild from verified sources may be necessary; installing a patch does not remove an attacker's persistence.

Limitations and troubleshooting

No PHP header is visible. This is common. It means UNKNOWN, not proof that the site does not use PHP. Use server-side inventory or ask the provider.

The CLI reports a fixed version but the site still looks old. Check separate PHP installations, Apache modules, FPM pools, caches, proxies, and replicas. The checker cannot map a public hostname to a specific runtime process.

The URL redirects. Redirects are deliberately not followed. Verify that the destination is within your authorized scope, then check its URL explicitly.

HTTPS fails. Fix the certificate, hostname, or trust-store problem. There is no option to disable certificate verification. The failure returns UNKNOWN.

The network requires a proxy. This version makes direct connections and has no proxy option. Run from a permitted network path or use verified local/offline inventory instead.

A backported package is flagged. Use the vendor's complete package release and advisory to resolve the finding. This is an expected version-only limitation.

A development or custom build is inconclusive. Suffixes at or above a fixed threshold are not treated as stable-release proof. Verify the build's contents. Below-threshold versions remain flagged even when they have a suffix.

Does this scan every PHP installation? No. Each invocation inspects one URL, one executable, or one version. There is no range scanner, authenticated session, filesystem inventory, or automatic discovery.

Does it check configuration or detect a web shell? No. Neither absence of a payload response nor an OK result establishes that the system is uncompromised.

Development

phpcheck.py                 Convenience entry point
php2shell_audit/
  cli.py                   Arguments, reports, and exit codes
  probes.py                HTTP and local evidence collection
  rules.py                 Version rules and conservative aggregation
tests/                     Offline and loopback-only regression tests
.github/workflows/tests.yml Cross-platform test workflow
NOTICE                     Upstream attribution and modification summary

Run the standard-library test suite from the repository root:

python -m unittest discover -s tests -v

Tests use local HTTP fixtures and mocked PHP subprocess results; they do not need a PHP installation or public targets. They cover branch boundaries, malformed and decorated versions, mixed banners, fallback failures, redirect behavior, invalid arguments, JSON/exit-code consistency, and local-process errors.

When changing rules, cite the authoritative advisory, update RULES_AS_OF, and add boundary tests. Do not assume a new branch is fixed just because its version number is larger. See CONTRIBUTING.md for contribution guidance.

Attribution and license

This project is a modified Python adaptation of OpenAI's PHP version checker. It adds modular collection and classification, offline input, conservative legacy/custom-build handling, expanded reports, and operational documentation. The original checker and vulnerability research are credited to their respective authors. See NOTICE for the change summary.

PHP's maintainers and the participating research teams coordinated the original disclosure and fixes; see the PHP Foundation's release account and OpenAI's disclosure account. This repository makes no affiliation or endorsement claim.

Distributed under the Apache License 2.0.

About

A lightweight Python CLI to assess PHP2Shell (CVE-2026-109618 & CVE-2026-109619) exposure via version checks.

Topics

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages