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 클라이언트가 처리하는 백슬래시 명령입니다. \!는 줄의 나머지를 운영체제 셸 명령으로 실행합니다.
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은 이를 자신이 실행 중인 머신에서 운영체제 명령으로 실행합니다.

소스 코드 비교: 취약한 구현 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()]

엔지니어링 및 시스템 보안 강화 체크리스트

출처