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.
- Understanding PHP2Shell
- Affected and fixed versions
- Features
- Installation
- Usage
- How checks work
- Interpreting results
- JSON and automation
- What to do after a vulnerable result
- When patching cannot happen immediately
- If compromise is suspected
- Limitations and troubleshooting
- Development
- Attribution and license
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.
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.
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.
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.
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.
- Python 3.10+ with no third-party runtime dependencies.
- HTTP
HEADinspection, withGETfallback 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.
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 --versionUse 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 --helpInstallation may download the setuptools build backend. Running directly from the repository needs no package installation or network access.
python phpcheck.py https://example.com
python phpcheck.py https://example.com --timeout 5 --jsonReplace 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.
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.
python phpcheck.py --php-version 8.4.26
python phpcheck.py --php-version 8.4.27 --json
python phpcheck.py --php-version 8.4.27RC1These 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.
| 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.
- Validate the selected mode and its arguments.
- Collect version evidence from HTTP headers, local stdout, or the supplied string.
- Assess every distinct version against the embedded branch rules.
- Select the most concerning result:
VULNERABLEtakes priority overUNKNOWN, which takes priority overOK. - 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.
| 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.
python phpcheck.py --php-version 8.4.26 --json > scan-result.jsonSelected 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.
Use this operational checklist to turn a version finding into a verified update.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 -vTests 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.
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.