Reports
MEDIA[DENEGACIÓN DE SERVICIO]#3871740

DoS en el conector R2DBC de MariaDB: un servidor malicioso fija las iteraciones PBKDF2 de Parsec y bloquea la CPU del cliente

En la autenticación Parsec del conector R2DBC de MariaDB, el servidor decide con un solo byte cuántas rondas de PBKDF2 calcula el cliente. Un servidor falso o un MITM podía tenerlo unos 25 minutos con un núcleo al 100 % por cada conexión.

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: mariadb-connector-r2dbc (versión 1.4.1, HEAD a82d265, y según el investigador todas las versiones publicadas) implementa el plugin de autenticación Parsec, que deriva la clave a partir de la contraseña con PBKDF2-HMAC-SHA512. El número de rondas no lo elige el cliente: lo calcula como 1024 << iterations, donde iterations es un byte que llega en la respuesta ext-salt del servidor.

La única comprobación es que ese byte no supere 20. Con iterations = 20, el cliente tiene que calcular 1024 << 20 = 1.073.741.824 rondas antes de terminar el handshake, lo que supone unos 25 minutos de un núcleo de CPU por cada intento de conexión.

Es una regresión: el commit b7766de cambió el límite de > 3 (máximo de 8.192 rondas) a > 20.

El código afectado está en src/main/java/org/mariadb/r2dbc/authentication/standard/ParsecPasswordPlugin.java:

 64   if (buf.readableBytes() > 2) {
 65     firstByte = buf.readByte();
 66     iterations = buf.readByte();          // fully server-controlled
 67   }
 68
 69   if (firstByte != 0x50 || iterations > 20) {   // only upper bound
 70     throw new R2dbcNonTransientResourceException(...);
 71   }
 ...
104   PBEKeySpec spec = new PBEKeySpec(password, salt, 1024 << iterations, 256);
105   SecretKey key = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512").generateSecret(spec);

Varias condiciones hacen que el ataque sea fácil de montar:

  • El plugin parsec está registrado en META-INF/services y es el servidor quien elige el plugin durante el handshake.
  • restrictedAuth vale null por defecto, así que el cliente no tiene ninguna lista de plugins permitidos y ejecuta parsec siempre que el servidor lo pida.
  • Todo ocurre antes de completar la autenticación.
  • El conector usa por defecto sslMode=DISABLE, es decir, texto plano, de modo que un atacante con posición de MITM en la red puede reescribir ese byte.

Pasos de reproducción

Entorno usado: JDK 21 y el propio proyecto compilado con mvn -o -DskipTests package, que genera r2dbc-mariadb-1.4.1.jar.

Prueba 1: servidor MariaDB falso de extremo a extremo (RogueParsecServer.java, adjunto al reporte original). El servidor habla el protocolo de MariaDB hasta el intercambio ext-salt de Parsec y envía un byte iterations elegido por el atacante; mide cuánto tiempo pasa el MariadbConnectionFactory.create() real calculando PBKDF2. La única diferencia entre la ejecución de control y la de ataque es ese byte.

  1. Compilar y lanzar el servidor falso:
javac -cp r2dbc-mariadb-1.4.1.jar:reactor-core.jar:r2dbc-spi.jar RogueParsecServer.java
java -Djava.security.egd=file:/dev/urandom -cp .:r2dbc-mariadb-1.4.1.jar:reactor-core.jar:r2dbc-spi.jar:reactive-streams.jar:netty-*.jar RogueParsecServer
  1. Comparar la salida del caso de control (iterations=3) con el de ataque (iterations=15):
[neg control] ext-salt iterations=3 (1024<<3 = 8192 rounds):
    server saw client stuck 160 ms ; client create() returned in 1148 ms

[attack] ext-salt iterations=15 (1024<<15 = 33554432 rounds):
    server saw client stuck 47286 ms ; client create() returned in 47604 ms
    at the maximum allowed iterations=20 this is 1024<<20 = 1073741824 rounds -> ~25 min.
    (real driver logs "Authentication success" -- the full handshake completes after the delay)

Con un solo byte distinto, la conexión quedó bloqueada unos 47 segundos frente a 160 ms, 296 veces más. El driver acaba registrando "Authentication success": no falla, simplemente tarda.

Prueba 2: medición directa del coste (ParsecPoc.java, también adjunto). Ejecuta el mismo cálculo de las líneas 104-105 con distintos exponentes y confirma que cada unidad más duplica el tiempo:

iterations   rounds           wall_ms
3            8192             231            <- negative control (old cap > 3)
10           1048576          1518
13           8388608          11745
15           33554432         45113
extrapolation to iterations=20 (rounds=1073741824): ~1503360 ms (~25.1 min)

Llamando al ParsecPasswordPlugin.next() real con iterations=13, el cálculo tardó 11.753 ms, 81 veces más que los 145 ms del control con iterations=3.

Impacto

  • Un servidor MariaDB malicioso, o un atacante en medio de la conexión, puede dejar un núcleo de CPU del cliente ocupado unos 25 minutos por cada intento de conexión, sin credenciales válidas y tantas veces como quiera.
  • Si la aplicación usa un pool de conexiones que reintenta las conexiones fallidas, el efecto se multiplica hasta agotar la CPU de toda la aplicación y provocar una denegación de servicio sostenida.
  • La amplificación es enorme: un byte elegido por el atacante obliga a calcular unos 1.070 millones de rondas de PBKDF2-HMAC-SHA512.
  • Al ser sslMode=DISABLE el valor por defecto, no hace falta controlar el servidor: basta con poder manipular el tráfico de red.

En HackerOne el reporte figura con severidad media. El investigador cita como precedente un reporte similar aceptado sobre mariadb-connector-nodejs (#3835450, también de severidad media).

Remediación

El reporte figura como resuelto en HackerOne. La corrección que propone el investigador es volver a poner un límite pequeño y fijo al exponente de iteraciones (por ejemplo, rechazar iterations > 3), como antes del commit b7766de. El reporte no detalla qué cambio exacto se aplicó finalmente.

Qué aprender de este caso

  • En cualquier cliente de base de datos, revisa los plugins de autenticación que leen parámetros de coste (rondas de PBKDF2, factores de scrypt o bcrypt, tamaños de sal) de la respuesta del servidor: si el exponente o el número de rondas viene del otro extremo sin un tope bajo, el servidor decide cuánta CPU gastas tú.
  • Cuando un valor se usa como desplazamiento (1024 << n), el límite hay que pensarlo en términos del resultado, no del byte: pasar de > 3 a > 20 parece un cambio pequeño y multiplica el coste máximo por más de 130.000.
  • Comprueba qué plugins de autenticación acepta el cliente si el servidor los pide: con una lista de permitidos vacía por defecto (restrictedAuth = null), cualquier plugin registrado en META-INF/services pasa a ser superficie de ataque.
  • Al usar este tipo de conectores, activar TLS con verificación del servidor y fijar los plugins permitidos limita el ataque a quien ya controle el servidor; con sslMode=DISABLE basta con estar en la red.