Blog
Security research

Exploiting PHP2Shell and Node2Shell: How we built RCE exploits with local AI models

 - 
November 3, 2026

Can local AI models develop working remote code execution exploits for complex memory corruption vulnerabilities? We researched this by reproducing PHP2Shell and Node2Shell in a controlled lab setup, using local AI models running on two NVIDIA DGX Spark systems. Here’s how the exploit chains worked, why reliable exploitation required target-specific calibration, and what the experiments tell us about the capabilities of locally hosted models for security research.

You information will be kept Private
Table of Contents

At Invicti, we recently researched two pre-auth remote code execution vulnerabilities: PHP2Shell (CVE-2026-109618 / CVE-2026-109619, affecting PHP) and Node2Shell (CVE-2026-111497, affecting Node.js). Both are memory corruption bugs that can give an attacker a shell on the server.

Beyond researching the flaws and developing security checks for the Invicti AppSec Platform, we also did something more experimental that we hadn’t done before: we built working exploits for both vulnerabilities using local LLMs running on our own hardware. No cloud API, no frontier model. 

If you’re looking for information about PHP2Shell detection and remediation, see PHP2Shell: Two critical PHP vulnerabilities require immediate patching.

This article explains:

  • How PHP2Shell and Node2Shell vulnerabilities work
  • Why both are very hard to exploit in the real world
  • Why a security scanner cannot check for their presence by running the type of exploit we built
  • What today’s local models are capable of in security research
This research used experimental local AI models in an isolated lab. The exploit research and development workflow described here is separate from the production vulnerability checks and security testing capabilities of the Invicti platform. The exploits described below are experimental proofs-of-concept, not the security checks used in the product.

What is PHP2Shell?

PHP2Shell is a vulnerability chain in PHP itself, so you can exploit it without vulnerabilities in a PHP application – an HTTP request is enough. For this research, our target was the official php:8.4.4-apache Docker image.

Here’s an overview of how we exploited PHP2Shell in our local experiments:

  • When PHP runs out of memory while parsing a request, it can leave dangling pointers behind, i.e. pointers to memory that was already freed. PHP counts list elements before it finishes allocating them, so if memory allocation fails at that stage, list teardown code later trusts and frees memory that was never properly initialized.
  • The exploitable case we targeted is the session upload progress feature. PHP opens a session while the request body is still being parsed. If parsing crashes with an out-of-memory error before parsing is complete, the session pointers survive while the rest of the request memory is freed.
  • The exploit is a three-stage chain, all sent over plain HTTP:
    • ‍Leak the heap. Crash the request by sending a huge body, then use the same connection to send a second request that reuses the freed memory. The server responds with a Set-Cookie header containing 4 KB of leaked memory. This reveals where the heap is.‍
    • Leak the code. Similar trick as for finding the heap, but this time the cookie leaks a code pointer that reveals where libphp.so is loaded and therefore gives us the address of the internal system() handler.‍
    • Execute a command. Spray the freed memory with fake objects whose destructor method is a system() call with our command. When PHP cleans up the request, it destroys the fake object and the destructor runs the command.

The final result is getting system("id > /var/www/html/p2s.txt") executed as the web server user – all pre-auth and with only a few HTTP requests.

PHP2Shell RCE exploit in action

What is Node2Shell?

Node2Shell is a vulnerability in the Brotli decompression library bundled with Node.js. The bug was fixed in Node.js v24.22.1, and our test target was v24.21.0. The target application is a simple TCP service that decompresses attacker-controlled data. This is the only application-level vulnerability needed for the exploit, but it needs to be combined with some specific application behavior (see below for the caveats).

