• Products & Services
        • Cloud

          • Endpoint Protection
          • Endpoint Detection and Response
          • Mobile Device Management
          • BYOD
          • Extended Detection and Response
          • Zero Trust Network Access
          • Data Privacy
        • On Prem

          • Endpoint Protection
          • Endpoint Detection and Response
          • Data Privacy
        • Platform

          • Malware Analysis Platform
        • Small Business

          • SOHO Total Edition
        • Services

          • Threat Intel
          • Digital Risk Protection Services (DRPS)
          • Ransomware Recovery as a Services (RRaaS)
          • DPDP Compliance
          • Managed Detection and Response
          • Cybersecurity & Data Privacy Awareness
  • Solutions
    • BFSI
    • Education
    • Government
    • Healthcare
    • ITeS
    • Manufacturing
  • Company
    • About Seqrite
    • Leadership
    • Awards & Certifications
    • Newsroom
  • Partners
    • Partner Program
    • Locate Partner
    • Become A Partner
  • Support
  • Resources
    • Blogs
    • Whitepapers
    • Datasheets
    • Case Studies
    • Threat Reports
    • Manuals
    • PoV
    • Understanding Data Privacy
    • Check You Risk Score
    • DPDP Dialogues
    • Privacy Hour
Seqrite Labs Blog
Contact Sales Under Attack?
  • Products & Services
        • Cloud

          • Endpoint Protection
          • Endpoint Detection and Response
          • Mobile Device Management
          • BYOD
          • Extended Detection and Response
          • Zero Trust Network Access
          • Data Privacy
        • On Prem

          • Endpoint Protection
          • Endpoint Detection and Response
          • Data Privacy
        • Platform

          • Malware Analysis Platform
        • Small Business

          • SOHO Total Edition
        • Services

          • Threat Intel
          • Digital Risk Protection Services (DRPS)
          • Ransomware Recovery as a Services (RRaaS)
          • DPDP Compliance
          • Managed Detection and Response
          • Cybersecurity & Data Privacy Awareness
  • Solutions
    • BFSI
    • Education
    • Government
    • Healthcare
    • ITeS
    • Manufacturing
  • Company
    • About Seqrite
    • Leadership
    • Awards & Certifications
    • Newsroom
  • Partners
    • Partner Program
    • Locate Partner
    • Become A Partner
  • Support
  • Resources
    • Blogs
    • Whitepapers
    • Datasheets
    • Case Studies
    • Threat Reports
    • Manuals
    • PoV
    • Understanding Data Privacy
    • Check You Risk Score
    • DPDP Dialogues
    • Privacy Hour
Home  /  Technical  /  PHP2Shell: Failed Requests, Memory Corruption and Remote Code Execution
23 October 2026

PHP2Shell: Failed Requests, Memory Corruption and Remote Code Execution

Written by Satyarth Prakash
Satyarth Prakash
Technical

PHP2Shell affects how PHP handles a request that runs out of memory or encounters a fatal error. If the failure interrupts an operation before it finishes, PHP can leave an array entry only partly initialized or retain a pointer to memory that is later freed. Request cleanup or a subsequent request can then use that invalid state, causing memory corruption that can lead to remote code execution.

Some exploitation paths run while PHP processes incoming form data, before the application script starts. Others require successive requests handled by the same server process. Because the flaws are in PHP’s runtime, the engine that executes application code, application-level authentication and input validation cannot protect the pre-execution paths. Updating the application alone does not fix the underlying runtime.

Vulnerability overview

Disclosed on 21 October 2026, PHP2Shell groups two critical PHP vulnerabilities. Each has a 9.2 score under version 4.0 of the Common Vulnerability Scoring System (CVSS), which rates vulnerability severity:

CVE Failure Security consequence
CVE-2026-109618 Memory exhaustion interrupts PHP while request data is only partly initialized Cleanup can use unfinished fields as values or memory addresses
CVE-2026-109619 PHP retains references to temporary data after releasing its memory A later request in the same server process can use freed memory

The published demonstrations achieved remote code execution (RCE): an attacker could run code on the server through requests that required no login. They also exposed data held in the PHP process’s memory. Which path is reachable depends on the application, configuration and the way the web server runs PHP.

