FlawOpen

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

● PostgreSQL / BeyondTrust · CVE-2025-1094 · December 2024 – February 2025 · CVSS 8.1 · Haute
Recherche · FlawOpen

CVE-2025-1094 : injection SQL dans psql PostgreSQL

Un UTF-8 invalide contournait libpq lors de la création d’entrées psql avec le texte échappé : injection SQL ou \!. Corrigé le 13 févr. 2025.

💡 Explication en Langage Simple (ELI5)

Imaginez un traducteur chargé de mettre entre guillemets tout ce que vous dites, pour que rien ne soit pris pour un ordre. Vous découvrez que si vous lui donnez un mot dans un alphabet qu'il ne connaît pas, il s'embrouille et oublie un des guillemets. Ce que vous avez dit sort alors des guillemets et est lu comme une instruction, alors que le traducteur a fait exactement ce pour quoi il était conçu avec toutes les entrées testées jusque-là.

Concepts Clés et Termes

PQescapeLiteral
Une fonction de libpq qui met une valeur entre apostrophes et l'échappe pour l'insérer dans du texte SQL. PQescapeIdentifier, PQescapeString et PQescapeStringConn étaient aussi concernées.
UTF-8 invalide
Une suite d'octets mal formée dans l'encodage déclaré. Le code d'échappement et psql n'étaient pas d'accord sur le nombre de caractères suivants que ces octets absorbent.
Méta-commande psql
Une commande avec barre oblique inverse traitée par le client psql et non par le serveur. \! exécute le reste de la ligne comme commande du shell du système.
CVE-2024-12356
Une faille critique d'injection de commandes dans Privileged Remote Access et Remote Support de BeyondTrust. Dans les tests de Rapid7, obtenir une exécution de code par ce biais exigeait CVE-2025-1094.
Paramètre lié
Une valeur envoyée au serveur séparément du texte SQL, si bien qu'aucune décision de guillemets ou d'échappement n'est jamais prise à son sujet.

Correspondances de référentiels

  • Référence CWE pour la faiblesse décrite dans ce guide : CWE-89 CWE-149

Chronologie de l'Incident

2024-12-16

BeyondTrust publie l'avis BT24-10 sur CVE-2024-12356 après enquête sur la compromission de certaines instances SaaS de Remote Support ; le Trésor américain a ensuite indiqué que cet incident avait atteint ses systèmes.

2024-12-19

La CISA ajoute CVE-2024-12356 à son catalogue Known Exploited Vulnerabilities.

2025-02-13

PostgreSQL publie les versions 17.3, 16.7, 15.11, 14.16 et 13.19 corrigeant CVE-2025-1094, et Rapid7 publie son analyse.

Déroulement de l'Attaque Étape par Étape

Étape 1

L'entrée atteint une fonction d'échappement

Une application prend un texte influencé par l'attaquant et le passe à PQescapeLiteral ou à une fonction voisine de libpq pour construire une requête SQL.

Étape 2

L'UTF-8 invalide cache une apostrophe

D'après l'avis de PostgreSQL, une séquence multioctet invalide placée avant une apostrophe amenait la routine d'échappement à laisser cette apostrophe non échappée.

Étape 3

psql ferme la chaîne trop tôt

Quand la requête est passée à psql, l'apostrophe non échappée ferme le littéral de chaîne, et le reste de l'entrée est interprété comme du SQL ou des commandes psql.

Étape 4

Une méta-commande lance un shell

Rapid7 a montré que le texte injecté peut contenir la méta-commande \!, que psql exécute comme commande système sur la machine où il tourne.

Code Source : Vulnérable vs Sécurisé

Python illustratif avec psycopg 3, pas le code de BeyondTrust. La version vulnérable reproduit le schéma risqué cité par l'avis : échapper une valeur avec libpq, l'insérer dans du texte SQL et envoyer ce texte à psql. La version durcie supprime les deux moitiés : elle rejette l'UTF-8 mal formé avant tout traitement et envoie la valeur comme paramètre lié via un vrai pilote, si bien qu'il n'y a ni étape d'échappement à tromper ni interpréteur de méta-commandes psql sur le chemin. Mettre à jour libpq reste nécessaire pour tout code qui échappe encore.

IMPLÉMENTATION VULNÉRABLE
import subprocess

import psycopg


def lookup_user(conn: psycopg.Connection, name: bytes) -> bytes:
    # PQescapeLiteral de libpq (avant 17.3/16.7/15.11/14.16/13.19) ne
    # neutraliser un guillemet masqué derrière une séquence d’octets UTF-8 invalide.
    literal = conn.pgconn.escape_literal(name)
    script = b"SELECT id FROM users WHERE name = " + literal + b";\n"
    # psql exécute son entrée comme un script, y compris les méta-commandes telles que \!
    result = subprocess.run(["psql", "-At", "-d", "app"], input=script,
                            capture_output=True, check=True)
    return result.stdout
PATCH SÉCURISÉ ET ROBUSTE
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:
        # La valeur est transmise comme paramètre lié, séparément du texte SQL,
        # et aucun processus psql (ni donc aucun analyseur de méta-commandes) n’intervient.
        cur.execute("SELECT id FROM users WHERE name = %s", (text,))
        return [row[0] for row in cur.fetchall()]

Liste de Contrôle de Sécurité pour l'Ingénierie

Sources