Quick Answer: The php2shell scanner is Assetnote's open-source command-line tool for detecting CVE-2026-109618 and CVE-2026-109619 in PHP applications. It sends a small multipart control request, then up to three crafted probes using deeply nested array keys. A host is flagged when the probe triggers a PHP memory-exhaustion error, a differential empty HTTP 500, a PHP-FPM worker failure, or an unpatched PHP version banner.
Most vulnerability write-ups tell you what the bug is. Fewer tell you how to test your own fleet without guessing. The php2shell scanner, published on GitHub by Assetnote, is one of the more useful tools for that job, and it's worth understanding how it works before you point it at production.
How the php2shell Scanner Decides a Host Is Vulnerable
The scanner doesn't try to execute anything on the target. It sends a minimal multipart control request first, then up to three crafted multipart probes. The probes use PHP array keys nested 62 levels deep, with payload sizes stepping from 1 MiB up to 20 MiB. That size progression is what lets the tool separate a vulnerable response from a normal rejection.
A host is reported vulnerable only when one of four signals appears:
- A PHP memory-exhaustion error in the response
- A differential empty HTTP 500, meaning the control request behaves normally but the probe produces an empty 500
- A PHP-FPM worker failure
- An advertised PHP version known to be unpatched
Here's what matters most: the scanner is deliberately strict about what it ignores. Initial 5xx responses, common request-rejection statuses, and nonempty HTTP 500 responses without the PHP fatal-error signature are not treated as crash signals. That's a sensible choice. A generic 500 from a misconfigured WAF or a broken Nginx upstream tells you nothing about PHP's parser, and counting it would flood your results with noise.
The original discovery of php2shell is credited to the Calif team, whose write-up at calif.io describes the underlying behavior. Assetnote's scanner builds on that logic using detection rules from its internal tooling.
Choosing the Right SAPI Mode: mod_php, PHP-FPM, or Auto
The scanner's --sapi flag controls which verdict rules apply. This matters more than most people expect, because the same crash looks different depending on how PHP is served.
| SAPI Mode | Verdict Rules Applied | Best Used When | Main Trade-off |
|---|---|---|---|
auto (default) | Both mod_php and PHP-FPM rules | You don't know the server stack | Broadest coverage, slightly higher chance of ambiguous verdicts |
mod_php | mod_php memory-exhaustion and empty-500 rules | Apache with mod_php confirmed | Misses FPM worker-failure signals |
fpm | PHP-FPM worker-failure rules only | Nginx or Apache front-ending PHP-FPM | Ignores mod_php signatures entirely |
My recommendation: start with auto. Most inventories don't record the SAPI accurately, and the default mode gives you the widest net. Once you have results, re-run any suspicious hosts with --sapi fpm or --sapi mod_php to confirm which rule fired. If a host only triggers under one mode, that tells you something useful about its real architecture.
Running the Scanner Against Single Hosts and Lists
Installation is straightforward. You need Python 3.9 or newer, plus the h11 and tqdm packages:
pip install -r requirements.txt
python3 scanner.py -u https://example.com
For fleet work, feed it a file:
python3 scanner.py -l hosts.txt -t 20 -o results.json
The default thread count is 10 and the request timeout is 60 seconds. Raising threads speeds up large lists, but be realistic about your target's capacity. Scanning 20 threads against a shared hosting box is a fast way to get your IPs rate-limited or blocklisted, and then you've got a different problem.
Two flags are worth knowing. --all-results saves every result, not just vulnerable hosts, which is useful for audit trails. --follow-redirects is off by default, and that's intentional. If you follow redirects, you may end up testing a different application behind a login gate and attributing its behavior to the original host.
Docker users can run the same thing with a mounted volume:
docker build -t php2shell-scanner .
docker run --rm -v "$PWD:/data" -w /data php2shell-scanner -l hosts.txt -o results.json
Testing Custom Paths and Admin Endpoints
By default the scanner tests /index.php. If you supply a path in the URL, it preserves that path. You can also test specific files with --path or a list with --path-file.
python3 scanner.py -u https://example.com --path /phpinfo.php
python3 scanner.py -u https://example.com --path-file paths.txt
Here's where it gets interesting. The scanner automatically retests phpMyAdmin targets at /show_config_errors.php. That's a practical detail: phpMyAdmin installations are a common exposure point, and many admins never think to probe that endpoint directly. If you run phpMyAdmin on a host that otherwise looks clean, don't stop at the index page.
Custom headers are useful when the application sits behind authentication. You can pass them with -H, for example -H "Authorization: Bearer token". Be careful with session cookies in shared lists, though. Credentials end up in logs and in the saved JSON evidence unless you handle the output file accordingly.
The Memory Limit False Positive You Need to Understand
This is the section most people skip, and it's the one that will save you from bad reports. The project's own limitations section says that on certain PHP configurations, the scanner can flag a patched host. The probe exceeds PHP's memory_limit, and a patched and a vulnerable server can both return the same empty HTTP 500.
The reported reproductions are specific. The team saw this on patched PHP 8.4.27 with memory_limit values from 2M to 32M. They also saw it on PHP 8.3.36 at 128M when max_multipart_body_parts was set to 2785 or higher. The README is explicit that the exact threshold may vary between builds.
The counter-intuitive part is this: the more a server is configured to resist memory exhaustion, the more likely it is to look vulnerable to this scanner. A tight memory_limit produces the same signature as the bug you're hunting. Most guides assume a higher limit is always safer, and for this particular check, that assumption can mislead you.
The fix is to verify. When PHP doesn't disclose its version, the scanner cannot tell a safe post-patch OOM from genuinely vulnerable pre-patch behavior. Pull the actual PHP version from your package manager or php -v, check it against the vendor advisory, and confirm whether your distribution has backported the fix. A Debian or Ubuntu package can carry a patched build while still reporting an older upstream version number. Version strings alone aren't proof.
Network Impact and Operational Safety
Each tested path may receive a 42-byte control request and three probes totalling about 29 MiB. That's not heavy traffic for a single host, but it adds up across thousands of targets. Probing stops as soon as the verdict is settled, and duplicate input URLs are scanned once, which limits the load.
Before you scan anything you don't own, get written authorization. Scanning third-party infrastructure without permission can create legal and contractual exposure even when your intent is defensive. Internally, notify the team that runs your WAF and log pipeline so an incident isn't opened over your own test traffic.
Most people stop here, and they shouldn't. A scanner result is a lead, not a verdict. Confirm the PHP version, check the vendor backport status, and re-test after patching. That's the difference between a clean report and a false sense of security.
Frequently Asked Questions
What is the php2shell scanner?
The php2shell scanner is an open-source command-line tool from Assetnote that detects CVE-2026-109618 and CVE-2026-109619 in PHP applications. It sends a control request and up to three multipart probes, then reports a host as vulnerable based on memory-exhaustion errors, differential empty HTTP 500 responses, PHP-FPM worker failures, or unpatched version banners. It requires Python 3.9 or later.
How do I check if my server is vulnerable to php2shell?
Run python3 scanner.py -u https://your-host after installing the requirements with pip. Review any hit with -v to see the verdict reason, then confirm the installed PHP version against your vendor's advisory. Treat the scanner output as a starting point, not a final answer, especially if memory limits are tight.
Why does the php2shell scanner report patched servers as vulnerable?
When a probe exceeds PHP's memory_limit, patched and unpatched servers can return identical empty HTTP 500 responses. Assetnote's documentation reports this on patched PHP 8.4.27 with limits from 2M to 32M. Verify the real PHP version and backport status before acting on a result.
Can the scanner test paths other than index.php?
Yes. Use --path for individual paths or --path-file for a list. The scanner also automatically retests phpMyAdmin targets at /show_config_errors.php, so you don't need to add that path manually for those installations.
Want to see how this fits your wider exposure picture? Start by running the scanner against a small pilot list, then read our guide on patching PHP across mixed server fleets to plan remediation. If you're tracking PHP-FPM hardening, our breakdown of PHP-FPM worker configuration best practices is the logical next read. Run the php2shell scanner on one non-production host this week, note the verdict reason, and compare it against your installed PHP version.