Here’s how we exploited Node2Shell:

  • The Brotli decoder has a bug in its fast code path where it ignores an error return value. When a compressed stream asks for a block switch that is not valid, the decoder should reject the stream – but on the fast path, it just keeps going in an inconsistent state.
  • While in that state, the decoder appends incoming bytes to a small 8-byte internal buffer without checking the buffer length. The bytes from that buffer overflow into the decoder’s own state fields.
  • One of those state fields is pos, which is the position where the decoder writes decompressed data. With pos under attacker control, the next decompression copy operation becomes a controlled write to memory. Our exploit points it at a return address on the stack.
  • The 16 bytes that are written redirect execution into an ROP chain (return-oriented programming – small pieces of existing code chained together) that calls execve("/bin/sh", ...) and replaces the Node process with a shell.

The whole chain fits into two application-level messages sent over TCP: one big message (~23 MB) that plants the payload and corrupts the decoder, and one 127-byte message that triggers the overflow.

Node2Shell RCE exploit in action

Why reliable exploitation heavily depends on the target environment

A question we get a lot, including from internal teams, is this: If the vulnerability is so bad, why can’t just anyone exploit it? The answer is that both flaws are real and dangerous, and both can lead to remote code execution. But getting from memory corruption to reliable code execution depends heavily on the target environment. Our exploits were developed against specific builds and configurations, with memory layouts and offsets calibrated for those individual targets. The payloads we created are not directly portable to arbitrary vulnerable installations.

Note that this is a limitation of the specific exploit chains we built, not a general limitation of the vulnerabilities. Other exploitation techniques may emerge that have different prerequisites. Just because this specific approach isn’t portable doesn’t mean that somebody won’t find a different method to exploit these and similar bugs.

Exploitation challenges common to PHP2Shell and Node2Shell

The first and most fundamental problem is that these are memory heap exploits, not simple code bugs. The exploit code operates on specific memory addresses and there is no single generic request that works everywhere to give you RCE. Every memory address, every offset, and every size in our experimental exploit was custom-matched to one exact build and tech stack.

Incorrect offsets or allocation assumptions can cause the process to crash instead of executing the payload. If any memory offset is slightly off, the process just dies with a SIGSEGV signal. From the outside, a failed exploit attempt and a patched server can look identical, so it’s really hard to determine if an attack succeeded.

Finally, the vulnerability class is broad and portable, but the exploit is not. A similar vulnerable pattern exists in many builds, but a working exploit must be re-calibrated for each PHP/Node version, operating system, and configuration.

Exploitation challenges specific to PHP2Shell

For this exploit, memory offsets depend on the exact build. The addresses of system() and other internals differ depending on the PHP version, distribution, or even compile flags. For example, the bookworm 8.4.4 build uses different offsets than the trixie 8.4.26 build.

A related problem is that the heap layout must be measured individually for every target. It’s true that the exploit leaks addresses at runtime, which defeats randomization via ASLR, but the relative positions of objects on the heap depend on the allocator’s behavior for a specific build, memory limit, and request shape. For our exploit, they were derived with a debugger on a matching container.

If that wasn’t hard enough, web server configuration changes everything again. Settings such as memory_limit, post_max_size, max_input_vars, the session upload progress setting, and the number of Apache workers change the heap geometry. So even if your exploit works fine with, say, five Apache workers, it will crash with one worker due to changes in the memory layout.

The exploit also depends on the PHP execution environment. Our chain was developed against Apache with mod_php and relies on memory reuse across requests handled by the same process. PHP-FPM has a different worker lifecycle, so the same sequence cannot be transferred directly. The underlying PHP vulnerabilities can still be present in PHP-FPM deployments, but exploiting them would require a different approach.

Exploitation challenges specific to Node2Shell

Our Node2Shell PoC ran in a controlled lab environment. We disabled ASLR, configured Node.js to use a single thread-pool worker, and used a warm-up connection to stabilize memory allocation. The ROP chain contains addresses specific to that environment. With ASLR enabled, this technique would require a way to recover or otherwise account for the relevant addresses at runtime. 

Similar to PHP2Shell, every memory offset must be exactly right and the exploit must be matched to the exact binary and glibc build. Every gadget in the ROP chain is based on the specific node binary and glibc present on the target machine. A different Node.js build or a different glibc version would shift them all to different offsets, requiring a new exploit.

