Reports
MEDIA[CORRUPCIÓN DE MEMORIA]#3871977

MariaDB Connector/ODBC: una versión de servidor demasiado larga desbordaba el búfer de errores del cliente

Un servidor MariaDB malicioso (o un atacante en medio) podía anunciar una cadena de versión de más de ~500 bytes que desbordaba un contador size_t del driver ODBC y provocaba escrituras fuera del búfer de errores en el cliente.

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

El fallo estaba en el lado del cliente: en MariaDB Connector/ODBC, el driver que usan muchas aplicaciones (herramientas BI, procesos ETL, servidores de aplicaciones, isql...) para conectarse a servidores MariaDB y MySQL. Cada mensaje de diagnóstico que genera el driver lleva un prefijo con la versión del servidor, y esa versión la elige el propio servidor durante el saludo inicial (handshake).

El problema es un desbordamiento de entero. El prefijo se construye en MADB_PutErrorPrefix con una función _snprintf propia del driver que, al truncar la salida, devuelve un valor negativo. Ese valor se suma a error->PrefixLen, que es un size_t (sin signo). Si el servidor anuncia una versión de más de unos 500 bytes, la salida se trunca, el retorno es negativo y al sumarlo PrefixLen «da la vuelta» hasta un número enorme (cerca de 2^64). A partir de ahí, cada vez que el driver usa PrefixLen como desplazamiento dentro del búfer fijo de 513 bytes SqlErrorMsg, escribe fuera de él, en una posición hacia atrás que elige el atacante según la longitud de la cadena de versión.

El investigador lo confirmó de extremo a extremo con AddressSanitizer contra el driver real compilado desde el commit 4e175d9 (versión 3.2.10).

Causa raíz

El fallo sale de tres piezas que encajan:

1. La _snprintf propia devuelve negativo al truncar (driver/ma_platform_posix.cpp:41):

int _snprintf(char *buffer, size_t count, const char *format, ...) {
    int result = vsnprintf(buffer, count, format, list);
    if (result > count) { return count - result - 1; }   // NEGATIVO
    return result;
}

2. MADB_PutErrorPrefix suma ese negativo a un size_t (driver/ma_error.cpp:155-170):

error->PrefixLen = strlen(MARIADB_ODBC_ERR_PREFIX);   // "[ma-3.2.10]" ~ 11
strcpy_s(error->SqlErrorMsg, SQL_MAX_MESSAGE_LENGTH + 1, MARIADB_ODBC_ERR_PREFIX);
if (dbc != NULL && dbc->mariadb != NULL) {
  error->PrefixLen += _snprintf(error->SqlErrorMsg + error->PrefixLen,
    SQL_MAX_MESSAGE_LENGTH + 1 - error->PrefixLen,
    "[%s]", mysql_get_server_info(dbc->mariadb));
    // ^^ cadena de versión del handshake, controlada por el servidor y sin tope
}

Con una versión de longitud L mayor de ~500, el retorno de _snprintf es negativo y PrefixLen += <negativo> se envuelve hasta aproximadamente 2^64 - (L - 510). La distancia hacia atrás la decide el atacante con la longitud de la cadena. El contador afectado, PrefixLen, es de tipo size_t (driver/ma_c_stuff.h:70).

3. Dos puntos escriben fuera de límites usando ese valor corrupto:

  • MADB_CLEAR_ERROR / MA_ClearError (driver/ma_error.h:168, driver/ma_api_internal.cpp:58): SqlErrorMsg[PrefixLen] = 0; — escritura de un byte fuera de límites.
  • MADB_SetNativeError (driver/ma_error.cpp:231,235): strcpy_s(SqlErrorMsg + PrefixLen, 513 - PrefixLen, msg) — escritura de varios bytes, con texto de error que también viene del servidor.

Pasos de reproducción

Entorno: Linux x86-64, unixODBC 2.3.14, driver compilado desde HEAD 4e175d9 con AddressSanitizer (cmake -DWITH_ASAN=ON). Los ficheros citados van adjuntos en el reporte original.

Paso 0 — Comprobar que libmariadb expone la versión larga (direct_version_check.c):

$ ./direct_version_check 3402   # servidor falso con version_len=2000
server_info_len=2000  first16=AAAAAAAAAAAAAAAA

mysql_get_server_info() devuelve la versión completa de 2000 bytes del handshake, sin recortar.

Paso 1 — Arrancar el servidor falso (rogue_server.py):

python3 rogue_server.py 3404 4000 &   # puerto 3404, cadena de versión de 4000 bytes

El servidor habla lo justo del protocolo de MariaDB para que la conexión tenga éxito y anuncia un server_version de 4000 bytes en el handshake.

