Skip to content
PaxDynamics.Let’s talk
English
Latest post:PHP after the patch
Back to Blog

Following the chain into application code

Part 2: Drupal, Joomla, Roundcube, and WordPress, and the exact conditions that separate a loaded gadget from a working PHP session chain.

PHP research · Part 2 of 3. Series index · Previous: the destructor path · Next: failed tests

Source: Application results · Drupal verifier and lab · Roundcube verifier and lab

An engine primitive does not identify an exposed application. The next question was whether loaded application code supplied the destructor, session path, and allocation behaviour that the lab chain needed.

Four codebases produced four outcomes. Drupal completed a lab profile with non-default session configuration. Joomla supplied re-entry but no command result. Roundcube completed the profile only with a lab plugin. A guard blocked the candidate path we inspected in WordPress. The stopping point is as important as the application name.

Scope: These are results for our CVE-2026-115560 session-delivery and var_destroy() path on named builds. They do not measure the separate October PHP2Shell paths reported by Wiz and Calif.

INTERACTIVE RESEARCH / 03

Where does the route stop?

Select a documented application profile and inspect how far the tested route progressed.

SELECT AN OBJECT TO INSPECT GEOMETRY + TIMING SCHEMATIC
Start with the session path

An engine primitive is not an exposed application. Storage, serializer configuration, and the application read path must align.

Recorded evidence ↗

Reduced motion: select steps to change the diagram. Symbolic objects; observed results keep their recorded conditions.

Transcript and recorded profiles
  1. Start with the session path. An engine primitive is not an exposed application. Storage, serializer configuration, and the application read path must align. Evidence
  2. Inspect the stored record. A shared key alone is insufficient. The consumer must read the intended data in the expected format. Evidence
  3. Resolve loaded classes. A candidate destructor must be loaded and reachable under the application state. A source-level path is not proof of execution. Evidence
  4. Reach parser re-entry. Drupal core and Joomla supplied different re-entry paths. Roundcube required a lab plugin; a guard blocked our WordPress candidate. Evidence
  5. Check the result boundary. Joomla reached the ASAN memory error. That result does not supply the terminal call shown in the completed lab profiles. Evidence
  6. Keep the conditions attached. The selected profile below states its measured result and required conditions. These cases do not establish default application exposure. Evidence
  • Drupal: Fixed command in lab. Core classes supplied re-entry and allocation behaviour; the completed lab needed non-default session configuration. Evidence
  • Roundcube: Plugin-assisted command result. A lab plugin supplied both the serializer switch and the re-entry destructor. Evidence
  • Joomla: ASAN UAF only. Application code supplied re-entry under the stated sink conditions. No terminal command was demonstrated. Evidence
  • WordPress: Candidate path blocked. A garbage-collection guard blocked the candidate path in the reviewed default state. This does not address the October CVE paths. Evidence

The survey asked whether loaded application code could supply a destructor that re-entered unserialize() in the required state. A lab-only class could not answer that question.

The recorded survey covered 46,508 PHP files across roughly 60 trees. It combined pattern searches, a call graph, source inspection, and selected runtime tests. Its scope did not include every plugin ecosystem or every deployment configuration.

The result for each application depends on its session path, loaded classes, and guards:

ApplicationRecorded resultRequired condition or remaining limit
Drupal 11.4.11Fixed command reached in the labNon-default session serializer configuration
Joomla 5.4.xRe-entry UAF confirmed under ASANAn application sink is required; no terminal call demonstrated
Roundcube 1.6.11Fixed command reached with a lab pluginPlugin supplies the switch and re-entry destructor
WordPress 7.1.3Our candidate var_destroy() gadget path was blocked under the reviewed default stateGarbage collection must be disabled to enter that path; this says nothing about the separate October CVEs

The application survey records the profiles and their controls.

Drupal: existing core code completes the tested chain

Drupal's P5 profile completed the chain using existing core classes. It required a non-default session serializer configuration. Wiz and Calif also reported Drupal results for the October CVEs; those are separate paths and test conditions.

The destructor path runs through existing core classes:

YamlCacheCollectorDiscovery
  → MenuActiveTrail
  → CacheCollector::updateCache
  → MemoryBackend::prepareItem
  → unserialize

The same path also creates an allocation that the lab could use to reclaim the freed parser bookkeeping. This mattered because the engine result needs both re-entry and suitable memory placement.

The direct profile reached the fixed command on PHP 8.4.27 and 8.5.12. Composer resolved the relevant classes during decoding without a separate class_exists() preload.

The application condition is explicit: the consumer needs the non-default session.storage.options.serialize_handler: php setting, while the producer writes using php_serialize with upload progress enabled.

The tested classes are Drupal core code. The complete deployment is still configuration-dependent.

Default cleanup adds a separate test

A read must meet the upload window

A / UPLOAD WORKERRECORD EXISTSCLEANUPB / SESSION READERRREAD IN WINDOWA / UPLOAD WORKERRECORD EXISTSCLEANUPB / SESSION READERRREAD IN WINDOWEVENT ORDER · TIMING SCHEMATIC

Documented race profile. The documented cleanup-on profile uses another worker to read while the progress record exists. Worker count, seed state, and placement remain part of the tested profile.

All recorded cases
  • Read during upload: Documented race profile. The documented cleanup-on profile uses another worker to read while the progress record exists. Worker count, seed state, and placement remain part of the tested profile.
  • Read after cleanup: Window missed. At upload completion, the progress record is removed. The late-read control misses the required record. This timeline shows event order, not measured request latency.