Unlike with the PHP2Shell exploit, our Node2Shell PoC also depends on the behavior of the test application. The service accepts large attacker-controlled Brotli streams and creates a decoder for each connection in a predictable way. These conditions are necessary for our particular exploit chain – they are not general requirements for triggering the underlying Brotli vulnerability. Other applications that decompress untrusted Brotli data may expose different exploitation paths.

A patched decoder rejects the malformed input before the vulnerable state transition used by our exploit.

Why such exploits are not suitable for automated vulnerability scanning

This question deserves its own section because the differences between different ways of detecting vulnerabilities are fundamental to understanding how security scanners work. There are really three kinds of checks a scanner can do.

Passive check to detect a vulnerable software version

In many cases, you can dynamically detect tech stack and component versions (runtime SCA). There are more sophisticated fingerprinting methods to do this, but sometimes you just need to check the version string. This works when the server discloses it – PHP, for example, often sends a header like X-Powered-By: PHP/8.4.4 with every response. In this simple case, the check says: “You’re running PHP 8.4.4, which we know is affected by CVE-2026-109618.”

This kind of passive detection is easy and safe, but it has a downside: it does not prove the vulnerable version is actually exploitable in a specific system. Maybe a patch was backported without the version number changing? Maybe the vulnerable feature is disabled in the configuration? You might be running an affected version on multiple systems without knowing which one is most at risk.

And passive detection only works when the version is disclosed at all: Node.js, for example, does not send its version in headers by default, so for Node2Shell, there is often nothing to look at passively.

Active check without exploitation

To actively check for a vulnerability, you need to send a request that triggers the same detection as an exploit but without doing any harm. For example, you might send the malformed Brotli stream and see if the server rejects it (meaning it’s patched) or silently accepts it (meaning it’s vulnerable). Compared to a passive check, which only signals a potentially vulnerable software version, a successful active check tells you the vulnerability itself is present in the target system.

An active check can provide stronger evidence than version fingerprinting alone, but the result depends on what the probe actually tests. A response that can distinguish between the behavior of a patched server and a vulnerable decoder may confirm the presence of the vulnerability without demonstrating remote code execution.

For memory corruption vulnerabilities, even a probe that does not attempt exploitation can potentially crash the target if it reaches the vulnerable code path. The check therefore needs to be designed and evaluated for both detection reliability and operational safety.

Active check with exploitation to a full RCE

Finally, you can actually exploit the vulnerability and run a non-destructive command like id to prove it. We did this in the lab – but this is exactly what a scanner cannot safely do in production for memory bugs. 

The exploits we developed are not usable for routine production scanning. They depend on build-specific offsets, controlled memory layouts, and assumptions about the target process. When those assumptions are even slightly wrong, the result may be a process crash rather than code execution. Repeatedly attempting such exploitation across unknown production environments would mean an unacceptable risk of service disruption.

Note that this limitation is specific to the exploitation techniques and vulnerability classes we investigated in this case. For many other vulnerability types, automated scanners like Invicti’s DAST can and do safely confirm exploitability using controlled, non-destructive techniques. But for memory corruption bugs such as these, version identification and carefully designed active checks are more appropriate, provided the scanner makes clear what its findings establish and what remains unverified.

How we built experimental exploits with local models

Both exploit chains were developed with AI agents running local models on our own private lab hardware – a cluster of two NVIDIA DGX Spark machines. Each Spark has a GB10 chip with 128 GB of unified memory (CPU and GPU share the same RAM), and the two are connected with a fast direct RDMA cable so they work as one bigger GPU.

The cluster can serve five switchable local models, but only one runs at a time. The two used for this research:

  • PHP2Shell was reproduced with GLM-5.3-Flash, running quantized across both Sparks.
  • Node2Shell was reproduced with DeepSeek-V4.1-Flash-1017, a 763B-parameter model, also running on the two Sparks.
