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
libpq 中的一个函数,把值加上引号并转义,以便嵌入 SQL 文本。PQescapeIdentifier、PQescapeString 和 PQescapeStringConn 也受影响。
非法 UTF-8
在声明的编码中格式不正确的字节序列。转义代码与 psql 对这类字节会“吞掉”后面几个字符的理解不一致。
psql 元命令
由 psql 客户端而非服务器处理的反斜杠命令。\! 会把该行剩余内容当作操作系统 shell 命令执行。
CVE-2024-12356
BeyondTrust Privileged Remote Access 和 Remote Support 中的一个严重命令注入漏洞。在 Rapid7 的测试中,要借它实现代码执行,必须同时利用 CVE-2025-1094。
绑定参数
与 SQL 文本分开发送给服务器的值,因此永远不需要对它做加引号或转义的判断。

框架映射

事件演进时间线

2024-12-16

BeyondTrust 在调查部分 Remote Support SaaS 实例遭入侵后,发布关于 CVE-2024-12356 的公告 BT24-10;美国财政部随后报告称这次事件波及了其系统。

2024-12-19

CISA 将 CVE-2024-12356 列入已知被利用漏洞目录。

2025-02-13

PostgreSQL 发布 17.3、16.7、15.11、14.16 和 13.19,修复 CVE-2025-1094,Rapid7 同时发布分析。

攻击执行流程分解

步骤 1

输入进入转义函数

应用把受攻击者影响的文本交给 PQescapeLiteral 或相关的 libpq 函数,用来拼接 SQL 语句。

步骤 2

非法 UTF-8 藏住一个引号

根据 PostgreSQL 公告,在单引号前放置精心构造的非法多字节序列,会让转义例程漏掉对该引号的转义。

步骤 3

psql 提前结束字面量

语句交给 psql 后,未被转义的引号会关闭字符串字面量,其余输入被当作 SQL 或 psql 命令解析。

步骤 4

元命令启动 shell

Rapid7 证明,注入的文本可以包含 \! 元命令,psql 会在其运行的机器上把它当作操作系统命令执行。

源代码对比:漏洞与安全实现

使用 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()]

工程与系统安全加固清单

参考来源