Select a case to replay. Schematic; recorded results only. Read the evidence ↗

The direct profile used session.upload_progress.cleanup=Off. That is useful for isolating the decoder path, but it does not answer what happens with the default cleanup behaviour.

With cleanup enabled, the progress record is removed when the upload finishes. A second request must read it while the upload is still active.

The Drupal race profile reached the fixed command on PHP 8.4.27 with multiple prefork workers and a persistent seed record. Its memory placement differed from the direct profile.

Those are two distinct results:

  • →The direct profile used a single child with cleanup disabled.
  • →The race profile used multiple children with cleanup enabled.

They must not share a single-worker limitation in the same result description.

The controls included removing the required allocation shape, using the wrong target address, waiting beyond the upload window, and omitting the seed record. The Drupal record contains the recorded outcomes; the lab setup rebuilds the tree and the three profiles.

Joomla: re-entry is present, but the terminal is missing

Joomla supplies a destructor path through PHPMailer, a callable wrapper, and Registry::__clone().

That path reaches:

unserialize(serialize($this->data))

ASAN confirmed the use-after-free in the tested builds. Selected FPM tests also reached the memory error through a lab endpoint.

The inner serialize() call is an important limit. It produces a normal serialized representation. It does not supply the out-of-range reference and raw pointer placement required by the forged-object terminal used elsewhere in this work.

Joomla therefore removes one application precondition: the lab does not need to define the re-entry classes itself. It does not provide an unauthenticated entry point or demonstrate command execution.

The stock-allocator run ended differently: the record describes a LogicException after the destructor chain, and the worker continued serving. The observed failure in the ASAN run therefore does not establish a stock-worker crash.

The supported result is the re-entry UAF under the stated sink conditions. The Joomla analysis explains the missing terminal primitive.

Roundcube: the two useful properties do not meet by default

Roundcube initially looked promising because plugins load before session initialization.

The stock driver behaviour creates the obstacle.

The db, redis, memcache, and memcached drivers perform the serializer switch, but read through their own storage formats. The native php session path can read the progress file, but does not perform the switch.

The lab plugin supplies the missing switch and defines the required re-entry destructor. With that assistance, the profile reached the fixed command on PHP 8.4.27 and 8.5.12.

That is a plugin-assisted result. Removing the plugin behaviour changes the tested system and the claim.

The Roundcube profile records the load order, storage conditions, and negative controls. The lab setup rebuilds the tree and installs the plugin.

Redis aligned the key, but not the value

Same key does not mean same record

KSAME KEYRAW ARRAY×EMPTY SESSIONKEY AGREEMENT ≠ FORMAT AGREEMENTKSAME KEYRAW ARRAY×EMPTY SESSIONKEY AGREEMENT ≠ FORMAT AGREEMENT

Wrapper absent. The upload hook and Roundcube can address the same Redis key. The raw progress array lacks the vars field required by Roundcube, so its driver returns an empty session value.

All recorded cases
  • Raw progress array: Wrapper absent. The upload hook and Roundcube can address the same Redis key. The raw progress array lacks the vars field required by Roundcube, so its driver returns an empty session value.
  • Expected wrapper control: Destructor reached in control. The isolation cell with the expected key and Roundcube wrapper reaches the destructor. That control does not turn the raw upload-hook record into the required wrapper.
Select a case to replay. Schematic; recorded results only. Read the evidence ↗

The next hypothesis was that a common Redis store could remove the need for plugin assistance.

One part worked: the session key names could be aligned, and stock Roundcube performed the serializer switch.

The record format blocked the delivery.

The upload-progress hook stored a serialized progress array. Roundcube expected its own wrapper, including a vars field containing the session data. Reading the hook's array did not supply that field, so the driver returned an empty session value.

The isolation matrix supported that explanation. The cell with the expected key and Roundcube wrapper reached the destructor. The raw hook record did not.

The isolation matrix falsified the same-store hypothesis for this profile: the expected wrapper reached the destructor; the raw hook record did not.

The test does not establish that every Redis-backed arrangement is safe. It covers one PHP build, the stated phpredis version, cleanup disabled, and the tested record formats. The Redis race was not rerun. Redis experiment and matrix

WordPress: code presence does not establish reachability

WordPress contains a relevant path through SimplePie and WP_HTML_Token.

A guard changes its reachability: SimplePie makes the direct destructor call only when garbage collection is disabled. The reviewed core path does not disable it.

The accurate result is therefore that our candidate path was not reachable as shipped in the reviewed configuration. Wiz reported unauthenticated WordPress execution through the October PHP2Shell vulnerabilities. Our negative result does not dispute that report or establish WordPress-wide safety.

The source contains the candidate chain, but the reviewed execution state blocks it. Both facts are needed to describe the result.

This also explains a limit of the static graph: a traversal through an untyped array was outside the modelled edges. An empty graph result did not settle the source-level question.

The reproduction package ships the lab setup

The repository now tracks the setup, Compose/INI files, endpoints, and plugin needed to rebuild the Drupal and Roundcube profiles. The upstream application trees themselves are downloaded by setup.sh and remain excluded from Git, as in the other application labs.

The trees are not vendored, so a reader fetches the same pinned upstream releases rather than downloading a Git history of application code. The recorded results still require the named builds, configurations, worker models, and address constants.

The setup files make the profiles easier to check. They do not turn the configuration-gated Drupal result, plugin-assisted Roundcube result, Joomla memory error, or Redis format negative into default application exposure. The series index tracks the remaining reproduction work.

The source review, controlled reproductions, and stated limits in this investigation are also part of our security research and application testing work.