- URL: https://www.laruence.com/en/2012/02/08/2528.html
- Please include attribution when republishing.
Remember the PHP Hash Collisions Ddos vulnerability I mentioned before? At first, the fix the dev group came up with was: if it exceeds max_input_vars, raise an error (E_ERROR), which in turn causes PHP to error out. Later, to solve this more lightly, we improved it to: if it exceeds max_input_vars, just issue a warning (E_WARNING) and stop adding to the destination array, but let the flow continue. Then we released 5.3.9.
This new fix had good intentions, but it introduced a serious problem (already fixed in 5.3.10). This problem was first discovered by Stefan Esser. Please look at the final (5.3.9) fix before (php_register_variable_ex):
while (1) {
if (zend_symtable_find(symtable1, escaped_index, index_len + 1, (void **) &gpc_element_p) == FAILURE
|| Z_TYPE_PP(gpc_element_p) != IS_ARRAY) { //(3)
if (zend_hash_num_elements(symtable1) <= PG(max_input_vars)) { // (4)
if (zend_hash_num_elements(symtable1) == PG(max_input_vars)) {
php_error_docref(NULL TSRMLS_CC, E_WARNING, "Input variables exceeded %ld. ...", PG(max_input_vars)); // (1)
}
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);
}
//......
}
//.....
symtable1 = Z_ARRVAL_PP(gpc_element_p); // (2)
goto plain;
}
Notice that if at this point you're registering an array variable (in GET, something like: a[]=2), and this variable happens to be exactly the max_input_vars-th variable, then a warning (1) is triggered, and everything is normal at this point.
However, if at this point you're still registering an array variable, but this variable is already the max_input_vars + 1-th variable, then gpc_element_p becomes an uninitialized pointer. And because the logic continues now, it flows through to position (2), causing a dereference of an uninitialized pointer. So, Boom~
So, up to this point, we can use this property to DoS 5.3.9. If the Server has Core Dump enabled, this effect is very obvious.
However, this problem can also lead to an even more serious issue:
Still with the code above, there's a loop in the outermost layer. This loop comes into play when registering pairs like a[b]=2 — the loop will execute twice: the first time inserts a[], the second time inserts b into a[]. Then let's pay attention again to (3): if a wanted element can't be found in the destination array, **or this element is not an array**, it also leads directly to the flow falling through to (2), and that's where the problem appears.
For a POST string like this (the default max_input_vars is 1000):
1=1&1=2&..........&999=1&x="I am a malicious string"&x[0]=0
What happens?
Let me describe it step by step:
1. From 1 to 999, there's no problem — they all get inserted normally.
2. x is the 1000th element, so a warning is triggered, no problem, x gets inserted.
3. When inserting x[0], the (3) statement finds it's not an Array and enters the if body, but at this point the (4) statement fails, so the flow ultimately falls through to (2).
4. At this point, gpc_element_p points to x — that is, the string we forged....
Now let's look at the key data structure, zval:
struct _zval_struct {
/* Variable information */
zvalue_value value; /* value */
zend_uint refcount__gc;
zend_uchar type; /* active type */
zend_uchar is_ref__gc;
};
Then look at zvalue_value:
typedef union _zvalue_value {
long lval; /* long value */
double dval; /* double value */
struct {
char *val;
int len;
} str;
HashTable *ht; /* hash table value */
zend_object_value obj;
} zvalue_value;
zvalue_value is a union, so the memory of the string region we construct will be treated as a Hashtable struct:
typedef struct _hashtable {
uint nTableSize;
uint nTableMask;
uint nNumOfElements;
ulong nNextFreeElement;
Bucket *pInternalPointer; /* Used for element traversal */
Bucket *pListHead;
Bucket *pListTail;
Bucket **arBuckets;
dtor_func_t pDestructor; //note this one
zend_bool persistent;
unsigned char nApplyCount;
zend_bool bApplyProtection;
#if ZEND_DEBUG
int inconsistent;
#endif
} HashTable;
In the Hashtable struct, there's a pDestructor. This pointer points to a function, which gets called when an element in this Hashtable is to be cleared...
In other words, you can set an address (pDestructor) however you like, and then make PHP call it (by luring an element to be deleted).
However, up to this point, because most of our operating systems have ASLR+NX protection, so this can only execute arbitrary code in theory, but in practice, up to now, this vulnerability cannot actually be exploited.
Finally, the Patch I previously provided for 5.2, because it adopts triggering E_ERROR, does not have this vulnerability. If you've already used my previous Patch, you can rest assured. Also thanks to Wingary for the help during my process of understanding this vulnerability 🙂
One last thing: I've finished introducing the principle. An introduction or discussion of the POC goes beyond the intent of this article. I won't cover it anymore — those interested can find it themselves.
Be First to Comment