Paso 2 — Caso positivo (ataque): driver real compilado con ASAN + unixODBC real (odbc_client.c):

./odbc_client "DRIVER=.../build_asan/driver/libmaodbc.so;SERVER=127.0.0.1;PORT=3404;UID=u;PWD=p;SSLVERIFY=0;"

Salida:

SQLDriverConnect r=0                        <- la conexión TIENE ÉXITO
allocating statement (MADB_StmtInit -> PutErrorPrefix)
==ERROR: AddressSanitizer: heap-buffer-overflow ... WRITE of size 1
    #0 MA_ClearError(short, void*)  driver/ma_api_internal.cpp:58
    #1 SQLGetStmtAttr               driver/odbc_3_api.cpp:997
    #2 libodbc.so.2                 (unixODBC Driver Manager)
    #3 main                         odbc_client.c:32

La escritura fuera de límites salta al reservar el statement (MADB_StmtInit, driver/ma_statement.cpp:4113). No hace falta que la aplicación lance ninguna consulta.

Paso 3 — Control negativo: mismo camino, pero con una versión normal de 20 bytes:

python3 rogue_server.py 3405 20 &
./odbc_client "DRIVER=...;SERVER=127.0.0.1;PORT=3405;UID=u;PWD=p;SSLVERIFY=0;"

Salida:

SQLDriverConnect r=0
SQLAllocHandle(STMT) r=0
SQLExecDirect r=0 (survived)
clean exit    EXIT=0

Con una versión de longitud normal no hay corrupción. La única diferencia es la longitud de la cadena de versión. El reporte incluye además run_poc.sh, que compila el driver con ASAN y ejecuta los casos positivo y negativo de forma automática.

Impacto

Un servidor MariaDB/MySQL malicioso —o un atacante capaz de interceptar la conexión— corrompe la memoria del proceso cliente (escritura fuera de límites) en cualquier aplicación que use MariaDB Connector/ODBC, en cuanto abre la conexión y reserva un statement. El efecto demostrado es una caída fiable del proceso (denegación de servicio), confirmada por AddressSanitizer como desbordamiento de montículo.

El desplazamiento de la escritura lo elige el atacante con la longitud de la versión, y el punto strcpy_s de MADB_SetNativeError copia contenido influido por el servidor (el texto de error) a esa posición elegida, por lo que en compilaciones sin protecciones es plausible la corrupción de objetos vecinos del montículo —y un impacto mayor que el de simple DoS—. Aquí solo se demostró la caída.

Alcance: toda aplicación que conecte a través de MariaDB Connector/ODBC. Condiciones previas: (a) que el cliente conecte a un servidor que el atacante controla o puede interceptar (el driver va en claro por defecto, sin TLS), y (b) que la aplicación reserve un statement, algo que hace cualquier flujo ODBC. No hacen falta credenciales, privilegios ni interacción humana más allá de iniciar la conexión.

Remediación

El reporte apunta a la raíz: la _snprintf propia del driver devuelve un valor negativo al truncar, y ese valor se suma a un contador size_t sin comprobar el signo, lo que provoca el desbordamiento. El investigador también señala que el mismo patrón peligroso (retorno negativo de _snprintf sumado sin control) aparece en MADB_DsnToString (la ruta del cuadro de diálogo en Windows) y en los constructores de consultas de catálogo, y recomienda revisarlos como parte del mismo arreglo. El estado del reporte es Resuelto.

Qué aprender de este caso

  • En código C/C++, desconfía de sumar el retorno de un *snprintf a una variable sin signo (size_t): las implementaciones que devuelven negativo al truncar hacen que la suma «dé la vuelta» a un número enorme. Revisa cualquier len += snprintf(...) donde len sea size_t y comprueba el signo antes de sumar.
  • Los datos del saludo del servidor (versión, banners) son entrada no confiable igual que cualquier otra: aquí la cadena server_version del handshake llegaba sin tope a un búfer de 513 bytes. Al escribir clientes de protocolos, pon límites a todo campo que venga del otro extremo antes de usarlo como longitud o desplazamiento.
  • Un desbordamiento de un contador de desplazamiento convierte un búfer de tamaño fijo en una escritura a posición arbitraria hacia atrás. Si un size_t que indexa un búfer puede quedar fuera de rango, añade una comprobación explícita de que sigue dentro antes de escribir en buf[offset].
  • El driver conectaba en claro por defecto, lo que permite el ataque por interposición (MITM) sin controlar el servidor. Forzar TLS con verificación en los clientes de base de datos reduce la superficie de este tipo de fallos de parseo del handshake.