CVE-2026-109618: An allocation failure leaves an unfinished array entry

While reading a web request, PHP builds arrays to hold form fields and upload metadata, such as filenames and file sizes. Each named entry has a key, used to look up its value. PHP allocates memory for both. Uploads commonly use the multipart/form-data format, which divides the request body into separate parts for fields and files.

PHP limits memory consumption through the memory_limit setting. In vulnerable versions, an allocation that exceeds this limit can immediately stop the operation in progress and transfer control to request-level error handling. For an out-of-memory (OOM) error, PHP calls this immediate transfer a bailout. Statements that would normally finish the interrupted operation are skipped.

The vulnerable insertion counts a new array entry before allocating its key. A bailout at that allocation leaves the count increased even though the entry was never initialized.

PHP implements these arrays in C as hash tables: the entry’s key is converted to a numeric hash that helps locate its value. Each entry is a Bucket. It contains a zval, PHP’s C structure holding a value and its type, the numeric hash, and a pointer storing the address of the key string.

The insertion code updates the table’s counters before allocating the new key:

    idx = ht->nNumUsed++;

ht->nNumOfElements++;

p = ht->arData + idx;

p->key = key = zend_string_init(str, len, GC_FLAGS(ht) & IS_ARRAY_PERSISTENT);

#if ZEND_RC_DEBUG

if (GC_FLAGS(ht) & GC_PERSISTENT_LOCAL) {

GC_MAKE_PERSISTENT_LOCAL(key);

}

#endif

p->h = ZSTR_H(key) = h;

HT_FLAGS(ht) &= ~HASH_FLAG_STATIC_KEYS;

