# Why the php2shell scanners disagree with each other

By PHP Patch Assurance · Last updated: 2026-10-30

Within a day of the php2shell disclosure, more than a dozen scanners appeared — and run against the same site, they disagree: one says vulnerable, one says patched, a third cannot tell. They are not measuring the same thing. Fingerprinting reads a disclosed string; behavioral probing triggers the fault itself. The two methods fail in opposite directions.

## The disagreement is not noise

Within a day of the php2shell disclosure (CVE-2026-109618 / CVE-2026-109619, fixed in PHP 8.2.35, 8.3.36, 8.4.27 and 8.5.12 on 21 October 2026), more than a dozen scanners appeared. Run two of them against the same site and you can get two different answers: one says vulnerable, one says patched, a third says it cannot tell. They are not all measuring the same thing. The disagreement is not noise — it is the predictable result of two detection methods that fail in opposite directions.

## What a version fingerprint actually measures

A fingerprinting scanner reads the PHP version a site discloses — in `X-Powered-By` or `Server` headers, a generator tag, an exposed phpinfo page — and compares it with the fixed releases. That is all it measures: a string the server chose to publish, against a table.

That string misleads in at least four ways. First, on well-run sites it is absent. `expose_php = Off` is baseline hardening, so for a large share of the web the fingerprint has nothing to read. Second, the header you receive may describe the CDN edge in front of the site, not the origin running PHP. Third, distribution packages break the string's meaning: Debian, Ubuntu and RHEL backport security fixes without changing the upstream version number, so a server can report 8.3.20 and already contain the fix. A naive fingerprint cries wolf on exactly the hosts that are patched most conservatively. Fourth, banners are self-reported and can be stale, stripped by one proxy and added by another, or simply wrong.

None of this makes fingerprinting useless. It makes it a statement about disclosure, not about code — and it should be phrased that way.

## What a behavioral probe actually measures

A behavioral scanner tries to reproduce the fault: for php2shell, it sends crafted multipart requests designed to push PHP out of memory and watches the response — a memory-exhaustion error, an empty HTTP 500, a dead FPM worker. If the crash signature appears, the host is declared vulnerable. This answers a question the fingerprint cannot: is the fault reachable on this host, backports and all?

But the crash signature is not unique to the vulnerability. Searchlight Cyber's scanner — the most prominent behavioral tool — documents this in its own Limitations section: on patched PHP 8.4.27 with `memory_limit` set between 2M and 32M, and on patched 8.3.36 at 128M with a high `max_multipart_body_parts`, its probes produce the same empty HTTP 500 as on a vulnerable build, and it reports a false positive. A safe, post-patch out-of-memory error can be byte-identical to the vulnerable one. The same binary can flip verdicts with configuration alone. And when a site hides its version, the behavioral method cannot distinguish the safe post-patch OOM from the dangerous pre-patch one — again, their words, not ours.

There is also a cost the fingerprint never imposes: the probe is the trigger. On a genuinely vulnerable production host, forcing PHP out of memory is what the bug does; workers can die mid-request under real user traffic. That trade may be right for an authorized assessment in a maintenance window. It is a strange default for a tool anyone can point at anyone's site.

The honest exception is the local behavioral check, SamJUK's php2shell-check: it runs the same termination test against a throwaway PHP CLI process on your own host, sends no network requests at all, and for that reason answers the backport question correctly with none of the collateral risk. If you operate the server, that is the behavioral check to use.

## What we built instead: verdicts that describe the evidence

We build phpscan, a fingerprinting scanner, so we are not neutral. But building it forced a design rule we now think is the whole argument: never print a verdict the evidence does not support. A false "patched" is the worst output a security tool can produce, because it ends the incident. So phpscan's verdicts are scoped to what was actually observed:

- VULNERABLE — a disclosed version below the fixed release for its branch.
- POSSIBLY VULNERABLE — below the floor, but the build looks like a distribution package; backports mean this is a check signal, not proof.
- PATCHED — the disclosed version is at or above every advisory floor in the database. It means exactly that, not "secure".
- END OF LIFE — the branch gets no upstream fix; the remedy is a branch upgrade, not a patch.
- VERSION HIDDEN — PHP is present, the version is not disclosed, and patch status cannot be confirmed from outside. This is a final answer, not an error: it tells you the check has to move on-server (`php -v`, or a read-only inventory).
- NOT ASSESSED — the branch is newer than the advisory database, so the tool declines to judge it.
Two more positions follow from the same rule. phpscan never sends an exploit probe — not as an option, not behind a flag. It reads what a site already discloses, plus a check for a publicly exposed phpinfo page at a few well-known paths. And because a scanner that fetches arbitrary URLs is itself an SSRF tool waiting to be borrowed, it refuses loopback, private and link-local targets (including cloud metadata endpoints), pins the validated IP, and re-validates every redirect; those refusals are covered by its test suite.

Fingerprinting the runtime also stops at the runtime. Most PHP breaches arrive through dependencies, so the same evidence rule applies one level up: `phpscan audit composer.lock` checks locked packages against OSV, the FriendsOfPHP advisory database and the CISA KEV catalog — offline, in CI, with no dependence on what a server discloses.

Finally, every actionable verdict carries a `remediation` field naming the release that fixes it — and every audit finding names the exact upgrade. A verdict without a next step leaves the incident where it started: known, and unfixed.

## Ask what each scanner actually measured

Run the same site through every scanner you can find. Where they disagree, ask what each one actually measured. The tool that tells you "I can't tell from here" has told you something true — and something the other two may have guessed past.

## Frequently asked questions

### Why can two php2shell scanners disagree about the same site?

Because they measure different things. A fingerprinting scanner reads the PHP version a site discloses and compares it with the fixed releases. A behavioral scanner tries to reproduce the fault and watches for a crash signature. The two methods fail in opposite directions, so the same site can earn a different answer from each — and both answers can be honest reports of what was actually measured.

### Can a scanner report a patched site as vulnerable?

Yes — both methods can. A fingerprint cries wolf on distribution packages: Debian, Ubuntu and RHEL backport security fixes without changing the upstream version number, so a patched server can still report an old version. A behavioral probe can also misfire: Searchlight Cyber's scanner documents in its own Limitations section that on some patched builds, at certain memory settings, its probes produce the same empty HTTP 500 as on a vulnerable build.

### Does phpscan send exploit probes?

Never — not as an option, not behind a flag. phpscan reads what a site already discloses, plus a check for a publicly exposed phpinfo page at a few well-known paths. On a genuinely vulnerable production host, a behavioral probe is itself the trigger: forcing PHP out of memory is what the bug does, and workers can die mid-request under real user traffic.

### What if my site hides its PHP version?

Then the honest verdict is VERSION HIDDEN: PHP is present, the version is not disclosed, and patch status cannot be confirmed from outside. That is a final answer, not an error — it tells you the check has to move on-server, with php -v or a read-only inventory. A false "patched" would be worse: it ends the incident on evidence nobody collected.
