flawopen.com/Incidents/postgresql-psql-injection-beyondtrust-treasury-december-2024
CVE-2025-1094: inyección SQL en psql de PostgreSQL
UTF-8 no válido eludía el escape de libpq al crear entradas de psql con texto escapado: permitía inyectar SQL o \!. Corregido el 13 feb. 2025.
Imagina un traductor cuyo trabajo es poner entre comillas todo lo que dices para que nunca se confunda con una orden. Descubres que, si le das una palabra en un alfabeto que no reconoce, se lía y se le cae una de las comillas. Lo que dijiste se escapa de las comillas y se lee como una instrucción, aunque el traductor hizo exactamente lo que estaba diseñado para hacer con todas las entradas que alguien había probado.
Conceptos Clave y Términos
PQescapeLiteral- Una función de libpq que pone un valor entre comillas y lo escapa para pegarlo en texto SQL. También estaban afectadas PQescapeIdentifier, PQescapeString y PQescapeStringConn.
UTF-8 no válido- Una secuencia de bytes mal formada en la codificación declarada. El código de escape y psql no coincidían en cuántos caracteres siguientes consumen esos bytes.
Metacomando de psql- Un comando con barra invertida que interpreta el cliente psql y no el servidor. \! ejecuta el resto de la línea como comando del shell del sistema operativo.
CVE-2024-12356- Un fallo crítico de inyección de comandos en Privileged Remote Access y Remote Support de BeyondTrust. En las pruebas de Rapid7, llegar a ejecutar código con él requería
CVE-2025-1094. Parámetro enlazado- Un valor que se envía al servidor separado del texto SQL, de modo que nunca se toma sobre él ninguna decisión de comillas o escape.
Mapeos de marcos
Cronología del Incidente
BeyondTrust publica el aviso BT24-10 sobre CVE-2024-12356 tras investigar el compromiso de algunas instancias SaaS de Remote Support; el Tesoro de EE. UU. informó después de que ese incidente llegó a sus sistemas.
CISA añade CVE-2024-12356 a su catálogo Known Exploited Vulnerabilities.
PostgreSQL publica 17.3, 16.7, 15.11, 14.16 y 13.19 con la corrección de CVE-2025-1094, y Rapid7 publica su análisis.
Flujo de Ataque Paso a Paso
La entrada llega a una función de escape
Una aplicación toma texto en el que influye el atacante y lo pasa a PQescapeLiteral u otra función parecida de libpq para construir una sentencia SQL.
El UTF-8 no válido esconde una comilla
Según el aviso de PostgreSQL, una secuencia multibyte no válida colocada antes de una comilla simple hacía que la rutina de escape dejara esa comilla sin escapar.
psql cierra el literal antes de tiempo
Cuando la sentencia se pasa a psql, la comilla sin escapar cierra el literal de cadena y el resto de la entrada se interpreta como SQL o como comandos de psql.
Un metacomando ejecuta un shell
Rapid7 demostró que el texto inyectado puede incluir el metacomando \!, que psql ejecuta como comando del sistema operativo en la máquina donde corre.
Código Fuente: Vulnerable vs. Seguro
Python ilustrativo con psycopg 3, no es código de BeyondTrust. La versión vulnerable reproduce el patrón de riesgo que cita el aviso: escapar un valor con libpq, insertarlo en el texto SQL y pasar ese texto a psql. La reforzada elimina las dos mitades: rechaza el UTF-8 mal formado antes de procesar nada y envía el valor como parámetro enlazado con un driver real, así que no hay paso de escape que engañar ni intérprete de metacomandos de psql por medio. Actualizar libpq sigue siendo importante para el código que siga escapando.
import subprocess
import psycopg
def lookup_user(conn: psycopg.Connection, name: bytes) -> bytes:
# PQescapeLiteral de libpq (antes de 17.3/16.7/15.11/14.16/13.19) no
# neutralizar una comilla oculta tras una secuencia de bytes UTF-8 no válida.
literal = conn.pgconn.escape_literal(name)
script = b"SELECT id FROM users WHERE name = " + literal + b";\n"
# psql ejecuta la entrada como un script, incluidos metacomandos como \!
result = subprocess.run(["psql", "-At", "-d", "app"], input=script,
capture_output=True, check=True)
return result.stdout
import psycopg
MAX_NAME_CHARS = 128
def lookup_user(conn: psycopg.Connection, name: bytes) -> list[int]:
try:
text = name.decode("utf-8") # reject malformed input at the boundary
except UnicodeDecodeError:
raise ValueError("name is not valid UTF-8") from None
if len(text) > MAX_NAME_CHARS:
raise ValueError("name too long")
with conn.cursor() as cur:
# El valor se envía como parámetro enlazado, separado del texto SQL,
# y no interviene ningún proceso psql (ni, por tanto, un analizador de metacomandos).
cur.execute("SELECT id FROM users WHERE name = %s", (text,))
return [row[0] for row in cur.fetchall()]
Lista de Verificación de Seguridad para Ingeniería
- ✓Busca en el código llamadas a psql mediante subprocess cuya entrada se construya con datos de la aplicación y sustitúyelas por un driver de base de datos con parámetros enlazados; añade una regla de CI que falle ante nuevas llamadas.
- ✓Rechaza en la frontera la entrada mal formada en su codificación declarada, antes de que llegue a cualquier parser, función de escape o llamada a la base de datos; añade una prueba que envíe b'\xc0\'' y espere un error de validación.
- ✓Actualiza libpq y las herramientas cliente, no solo el servidor:
CVE-2025-1094está corregida en PostgreSQL 17.3, 16.7, 15.11, 14.16 y 13.19, y también hay que actualizar los paquetes cliente de los servidores de aplicación. - ✓Donde todavía no se pueda eliminar el escape, añade una prueba de regresión que pase UTF-8 no válido seguido de una comilla por la ruta de escape y compruebe que el resultado se rechaza o queda entrecomillado de forma segura.
- ✓Trata las herramientas de acceso remoto privilegiado como parte de tu superficie de ataque: restringe sus interfaces de gestión, parchéalas en cuanto avise el proveedor y vigila el uso de sus claves de API desde ubicaciones inesperadas.
Fuentes
- PostgreSQL — aviso de seguridad de CVE-2025-1094
- Rapid7 — CVE-2025-1094: inyección SQL en psql de PostgreSQL, hallada al analizar la CVE-2024-12356 de BeyondTrust
- Inyección SQL — referencia completa de la clase en 11 lenguajes
- Inyección de comandos — referencia completa de la clase
- ¿Evitan los procedimientos almacenados la inyección SQL?