FlawOpen

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

● PostgreSQL / BeyondTrust · CVE-2025-1094 · December 2024 – February 2025 · CVSS 8.1 · 高
セキュリティ研究 · FlawOpen

CVE-2025-1094:PostgreSQL psqlのSQLインジェクション

アプリがエスケープ済みテキストからpsql入力を作る際、不正なUTF-8でlibpqの処理を回避し、SQLや\!を注入できました。2025年2月13日修正。

💡 わかりやすい解説 (ELI5)

あなたの言うことをすべてかぎかっこでくくり、決して命令と取り違えられないようにするのが仕事の通訳を想像してください。ところが、知らない文字で書かれた単語を渡すと、通訳は混乱してかぎかっこを一つ落としてしまうと分かります。あなたの言葉はかっこの外へ抜け出し、指示として読まれます。通訳は、誰かが試したあらゆる入力に対しては、設計どおりに正しく動いていたにもかかわらずです。

主要な概念と専門用語

PQescapeLiteral
値を引用符でくくってエスケープし、SQL テキストに埋め込めるようにする libpq の関数。PQescapeIdentifier、PQescapeString、PQescapeStringConn も影響を受けました。
不正な UTF-8
宣言された文字コードとして正しくないバイト列。エスケープ処理と psql とで、そのバイトが後続の何文字を取り込むかの解釈が食い違っていました。
psql のメタコマンド
サーバーではなく psql クライアントが処理するバックスラッシュ付きのコマンド。\! は行の残りを OS のシェルコマンドとして実行します。
CVE-2024-12356
BeyondTrust の Privileged Remote Access と Remote Support にある重大なコマンドインジェクションの欠陥。Rapid7 のテストでは、これでコード実行に至るには CVE-2025-1094 が必要でした。
バインドパラメーター
SQL テキストとは別にサーバーへ送られる値。引用符やエスケープの判断がそもそも行われません。

フレームワーク対応

  • このガイドで説明する弱点の CWE 参照: CWE-89 CWE-149

インシデントのタイムライン

2024-12-16

BeyondTrust が一部の Remote Support SaaS インスタンスの侵害を調査したのち、CVE-2024-12356 の公告 BT24-10 を公開する。米財務省は後に、この事案が自らのシステムに及んだと報告した。

2024-12-19

CISA が CVE-2024-12356 を既知の悪用された脆弱性カタログに追加する。

2025-02-13

PostgreSQL が CVE-2025-1094 を修正した 17.3、16.7、15.11、14.16、13.19 を公開し、Rapid7 が分析を発表する。

ステップ・バイ・ステップの攻撃フロー

ステップ 1

入力がエスケープ関数に届く

アプリケーションが攻撃者の影響を受けたテキストを受け取り、SQL 文を組み立てるために PQescapeLiteral などの libpq 関数へ渡します。

ステップ 2

不正な UTF-8 が引用符を隠す

PostgreSQL の公告によれば、シングルクォートの前に細工した不正なマルチバイト列を置くと、エスケープ処理がその引用符をエスケープしないままにしていました。

ステップ 3

psql が文字列リテラルを早く閉じる

文が psql に渡されると、エスケープされなかった引用符が文字列リテラルを閉じ、残りの入力は SQL か psql のコマンドとして解釈されます。

ステップ 4

メタコマンドがシェルを動かす

Rapid7 は、注入したテキストに \! メタコマンドを含められることを示しました。psql はそれを、自分が動いているマシン上で OS コマンドとして実行します。

ソースコード比較:脆弱 vs 堅牢化

psycopg 3 を使った説明用の Python で、BeyondTrust のコードではありません。脆弱な版は、公告が挙げる危険なパターン、つまり libpq で値をエスケープし、SQL テキストに埋め込み、そのテキストを psql に流す処理を再現しています。強化版はその両方を取り除きます。処理の前に不正な UTF-8 を拒否し、値は本物のドライバー経由でバインドパラメーターとして送るので、だませるエスケープ処理も、経路上の psql メタコマンド解釈器もありません。エスケープを使い続けるコードには、libpq の更新も引き続き重要です。

脆弱な実装
import subprocess

import psycopg


def lookup_user(conn: psycopg.Connection, name: bytes) -> bytes:
    # libpq の PQescapeLiteral(17.3/16.7/15.11/14.16/13.19 より前)は、
    # 不正な UTF-8 バイト列の後ろに隠れた引用符を無害化できなかった。
    literal = conn.pgconn.escape_literal(name)
    script = b"SELECT id FROM users WHERE name = " + literal + b";\n"
    # psql は入力をスクリプトとして実行し、\! などのメタコマンドも処理する。
    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:
        # 値は SQL テキストとは別のバインドパラメーターとして送信され、
        # psql プロセス(したがってメタコマンドパーサー)も介在しない。
        cur.execute("SELECT id FROM users WHERE name = %s", (text,))
        return [row[0] for row in cur.fetchall()]

エンジニアリング&システム堅牢化チェックリスト

参考資料