FlawOpen

flawopen.com/Incidents/postgresql-psql-injection-beyondtrust-treasury-december-2024

● PostgreSQL / BeyondTrust · CVE-2025-1094 · December 2024 – February 2025 · CVSS 8.1 · Alta
Investigación · FlawOpen

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.

💡 Explicación en Lenguaje Sencillo (ELI5)

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

  • Referencia CWE para la debilidad descrita en esta guía: CWE-89 CWE-149

Cronología del Incidente

2024-12-16

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.

2024-12-19

CISA añade CVE-2024-12356 a su catálogo Known Exploited Vulnerabilities.

2025-02-13

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

Paso 1

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.

Paso 2

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.

Paso 3

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.

Paso 4

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.

IMPLEMENTACIÓN VULNERABLE
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
PARCHE SEGURO Y ROBUSTO
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

Fuentes