This is an experimental lab setup only. Security checks for Invicti products are developed using different tools, and Invicti’s AI features are powered by frontier models.

With the target environment set up, the general workflow was the same in both cases:

  1. The model reads prior public research. In this case, this was blog posts about related PHP bugs for PHP2Shell, and the public crash reproduction and the upstream Brotli fix for Node2Shell.
  2. It forms a hypothesis about how the bug can be controlled, and writes a probe.
  3. The probe runs against the target in a debugger; the model observes the leak or the crash.
  4. It adjusts parameters such as offsets, spray sizes, and request shapes, and loops until the exploit is successfully proven end to end.

The agents repeated this process many times, calibrating memory offsets, allocation patterns, and request parameters against the test environments. Our experiments demonstrate that local models can carry out substantial exploit development work.

Why local AI models are increasingly useful for exploit research

Only a few years ago, locally running a model capable of this kind of work was not realistic. But for this research, we used DeepSeek-V4.1-Flash-1017, a 763B-parameter model, on two NVIDIA DGX Spark systems with 128 GB of unified memory each. At 16-bit precision, the model’s weights alone would require roughly 1.5 TB of storage, which far beyond the available memory. Running it locally requires a combination of model architecture, aggressive quantization, and an inference engine designed for this hardware.

Several features made the setup practical:

  • Engram lookup tables: Approximately 197B parameters are stored in token-indexed lookup tables rather than conventional neural network layers. The inference engine keeps these tables on NVMe storage and fetches the required entries as needed. This reduces GPU memory requirements, although caching and disk access introduce additional overhead.
  • Quantization: We used an EXL3-quantized checkpoint with an average weight precision of approximately 2.9 bits. Different model components use different precisions according to their sensitivity to quantization. This allows the main model weights to fit within the available memory, with some loss of numerical precision.
  • Mixture-of-Experts routing: Although the model contains hundreds of billions of parameters, only a small subset of its expert networks is activated for each token. This reduces the amount of weight data that must be accessed during generation, which is particularly important on memory-bandwidth-constrained hardware.
  • Speculative decoding: A smaller draft component proposes tokens that the main model verifies in batches. In our configuration, this reduced the number of full-model decoding passes required to generate output. The benefit depends on how often the proposed tokens are accepted.

We served the model using TensorFold, which handles quantized inference, asynchronous retrieval of Engram entries, and execution across both DGX Spark systems. The two machines are connected directly over RDMA, allowing model computation to be distributed between them.

Long-context support was also useful for this work. Exploit development generates large amounts of intermediate material, including debugger output, disassembly, test scripts, and research notes. Sparse attention and KV-cache quantization reduce the memory cost of retaining that context, although the practical limit still depends on the inference configuration and available memory.

The important result is that this configuration was sufficient to run the model locally and use it interactively during exploit development. The experiment did not require a cloud inference API, and the target binaries, debugger output, and exploit artifacts remained within our controlled research environment.

Conclusion

PHP2Shell and Node2Shell are both critical pre-authentication RCE vulnerabilities, but our experiments demonstrate how much work can separate a memory corruption vulnerability from a reliable remote code execution exploit. Our exploit chains required target-specific memory layouts, offsets, and process behavior. Those dependencies make those specific payloads unsuitable for routine production scanning, where an unsuccessful attempt could crash the target. The underlying vulnerabilities remain serious, regardless of the limitations of the particular exploit chains we examined.

The experiments also demonstrate that local models can contribute meaningfully to exploit development. Working from public research and debugger feedback, the agents iteratively constructed and tested exploit chains against controlled targets. The results suggest that local models are becoming practical tools for vulnerability research, including crash analysis, exploitability assessment, and the development and validation of fixes. The same loop that builds exploits can also run for defense: fuzzing, triaging, and patching these bug classes before attackers get to them.

Frequently asked questions

Table of Contents