Description:
------------
mysqlnd extension assumes the `flags` returned for a BIT field necessarily contain UNSIGNED_FLAG; this might not be the case, with a rogue mysql server, or a MITM attack.
In php_mysqlnd_rowp_read_text_protocol_aux (mysqlnd_wireprotocol.c) BIT fields are treated specially and use extra space pre-allocated for that processing at the end of `row_buffer` (see the comment there).
The size of that extra space that is allocated is calculated in mysqlnd_res_meta::read_metadata (in mysqlnd_result_meta.c), and depends on the number of BIT fields and their respective size in bytes. For a BIT(8) field, which has 8 bits, the function reserves 3 bytes. This is fine if the field is UNSIGNED (which BIT values should always be with a normal mysql server), however if the server returns BIT fields without the UNSIGNED_FLAG set, a BIT(8) will then be interpreted as signed, and can contain negative values such as -127, which no longer fit the reserved space:
To read BIT values off a row_buffer, php_mysqlnd_rowp_read_text_protocol_aux uses the generic function ps_fetch_from_1_to_8_bytes (mysqlnd_ps_codec.c) which starts exactly with a check of the UNSIGNED_FLAG but is not aware of whether it is processing a BIT field.
Thus, a malicious mysql server or MITM can return field metadata for BIT fields that does not contain the UNSIGNED_FLAG, leading to a heap overflow.
Tested in 5.6.x and latest packaged PHP 7.0.7, but should affect a lot more versions. Affects queries through mysql / mysqli / anything that uses mysqlnd.
To simulate a rogue mysql server apply the following patch to mysqlnd_ps_codec.c before running the test case:
< if (field->flags & UNSIGNED_FLAG) {
> if (field->flags & UNSIGNED_FLAG && field->type != MYSQL_TYPE_BIT) {
Test script:
---------------
<?php
/*
Please setup the following database/table:
CREATE DATABASE php; USE php;
CREATE TABLE `php` (`moo` bit(8) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
INSERT INTO `php` VALUES (0x81); # -127 when signed
*/
$link = mysqli_connect('127.0.0.1', 'root', '', 'php');
if (!$link) die("Cannot connect");
$s = str_repeat("moo,", 60000); /* can play with this value a bit to see different corruption */
$result = mysqli_query($link, "SELECT $s 1 FROM php");
while($row = mysqli_fetch_row($result)) { $v = print_r($row, true); /* just to exercise heap */};
mysqli_close($link);
?>