Reports
MEDIA[CORRUPCIÓN DE MEMORIA]#3896671

MariaDB Connector/C: lectura fuera de límites en unpack_fields() con metadatos de columna truncados

Un servidor malicioso podía enviar al cliente un campo de metadatos de columna de menos de 12 bytes y provocar que unpack_fields() leyera memoria fuera del búfer, con fuga limitada de memoria o caída del proceso.

Resumen
Resumen en castellano de un reporte público, no una traducción literal. El código y los comandos se mantienen como en el original.

Resumen

MariaDB Connector/C, la biblioteca cliente en C de MariaDB, confiaba en que el servidor enviara completos los metadatos de cada columna. La función unpack_fields(), en libmariadb/mariadb_lib.c, lee 12 bytes de cabecera fija por cada descriptor de columna (juego de caracteres, longitud, tipo, flags y decimales) sin comprobar antes que ese campo exista ni que tenga al menos ese tamaño. Si el servidor manda un campo de 0 a 11 bytes, el cliente lee más allá de los datos copiados: una lectura fuera de límites (out-of-bounds read) en el proceso cliente.

El fallo lo envió a HackerOne mschorm, que indica que el hallazgo original es de Aisle Research, comunicado a Red Hat mediante un proceso de embargo, y pide que se atribuya a ese equipo.

Este es el bloque que da por hecho los 12 bytes:

p= (char *)row->data[i];
/* filler */
field->charsetnr= uint2korr(p);
p+= 2;
field->length= (uint) uint4korr(p);
p+= 4;
field->type= (enum enum_field_types)uint1korr(p);
p++;
field->flags= uint2korr(p);
p+= 2;
field->decimals= (uint) p[0];
p++;
/* filler */
p+= 2;

El origen del problema está un paso antes, en mthd_my_read_rows(), que carga las filas recibidas. Acepta cualquier longitud de campo que no sea NULL_LENGTH y copia exactamente esos bytes:

if ((len=(ulong) net_field_length(&cp)) == NULL_LENGTH) {
    cur->data[field] = 0;
} else {
    cur->data[field] = to;
    if (len > (ulong)(end_to - to) || to > end_to) {
        free_rows(result);
        ...
        return(0);
    }
    memcpy(to,(char*) cp,len); to[len]=0;
    ...
}

La comprobación que hay ahí protege el búfer de la fila durante la copia, pero no impone el mínimo de 12 bytes que después presupone unpack_fields().

unpack_fields() se llama desde tres sitios, y los tres procesan paquetes que llegan directamente del servidor por TCP a través de ma_net_safe_read():

  • mysql_read_query_result() (línea 3058)
  • mysql_list_fields() (línea 3443)
  • mysql_list_processes() (línea 3475)

Están afectadas todas las versiones hasta la rama 3.4 actual, incluida la 3.4.9; se verificó en el commit a4a04c52, 82 commits posterior a la etiqueta v3.4.9. El código no depende de la plataforma, así que afecta a todas.

El reporte señala además que el propio código ya tiene el patrón adecuado para estos casos: ma_check_buffer_boundaries() (línea 162) es una función auxiliar dedicada a la lectura con límites que se usa 8 veces en el seguimiento de sesión, y CONC-835 (3edd223c) añadió una validación parecida de datos del servidor en el reensamblado de paquetes.

Pasos de reproducción

  1. Compilar Connector/C con AddressSanitizer.
  2. Preparar un servidor modificado, o un intermediario que altere el protocolo, que devuelva metadatos de definición de columna normales en todo salvo en el campo de cabecera fija, codificado con longitud 0 (o cualquier valor de 1 a 11) en lugar de los 12 bytes o más esperados.
  3. Conectar un cliente y lanzar cualquier consulta que provoque el análisis de los metadatos.
  4. AddressSanitizer informa de una lectura heap-buffer-overflow en unpack_fields(), en el bloque que interpreta la cabecera fija.

Impacto

El ataque va contra el cliente y exige que este se conecte a un servidor malicioso o comprometido, o que el atacante esté en posición de intermediario y pueda manipular el tráfico del protocolo. Cumplida esa condición, no hace falta nada más: todos los tipos de consulta pasan por el análisis de metadatos, de modo que cualquier conexión correcta a un servidor hostil sirve.

Las consecuencias descritas son dos:

  • Lectura fuera de límites en el cliente, con una divulgación limitada de la memoria contigua del proceso.
  • Caída del proceso cliente (fallo de segmentación, o aborto si está compilado con ASan).

Remediación

El reporte figura como resuelto. La corrección que propone consiste en validar la longitud de los datos del campo antes de interpretar la cabecera fija, y abortar con error si el campo falta o tiene menos de 12 bytes:

diff --git a/libmariadb/mariadb_lib.c b/libmariadb/mariadb_lib.c
index 292f1db6..935a1280 100644
--- a/libmariadb/mariadb_lib.c
+++ b/libmariadb/mariadb_lib.c
@@ -1203,6 +1203,18 @@ unpack_fields(const MYSQL *mysql,
     }

     p= (char *)row->data[i];
+    /* fixed-header field must hold at least 12 bytes */
+    if (!p)
+      goto error;
+    {
+      size_t field_len;
+      if (i + 1 <= data->fields && row->data[i + 1])
+        field_len= (char *)row->data[i + 1] - p - 1;
+      else
+        field_len= 0;
+      if (field_len < 12)
+        goto error;
+    }
     /* filler */
     field->charsetnr= uint2korr(p);
     p+= 2;

El reporte no indica si el parche que finalmente se aplicó es este mismo ni en qué versión se publicó.