Reports
MEDIA[CORRUPCIÓN DE MEMORIA]#3891337

MariaDB Connector/ODBC: SQLGetDescRec trata un descriptor como sentencia y escribe fuera de su bloque en el heap

SQLGetDescRec y SQLGetDescRecW limpiaban el error con la macro de sentencias sobre un descriptor: 13 bytes escritos fuera del bloque en cada llamada y, con un servidor hostil o un MITM, una escritura cuyo desplazamiento elige el servidor.

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 está en el cliente, no en el servidor: en MariaDB Connector/ODBC, el driver que cargan las aplicaciones para hablar con MariaDB por ODBC. Las funciones SQLGetDescRec y SQLGetDescRecW reciben un descriptor (SQLHDESC), pero al empezar limpian su registro de error con la macro pensada para sentencias. Es una confusión de tipos (CWE-843): el driver trata un objeto MADB_Desc como si fuera un MADB_Stmt, que es más grande y tiene el campo de error en otra posición.

El error está en driver/odbc_3_api.cpp, líneas 855 y 874:

SQLRETURN SQL_API SQLGetDescRec(SQLHDESC DescriptorHandle, ...)
{
  CHECK_STMT_CLEAR_ERROR(DescriptorHandle);
  return MADB_DescGetRec(DescriptorHandle, ...);

Justo encima, SQLGetDescField (línea 824) y SQLGetDescFieldW (línea 837) hacen lo correcto con el mismo parámetro: usan CHECK_DESC_CLEAR_ERROR.

Por qué acaba escribiendo fuera de la memoria reservada

CHECK_STMT_CLEAR_ERROR termina en MA_ClearError(SQL_HANDLE_STMT, handle), que ejecuta MADB_CLEAR_ERROR(&((MADB_Stmt*)Handle)->Error) en driver/ma_api_internal.cpp:58. Según la información de depuración del driver compilado:

sizeof(MADB_Desc)                 = 720
offsetof(MADB_Desc,  Error)       = 96
offsetof(MADB_Stmt,  Error)       = 192
offsetof(MADB_Error, SqlErrorMsg) = 24
offsetof(MADB_Error, SqlState)    = 537

Es decir, el driver interpreta desc + 192 como una estructura MADB_Error. La macro (driver/ma_error.h:166) hace dos escrituras:

#define MADB_CLEAR_ERROR(a) do { \
  (a)->SqlState[0]= (a)->SqlState[1]= (a)->SqlState[2]= (a)->SqlState[3]= (a)->SqlState[4]= '0'; \
  (a)->SqlErrorMsg[(a)->PrefixLen]= 0; \
  1. Desbordamiento fijo. Los cinco bytes de SqlState caen en desc + 729 a desc + 733, más allá de los 720 bytes reservados. Ocurre en todas las llamadas, sin que el servidor intervenga.
  2. Escritura con desplazamiento controlado por el servidor. PrefixLen se lee de desc + 192, que en realidad está dentro del buffer Error.SqlErrorMsg del propio descriptor, en el índice 72. Ese buffer lo rellena MADB_DescInit (driver/ma_desc.cpp:96) mediante MADB_PutErrorPrefix con "[ma-<version>][" + mysql_get_server_info() + "]". El prefijo fijo ocupa 12 bytes, así que si el servidor anuncia una versión de 61 bytes o más, los bytes 72-79 los decide él, y se usan como el size_t donde se escribe un cero.

Además, con un DSN por defecto el driver no exige TLS: solo cifra si se fija alguna opción SSL explícitamente (driver/ma_connection.cpp:778). En una conexión en claro, quien esté en medio puede manipular el saludo inicial (handshake).

El investigador lo confirmó sobre master en el commit 4e175d9 (MARIADB_ODBC_VERSION 03.02.0010), contra MariaDB 11.4.12 y unixODBC 2.3.14, sin modificar el directorio driver/.

Pasos de reproducción

  1. Compilar el driver desde master (4e175d9) con AddressSanitizer:
cmake .. -DCMAKE_C_FLAGS="-fsanitize=address -g" -DCMAKE_CXX_FLAGS="-fsanitize=address -g" -DWITH_SSL=OPENSSL -DWITH_EXTERNAL_ZLIB=OFF -DWITH_UNIT_TESTS=OFF && make
  1. Conectar a cualquier servidor MariaDB con un DSN sin cifrado, por ejemplo:
DRIVER=<built libmaodbc.so>;SERVER=127.0.0.1;PORT=3306;UID=root;PWD=...;DATABASE=test;SSLVERIFY=0;
  1. Reservar un descriptor explícito y consultar un registro:
SQLAllocHandle(SQL_HANDLE_DESC, dbc, &desc);
SQLGetDescRec(desc, 1, name, sizeof(name), &namelen, &type, &subtype, &length, &prec, &scale, &nullable);

AddressSanitizer informa de un desbordamiento de heap en escritura, 13 bytes después del bloque de 720 bytes:

==15407==ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 1
    #0 MA_ClearError driver/ma_api_internal.cpp:58
    #1 SQLGetDescRec driver/odbc_3_api.cpp:855

0x7305ae4e185d is located 13 bytes after 720-byte region [0x7305ae4e1580,0x7305ae4e1850)
allocated by MADB_DescInit driver/ma_desc.cpp:92
  1. Repetir el paso 3 con una compilación normal, sin sanitizer, contra el mismo servidor legítimo: el bloque vecino del heap queda corrupto y glibc aborta el proceso (código de salida 134).
  2. Arrancar el MITM del investigador (mitm_version.py, unas 100 líneas, que solo reescribe la cadena de versión del handshake y reenvía el resto sin tocarlo) y repetir el paso 4 apuntando el DSN al puerto 14001:
mitm_version.py 14001 3306 100

Con una versión de 100 bytes cuyos ocho bytes en el índice 72 son MMMMMMMM, el proceso cae así:

Program received signal SIGSEGV
MA_ClearError (...) at driver/ma_api_internal.cpp:58
=> 0x7ffff7ca11a8 <MA_ClearError+152>:	mov    BYTE PTR [rsi+rax*1+0xd8],0x0
rax            0x4d4d4d4d4d4d4d4d
rsi            0x5555555e53f0

rsi es el descriptor, 0xd8 (216) es offsetof(MADB_Stmt,Error) + offsetof(MADB_Error,SqlErrorMsg), y rax son exactamente los ocho bytes que puso el MITM.

  1. Control: cambiar solo las dos macros a CHECK_DESC_CLEAR_ERROR en driver/odbc_3_api.cpp:855 y :874, recompilar y repetir los pasos 3 a 5. AddressSanitizer no informa de nada y el driver normal, a través del mismo MITM, termina con código 0.

Impacto

Corrupción de memoria en el proceso cliente que carga el driver:

  • Cualquier aplicación que llame a SQLGetDescRec o SQLGetDescRecW escribe 13 bytes fuera del bloque en cada llamada. En una compilación normal basta para que glibc aborte el proceso, incluso con un servidor legítimo.
  • Un servidor malicioso, o alguien en medio de una conexión sin TLS, elige el desplazamiento de una segunda escritura (un byte a cero) relativa al descriptor. Como esos bytes tienen que ser caracteres válidos dentro de una cadena de versión, los desplazamientos posibles son grandes y poco precisos: lo demostrado es una caída provocada en remoto con corrupción de memoria, no una escritura dirigida a un punto concreto.
  • No hace falta autenticarse en el servidor real, porque el handshake se procesa antes de que termine la autenticación.

El investigador lo valoró con CVSS 3.1 AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:H = 6.5 (Media); la complejidad alta refleja las dos condiciones previas: que la aplicación use SQLGetDescRec y, para la escritura influida por el servidor, una posición en la red o un servidor hostil. El reporte se envió el 26 de julio de 2026, quedó resuelto y se divulgó el 4 de noviembre de 2026.

Remediación

La corrección propuesta, y la que el investigador probó en su control, es usar CHECK_DESC_CLEAR_ERROR en SQLGetDescRec y SQLGetDescRecW, igual que en SQLGetDescField y SQLGetDescFieldW. Con ese cambio desaparecen las dos escrituras indebidas.

Qué aprender de este caso

  • En APIs con manejadores opacos (SQLHANDLE, void*), compara cada función con sus vecinas que reciben el mismo tipo de manejador: aquí SQLGetDescField usaba la macro de descriptor y SQLGetDescRec, unas líneas más abajo, la de sentencia. Ese tipo de incoherencia se encuentra con un simple grep de las macros de comprobación.
  • Cuando un cast a otra estructura no falla de forma visible, mira los offsetof: un campo que en el tipo real cae dentro de un buffer rellenado con datos externos (aquí, la versión del servidor) convierte un error de tipos en una escritura con desplazamiento controlado.
  • Las rutas de limpieza de errores también escriben en memoria; compilar con AddressSanitizer y ejercitar funciones poco usadas, como las de descriptores explícitos, saca a la luz fallos que una compilación normal solo muestra como caídas aleatorias.
  • En un cliente de base de datos, no exigir TLS por defecto amplía el atacante posible de "servidor malicioso" a "cualquiera en la red": conviene que los DSN de producción fuercen el cifrado y la verificación del certificado.