if (flag & HASH_LOOKUP) {

ZVAL_NULL(&p->val);

} else {

ZVAL_COPY_VALUE(&p->val, pData);

nNumUsed defines the used range of bucket slots; nNumOfElements counts array elements. Both increase before PHP initializes the new slot at p. The key assignment waits for zend_string_init(), which allocates memory through zend_string_alloc() and pemalloc(). A bailout inside that call skips both the key assignment and the later value initialization.

The bailout does not undo nNumUsed, so the unfinished bucket remains inside the range cleanup will traverse.

Array cleanup reaches the unfinished entry

PHP stores submitted fields and upload metadata in $_POST and $_FILES. These arrays are superglobals, available throughout a script. During php_request_shutdown(), PHP releases their contents before reclaiming the request’s heap, the memory used for dynamic allocations. Array cleanup calls zend_array_destroy(), which walks from the bucket storage at ht->arData to ht->arData + ht->nNumUsed, including the unfinished slot.

PHP uses a count of outstanding references to manage the lifetime of many values. Releasing a reference decreases that count; at zero, PHP can destroy the value. The cleanup function, or destructor, first inspects the zval’s type to decide whether it refers to such an allocation.

If the unfinished zval appears reference-counted, cleanup follows its pointer and decrements the target’s count. At zero, rc_dtor_func() selects a destructor using the target’s type. The bucket’s uninitialized key pointer can also reach string-release code when it appears non-null on the applicable cleanup branch.

An attacker who influences earlier allocations may influence the bytes left in that slot. Treating them as a C-level pointer and type can make cleanup modify or release the wrong memory. This is a memory-corruption primitive, a low-level operation an exploit can build on.

Complete unauthenticated RCE was demonstrated with mod_php, the module that runs PHP inside the Apache web-server process. Apache runs request cleanup even after startup fails, exposing this unfinished entry to destruction. The remaining exploit work depends on the PHP build and allocation layout.

Request parsing can exhaust the memory limit

A request body can occupy substantially more memory after PHP converts it into arrays and strings. The initial check compares content_length, the declared body length, with post_max_size, PHP’s configured limit for POST request data. When a usable length is absent, that initial comparison does not bound the bytes subsequently read.

Nested field names multiply this work. PHP builds their hierarchy under the metadata’s name, full_path, type, tmp_name, error and size branches. A short field name can therefore generate many arrays and buckets. Headers inside multipart parts add more allocations, allowing input processing to exhaust memory before it finishes building these structures.

CVE-2026-109619: A later request uses pointers to freed memory

A PHP server process, called a worker, can handle several requests in succession. After each request, PHP releases its temporary memory while keeping the process running for the next request.

Fields called globals remain in the worker between requests. If a global points to request memory, PHP must clear it before releasing that memory. A missed reset lets a later request follow the old pointer after its target has been freed or reused. This is a use-after-free, the failure behind CVE-2026-109619.

PHP sessions preserve data between requests, using a session ID to identify the stored record. The Session extension provides this feature and loads a copy of the record into request memory. Upload-progress tracking can load that data while the request body is still being parsed, before PHP finishes setting up its extension modules for the request.

The input parser notifies the Session extension as it encounters upload parts. The extension handles each notification in a callback, a function the parser invokes for that event. During the first qualifying file-part event, it initializes its working state and stores the session ID in request memory. PHP stores the session ID and loaded session data in internal Session-extension fields. The C code accesses these fields through the PS(…) macro

Normally, PHP clears this Session state when upload parsing finishes or when the extension’s request-shutdown routine runs. An out-of-memory bailout during parsing can skip the end-of-upload cleanup. The extension shutdown is also skipped if startup has not reached module activation. The flag PG(modules_activated) records whether normal extension initialization completed.

Because the flag is still false, PHP skips extension shutdown but later frees the request memory. Under Apache mod_php, the process can survive this cleanup with Session fields pointing to the freed allocations.

Session cleanup can use pointers from an earlier request

The old callback reset Session state at MULTIPART_EVENT_FILE_START, the start of a file part. Its end event, MULTIPART_EVENT_END, could independently invoke progress cleanup when the request supplied a session ID and progress key, the name identifying the upload-progress record. A multipart request without a file part could therefore reach end-event cleanup without first resetting the old fields.

Cleanup calls php_session_initialize(), which can read the old session-ID pointer. It can then reach php_session_track_init(), whose job includes releasing the previous in-memory session value:

if (!Z_ISUNDEF(PS(http_session_vars))) {

zval_ptr_dtor(&PS(http_session_vars));

}

Z_ISUNDEF tests whether the PS(http_session_vars) value is marked undefined; it does not establish that the memory it refers to still exists. If the old value remains marked as defined, zval_ptr_dtor() attempts to release it through the stale pointer.

If the old allocation has been reused, this destruction acts on unrelated or attacker-influenced bytes as though they were the original Session value. Both requests must reach the same worker. No application code needs to run for this mod_php startup sequence.

With PHP-FPM, short for FastCGI Process Manager, PHP runs in a pool of separate worker processes. A web server such as Nginx passes requests to those workers. Their handling of failed startup differs from mod_php.

PHP-FPM terminates workers after failed request startup

When request startup fails, PHP-FPM ends the response and terminates the affected worker:

if (UNEXPECTED(php_request_startup() == FAILURE)) {

fcgi_finish_request(request, 1);

SG(server_context) = NULL;

php_module_shutdown();

return FPM_EXIT_SOFTWARE;

}

The terminated worker cannot carry stale pointers into another request. If the fatal error occurs after startup succeeds, however, FPM can end the request and keep the worker. Decoding during application execution provides such a path.

PHP-FPM can reuse a decoder context after it has been freed

PHP can save values as a byte sequence and reconstruct them later, for example when loading session data. Reconstructing those values is called unserialization. During this work, PHP keeps track of values already decoded, references between them and temporary values that must be released afterward. This working information forms the decoder’s context.

One decoding operation can start another before it finishes. PHP records that nesting depth in a counter and uses it to decide whether to share an existing context. PHP’s Basic extension provides general-purpose functions, including unserialization. It keeps the decoder’s pointer and nesting count in internal global fields. The C code accesses these fields through the BG(…) macro: BG(unserialize).data holds the context pointer, while BG(unserialize).level holds the nesting count. BG(serialize_lock) controls whether context sharing is permitted.

With sharing permitted, a zero level creates a fresh php_unserialize_data context; a nonzero level reuses the pointer. On normal completion, php_var_unserialize_destroy() frees the context and clears the pointer as the counter reaches zero.

A bailout during application decoding can skip this cleanup. Request shutdown then frees the context, but the worker retains its address and a nonzero nesting count. Upload progress in a later request can start decoding before Basic’s normal reset. The old count sends initialization into the nested-operation branch, where it reuses the freed pointer.

The demonstrated PHP-FPM exploit requires a reachable application decoding operation to leave this context behind, followed by an upload-progress request handled by the same worker.

PHP 8.5 can retain a backtrace reference after freeing its memory

PHP 8.5 can record the sequence of function calls leading to a fatal error. This record, called a backtrace, helps explain where the failure occurred. Like Session data, the record uses memory belonging to the current request, so a surviving worker must clear its reference before that memory is released.

EG accesses globals in PHP’s execution engine.

PHP can release request memory in bulk rather than destroy each allocation separately. In the affected PHP 8.5 code, this fast shutdown skips the backtrace reset performed by the slower branch. A normal non-debug build selects it when is_zend_mm() && !EG(full_tables_cleanup): PHP’s memory manager is in use and a full table cleanup has not been requested. Heap release can then free the backtrace while leaving its global value marked as defined.

Some PHP configuration settings are applied separately for each request. If applying a setting produces a warning before init_executor() resets the engine, error handling can reach the stale backtrace. That happens before the decision to display or hide the message, so turning off error display does not prevent the access. The fatal-backtrace path has demonstrated RCE against mod_php.

Patch analysis

The patch changes when PHP stops a request and when it clears old references. Delayed OOM handling protects partially initialized structures. Resets before input processing and before heap release prevent later requests from using freed data.

In the patch excerpts below are shortened for readability, surrounding context and line numbers are omitted.

Defer the memory-limit error until initialization can finish

Instead of abandoning an allocation immediately at memory_limit, PHP records the error and permits bounded extra allocation. The current operation can finish initializing its data before the engine stops the request.

The usual memory_limit becomes a soft limit: crossing it records an error that must be handled. A second, higher hard limit bounds the additional memory available before the process is terminated.

The allocator removes the old zend_mm_safe_error() path and records the pending failure:

— a/Zend/zend_alloc.c

+++ b/Zend/zend_alloc.c

+   EG(delayed_oom).filename = script_filename;

+   EG(delayed_oom).lineno = script_lineno;

+   EG(delayed_oom).state = ZEND_OOM_STATE_PENDING;

+

+   zend_atomic_bool_store_ex(&EG(vm_interrupt), true);

EG(delayed_oom).state records the pending error. vm_interrupt asks PHP’s virtual machine (VM), which executes PHP instructions, to handle it at an interrupt check, a checkpoint for pending stop requests and other special handling. Allocation continues until PHP reaches a checked exit or the hard limit.

Key allocation and value initialization can now finish before the request stops. The bucket insertion order is unchanged; its safety depends on this new allocation-failure behavior.

Clear old Session state at the start of every multipart request

The extensions’ request-initialization routines are known as RINIT. The reset now happens at multipart start:

— a/ext/session/session.c

+++ b/ext/session/session.c

case MULTIPART_EVENT_START: {

multipart_event_start *data = (multipart_event_start *) event_data;

+

+           /* This runs before RINIT, so we need to clear these early */

+           php_rinit_session_globals();

php_rinit_session_globals() clears the old fields before progress handling begins, including when no file part follows. End-event cleanup can no longer reach the previous request’s Session pointers through the missing file-start reset.

Reset the decoder pointer and nesting count before input processing

The decoder pointer and nesting counter belong to PHP’s Basic extension, so the Session reset does not clear them. Basic’s ordinary initialization ran after automatic input processing, too late for upload-progress decoding. The new php_basic_early_rinit() resets them earlier:

— a/ext/standard/basic_functions.c

+++ b/ext/standard/basic_functions.c

+   BG(serialize_lock) = 0;

+   memset(&BG(serialize), 0, sizeof(BG(serialize)));

+   memset(&BG(unserialize), 0, sizeof(BG(unserialize)));

+   php_url_scanner_early_rinit();

It clears the flag controlling context sharing, the encoder bookkeeping and the decoder bookkeeping, including the old pointer and nesting count. The caller places this work before the server-integration code activates input processing:

— a/main/main.c

+++ b/main/main.c

zend_activate();

+       php_basic_early_rinit();

sapi_activate();

Clearing the pointer and nesting count before input processing prevents the next upload-progress decode from treating the abandoned context as an active nested operation.

Clear backtrace and error references before releasing request memory

The backtrace reset must also cover fast shutdown, which skips individual cleanup before the heap is reclaimed in bulk. The PHP 8.5 patch places these resets after the fast and slow branches converge:

— a/Zend/zend_execute_API.c

+++ b/Zend/zend_execute_API.c

+   /* may be used by zend_error_zstr_at before init_executor */

+   ZVAL_UNDEF(&EG(last_fatal_error_backtrace));

+   EG(record_errors) = false;

+   EG(num_errors) = 0;

+   EG(errors) = NULL;

+   EG(error_handling) = EH_NORMAL;

+   EG(exception) = NULL;

ZVAL_UNDEF() marks the old backtrace value as undefined, so a diagnostic in the next request cannot treat it as an existing value to destroy. The surrounding assignments also clear stored error information, exception state and error-handling mode that an early diagnostic may consult.

Impact and affected versions

Code execution has the permissions of the operating-system account running the worker. That can expose application files, credentials and other data available to that account. Memory disclosure can occur without starting an additional process.

The first fixed upstream releases are:

Branch Fixed release
PHP 8.2 8.2.35
PHP 8.3 8.3.36
PHP 8.4 8.4.27
PHP 8.5 8.5.12

Older runtimes require vendor-specific verification. Operating-system vendors and long-term support providers may apply the fix to an older PHP release without changing its upstream version number. These are backported fixes. Check the full package revision against the vendor advisory.

Mitigation and detection

Find the PHP installations behind each site and service, including isolated application containers and products that bundle PHP. Install the corrected packages and restart the workers. Check the version serving web requests separately from the command-line PHP executable; they can be different installations.

Temporary configuration changes address specific paths. Disabling Session upload progress prevents the confirmed early session-processing operations. Disabling PHP 8.5 fatal backtraces prevents that record from being created. Removing invalid or deprecated per-request settings avoids warnings that could touch stale error state. Apply these changes in server or startup configuration and restart workers so they take effect before input processing.

The temporary guidance also recommends enforcing memory_limit=-1, which disables PHP’s own memory limit, while arranging the update. Resource limits outside PHP must then contain memory consumption. This avoids predictable failures at the PHP limit but does not prevent underlying allocation failures or other fatal interruptions. Do not substitute RLIMIT_AS or RLIMIT_DATA, operating-system limits on process address space or data memory: they can make allocations fail through the vulnerable path. Only the complete security update covers the vulnerability family.

During investigation, compare unusual multipart requests with memory-limit errors, failed HTTP responses and repeated worker exits or replacements. Logs containing process identifiers (PIDs) can help establish whether related requests reached the same surviving worker. Also review unexpectedly long session cookies, warnings before script execution, and workers launching unexpected programs.

An isolated memory-limit error does not establish exploitation. Application logs may also miss failures before the script starts. Preserve the surrounding web-server logs, process records and file changes so the sequence can be reconstructed.

Quick Heal Protection

All Quick Heal customers are protected from this vulnerability by following signatures:

  • HTTP/CVE-2026-109618!SP.50469
  • HTTP/CVE-2026-109619!SP.50470

Conclusion

The release changes both the interruption and the cleanup order. Operations get bounded room to finish or return through checked exits, and old references are cleared before another request can use them. Deploy the full runtime update, restart the workers and review suspicious activity from the period before patching.

References

  • PHP security announcement
  • CVE-2026-109618 advisory
  • CVE-2026-109619 advisory
  • PHP 8.2 release patch
  • PHP 8.5 release patch
  • Calif technical research
  • Wiz technical research
  • PHP2Shell research overview
  • Ubuntu remediation guidance

Authors

Satyarth Prakash
Vinay Kumar
Adrip Mukherjee

 Previous PostWP2Shell Exploit Chain: Anatomy of a Sophisticated WordPress Atta...
Satyarth Prakash

About Satyarth Prakash

Satyarth Prakash is working as a Security Researcher with Quick Heal Security Labs. His interest lies in threat research and exploring effective solutions through...

Articles by Satyarth Prakash »

Related Posts

  • WP2Shell Exploit Chain: Anatomy of a Sophisticated WordPress Attack

    WP2Shell Exploit Chain: Anatomy of a Sophisticated WordPress Attack

    October 16, 2026
  • CVE-2026-3854: How Git Push Options Became an Internal Metadata Injection Primitive

    October 6, 2026
  • Inside DragonForce: How a Ransomware Cartel’s Payload Actually Runs

    October 1, 2026
Featured Authors
  • Seqrite
    Seqrite

    Seqrite is a leading enterprise cybersecurity solutions provider. With a focus...

    Read more articles by Seqrite
  • Bineesh P
    Bineesh P

    I am a passionate cybersecurity enthusiast and a dedicated writer. With a knack...

    Read more articles by Bineesh P
  • Jyoti Karlekar
    Jyoti Karlekar

    I'm an avid writer who enjoys crafting content about emerging technologies and...

    Read more articles by Jyoti Karlekar
  • Sanjay Katkar
    Sanjay Katkar

    Sanjay Katkar is the Joint Managing Director of Quick Heal Technologies...

    Read more articles by Sanjay Katkar
Topics
apt (25) Cyber-attack (36) cyber-attacks (58) cyberattack (16) cyberattacks (15) Cybersecurity (344) cyber security (34) Cyber threat (33) cyber threats (51) data breach (56) data breaches (29) data loss (28) data loss prevention (34) data privacy (17) data protection (35) data security (19) DLP (50) DPDP (14) DPDPA (17) Encryption (16) endpoint security (113) Enterprise security (20) Exploit (13) GDPR (14) malware (76) malware analysis (15) malware attack (23) MDM (27) Microsoft (15) MITRE ATT&CK (14) Network security (26) phishing (30) Ransomware (69) ransomware attack (31) ransomware attacks (31) ransomware protection (17) Seqrite (41) Seqrite Encryption (27) Seqrite EPS (33) Seqrite Services (16) threat detection (15) Threat Intelligence (26) UTM (34) Vulnerability (16) zero trust (13)
Seqrite Labs

Leading enterprise IT security solutions provider simplifying endpoint, data, and network security with best-in-class threat prevention, detection, and response solutions worldwide.

Read More About Seqrite

Follow us:

Subscribe To Our Newsletter

Stay informed about the latest cybersecurity trends and insights.

Loading
Products & Services
  • Cloud
  • Endpoint Protection
  • Endpoint Detection and Response
  • Mobile Device Management
  • BYOD
  • Extended Detection and Response
  • Zero Trust Network Access
  • Data Privacy
  • On Prem
  • Endpoint Protection
  • Endpoint Detection and Response
  • Data Privacy
  • Platform
  • Malware Analysis Platform
  • Micro Business
  • SOHO Total Edition
  • Services
  • Threat Intel
  • Digital Risk Protection Services (DRPS)
  • Ransomware Recovery as a Services (RRaaS)
  • DPDP Compliance
  • Managed Detection and Response
  • Cybersecurity & Data Privacy Awareness
Resources
  • Blogs
  • Whitepapers
  • Datasheets
  • Threat Reports
  • Manuals
  • PoV
  • Understanding Data Privacy
  • DPDP Dialogues
  • Policy & Compliance
  • EULA
  • GoDeep.AI
  • SIA
  • Privacy Hour
Contact Us
  • Registered Offices
  • Let’s Talk Cybersecurity
Support
  • Technical Support
  • Download Software
  • Offline Updater
  • Firmware Upgrades
  • Upgrades
  • Product Documentation
About Us
  • About Seqrite
  • Leadership
  • Awards & Recognition
  • Newsroom
Partner
  • Partner Program
  • Locate Partner
  • Become A Partner
  • Seqrite Certification

© 2026 Quick Heal Technologies Ltd.

Sitemap Privacy Policies Legal Notices Cookie Policies Terms Of Use