One Security Fix Introduces Another

Today, Stefan Esser (@i0n1c) reported a critical remotely exploitable vulnerability in PHP 5.3.9 (update assigned CVE-2012-0830). The funny thing is that this vulnerability was introduced in the fix for the hash collision DOS (CVE-2011-4885) reported in December.



The Vulnerable Fix

The fix to prevent hash collisions introduces a new configuration property in php.ini called

max_input_vars

This configuration element limits the number of variables that can be used in a request (e.g. . The default is set to 1000.

The changes were made to php_variables.c in the function php_register_variable_ex.

PHP starts off by “registering” all variables in a request through this function

if (sapi_module.input_filter(PARSE_POST, var, &val, val_len, &new_val_len TSRMLS_CC)) { php_register_variable_safe(var, val, new_val_len, array_ptr TSRMLS_CC); }

Which in turn calls the vulnerable function

PHPAPI void php_register_variable_ex(char *var_name, zval *val, zval *track_vars_array TSRMLS_DC) { ...tons of code removed... if (is_array) { ... code removed ... if (zend_hash_num_elements(symtable1) <= PG(max_input_vars)) { if (zend_hash_num_elements(symtable1) == PG(max_input_vars)) { php_error_docref(NULL TSRMLS_CC, E_WARNING, "Input variables exceeded %ld. To increase the limit change max_input_vars in php.ini.", PG(max_input_vars)); } MAKE_STD_ZVAL(gpc_element); array_init(gpc_element); zend_symtable_update(symtable1, escaped_index, index_len + 1, &gpc_element, sizeof(zval *), (void **) &gpc_element_p); } ... some code removed... symtable1 = Z_ARRVAL_PP(gpc_element_p); ... tons more code removed ... }

The vulnerability happens when the number of variables exceeds max_input_vars and the variable is an array variable (if (*p == ‘[‘)). Instead of an else case which would stop and return, the code is allowed to continue executing. The code continues to execute up to line 207, the second highlighted line. At line 207, it is calling a macro Z_ARRVAL_PP to get a reference to the updated hashtable. This is where the code execution can occur.

If we look a little further down, gpc_element is eventually assigned the value of the variable.

plain_var: MAKE_STD_ZVAL(gpc_element); gpc_element->value = val->value;

When the code reaches the point where the number of variables is greater than max input, gpc_element_p will point to the previous variable value. Z_ARRVAL_PP will expect the value to be a ZVAL struct with a hashtable (usually initialized by array_init(…)) but that memory hasn’t been initialized in the case were the previous value was a non-array (plain) variable.

The Ironic Part

The most ironic thing about all of this is that because this fix was for a security vulnerability, it was backported and applied more likely than a regular bug fix or update.

Update 1: PoC

A very nice PoC by Paul Westin

Update 2: Fixed Released

PHP 5.3.10 released – UPDATE NOW!