| 항목 | 내용 |
|---|---|
| CVE ID | CVE-2007-3280 |
| 영향 소프트웨어 | PostgreSQL 8.1 계열의 dblink contrib 모듈 (기본 pg_hba.conf의 local trust 인증과 결합됐을 때) |
| CWE | NVD-CWE-Other (NVD가 구체적 CWE를 배정하지 않음). 본질은 설계상의 신뢰 경계 결합 문제로 Privilege Escalation에 해당한다 |
| CVSS | NVD CVSS v2.0 9.0 HIGH AV:N/AC:L/Au:S/C:C/I:C/A:C |
| 공개일 | 2007-06-19 (NVD 등록), 최초 상세 공개는 2007-06-05 (Portcullis/Leidecker 백서) |
| 발견/정리자 | Nico Leidecker (Portcullis Computer Security), "Having Fun With PostgreSQL" |
| 공식 수정 | 특정 코드 패치 없음. PostgreSQL 8.2가 도입한 공유 라이브러리 "매직 블록(magic block)" 호환성 검사로 핵심 악용 경로가 막혔고, 근본 대응은 pg_hba.conf의 local trust 인증을 비활성화하는 운영 설정 변경이다 |
PostgreSQL은 CREATE FUNCTION ... LANGUAGE C로 시스템의 임의 공유 라이브러리(.so 파일)
안의 함수를 SQL에서 호출 가능한 함수로 등록할 수 있다. 이 기능 자체는 슈퍼유저 권한을
요구하도록 제한돼 있어 안전하다고 여겨졌지만, 매핑 대상이 PostgreSQL 확장 모듈이어야 한다는
제약은 없었다 — glibc의 libc.so.6 안에 있는 system 심볼을 그대로 SQL 함수로 매핑하면,
SQL 한 줄로 셸 명령을 실행할 수 있었다. 문제는 여기서 끝나지 않는다. PostgreSQL 소스
배포판의 기본 인증 설정(pg_hba.conf)은 로컬 접속에 대해 비밀번호 없이 요청한 사용자
이름을 그대로 신뢰하는 "local trust" 인증을 켜 두고 있었다. 원격 데이터베이스에 접속해
쿼리를 실행하는 dblink 모듈이 설치돼 있으면, 슈퍼유저가 아닌 일반 사용자도 dblink로
자기 자신의 서버(127.0.0.1)에 슈퍼유저 이름으로 다시 접속을 열 수 있었다 — local trust가
비밀번호를 요구하지 않으므로 이 접속은 그대로 성립한다. 이렇게 얻은 슈퍼유저 세션으로
앞서 설명한 라이브러리 함수 매핑을 수행하면, 처음에는 아무 권한도 없던 사용자가 결국
PostgreSQL 서버 프로세스의 OS 계정 권한으로 셸 명령을 실행하는 데까지 도달한다. dblink나
C 함수 매핑 기능 각각은 설계된 대로 동작했을 뿐이지만, 두 기능이 "local trust는 안전한
기본값"이라는 잘못된 전제 위에서 조합되면서 권한 상승 경로가 만들어졌다.
PostgreSQL은 SQL, PL/pgSQL 같은 절차 언어뿐 아니라 C로 작성된 공유 라이브러리 안의 함수도
SQL 함수로 등록해 호출할 수 있다. CREATE FUNCTION에 LANGUAGE C를 지정할 때는 두 정보,
즉 로드할 공유 오브젝트 파일의 경로와 그 안에서 호출할 C 심볼(함수) 이름을 함께 지정한다.
공식 문서는 이 과정을 다음과 같이 설명한다.
The
CREATE FUNCTIONfor a user-defined C function must therefore specify two pieces of information for the function: the name of the loadable object file, and the C name (link symbol) of the specific function to call within that object file. ... If the name is an absolute path, the given file is loaded.
즉 파일 경로로 절대 경로를 주면 PostgreSQL은 그 경로의 공유 오브젝트를 그대로
dlopen()류의 메커니즘으로 로드하고, 지정한 심볼을 함수 포인터로 찾아낸다. 이 메커니즘
자체는 PostgreSQL 확장을 만드는 정상적인 방법이며, LANGUAGE C로 함수를 만드는 행위는
슈퍼유저만 할 수 있도록 제한돼 있다 — 이것이 이 기능의 유일한 접근 통제 지점이다. 문제는
이 시점(8.1 계열)의 PostgreSQL이 "로드하려는 공유 오브젝트가 실제로 PostgreSQL 확장으로
만들어졌는가"를 전혀 검증하지 않았다는 데 있다. 경로와 심볼 이름이 문법적으로 유효하기만
하면, 그 라이브러리가 PostgreSQL을 위해 만들어졌든 아니든 상관없이 로드하고 호출을
시도한다. 이는 시스템에 이미 존재하는 /lib/libc.so.6 같은 C 표준 라이브러리에도 그대로
적용된다 — glibc는 system(), open(), write(), close()처럼 셸/파일 조작에 쓸 수
있는 표준 C 함수들을 심볼로 노출하므로, 이들을 그대로 SQL 함수 이름에 매핑하는 것이
가능했다.
dblink는 PostgreSQL 7.2부터 제공된 contrib 모듈로, SQL 안에서 다른(또는 같은) PostgreSQL
서버에 접속해 쿼리를 실행하고 그 결과를 로컬 쿼리의 일부처럼 받아올 수 있게 해 준다. 전형적인
사용례는 다음과 같다.
CREATE VIEW entry_states AS SELECT * FROM
dblink('host=1.2.3.4 dbname=remotedb user=dbuser password=secretpass',
'SELECT id, title FROM entries')
AS remote_entries(id INT, title TEXT);dblink()의 첫 인자는 libpq 접속 문자열(호스트, DB 이름, 사용자, 비밀번호 등)이고, 둘째
인자는 그 접속 위에서 실행할 SQL이다. 중요한 점은, dblink()가 호출한 세션과는 별개의
새 데이터베이스 연결을 스스로 열어서 그 연결의 인증을 통과한 뒤 쿼리를 실행한다는
것이다. 즉 중첩된 쿼리는 dblink()를 호출한 세션의 권한이 아니라, 접속 문자열에 명시한
대상 서버·사용자의 인증과 권한 아래에서 실행된다. 이 자체는 "원격 접속"이라는 기능의
당연한 동작이며 버그가 아니다.
PostgreSQL은 어떤 접속 경로(로컬 유닉스 소켓, TCP 등)에 어떤 인증 방식을 요구할지를
pg_hba.conf에서 정한다. 소스에서 직접 빌드한 기본 설정은 로컬(같은 머신에서의) 접속에
대해 trust 방식을 사용하도록 되어 있었다. trust 인증은 접속을 요청한 클라이언트가
"나는 사용자 X다"라고 주장하면 비밀번호나 다른 증명 없이 그대로 X로 로그인시켜 준다 —
슈퍼유저 이름을 포함해서다. PostgreSQL 자신의 설정 파일 주석도 이 위험을 명시적으로
경고한다.
CAUTION: Configuring the system for local 'trust' authentication allows any local user to connect as any PostgreSQL user, including the database superuser. If you do not trust all your local users, use another authentication method.
이 경고는 "로컬 유닉스 소켓/로컬호스트로 접속하는 그 누구든, 슈퍼유저 이름만 대면 그대로
슈퍼유저가 될 수 있다"는 뜻이다. 단독으로는 "로컬 사용자를 모두 신뢰한다"는 운영 전제로
받아들여질 수 있지만, dblink가 설치돼 있으면 이 전제가 SQL 세션 안에서도 그대로
재현된다는 점이 이 CVE의 핵심이다 — SQL 쿼리 자체가 "로컬에서 접속하는 클라이언트"가
되어 이 신뢰를 그대로 이용할 수 있기 때문이다.
dblink가 설치되어 있고 local trust 인증이 켜져 있다면, 슈퍼유저가 아닌 일반 사용자
someuser도 dblink()의 접속 문자열에 슈퍼유저 이름(예: admin)을 지정해 자기 자신의
서버로 새 접속을 열 수 있다.
-- someuser로 접속한 세션에서 실행
SELECT * FROM dblink('host=127.0.0.1 user=admin dbname=somedb',
'select username, passwd from pg_shadow')
returns (username TEXT, passwd TEXT);이 접속은 127.0.0.1을 대상으로 하므로 pg_hba.conf의 local trust 규칙이 적용되고,
user=admin이라고 주장하는 것만으로 비밀번호 검증 없이 admin(슈퍼유저)으로 로그인이
성립한다. 중첩 쿼리(select username, passwd from pg_shadow)는 someuser가 아니라
admin의 권한으로 실행되므로, 원래 someuser에게는 접근이 거부됐을 pg_shadow(비밀번호
해시 테이블)를 그대로 읽어낼 수 있다. 이 시점부터 공격자는 SQL 인젝션이나 저권한 계정
하나만으로도, 사실상 슈퍼유저의 SQL 권한을 대신 행사할 수 있는 창구를 얻는다.
슈퍼유저 권한(직접 보유했든, 1단계로 얻어냈든)을 가진 세션에서는 CREATE FUNCTION으로
임의 공유 라이브러리의 심볼을 SQL 함수에 매핑할 수 있다. dblink 자신도 다음과 같은
형태로 등록되어 있다.
CREATE OR REPLACE FUNCTION dblink_connect (text) RETURNS text AS
'$libdir/dblink','dblink_connect' LANGUAGE 'C' STRICT;$libdir는 PostgreSQL 라이브러리 디렉터리로 치환되는 변수일 뿐, 이 문법 자체는 경로와
심볼 이름을 받는 일반적인 매핑 선언이다. 여기서 $libdir/dblink 대신 시스템에 이미 존재하는
/lib/libc.so.6를, dblink_connect 대신 glibc의 system 심볼을 지정하면 그대로 동작한다.
CREATE OR REPLACE FUNCTION system(cstring) RETURNS int AS
'/lib/libc.so.6', 'system' LANGUAGE 'C' STRICT;PostgreSQL은 SQL의 TEXT를 C 문자열로 변환하는 cstring 타입 변환을 제공하므로, 이렇게
등록한 system() SQL 함수는 인자로 받은 텍스트를 그대로 glibc의 system(3)에 전달한다.
이후 SELECT system('...')을 실행하면 그 문자열이 PostgreSQL 서버 프로세스를 실행하는
OS 계정(전형적으로 postgres 계정, 반드시 root는 아니다)의 권한으로 셸에 전달되어
실행된다. 같은 방식으로 open/write/close를 매핑하면, 서버가 쓸 수 있는 임의 경로에
바이너리 데이터를 파일로 기록하는 것도 가능하다.
┌──────────────────────────────────────────────────────────────────┐
│ 패치 전 (PostgreSQL 8.1, dblink 설치 + local trust) │
├──────────────────────────────────────────────────────────────────┤
│ 저권한 로컬 사용자 someuser │
│ │ psql -U someuser somedb (local trust: 비밀번호 없이 로그인) │
│ ▼ │
│ SELECT * FROM dblink('host=127.0.0.1 user=admin ...', '...') │
│ │ dblink가 127.0.0.1로 새 접속을 염 │
│ │ local trust: user=admin 주장만으로 인증 통과 │
│ ▼ │
│ 중첩 쿼리가 admin(슈퍼유저) 권한으로 실행 │
│ │ 깨진 불변식 #1: "로컬 신뢰"가 OS 셸 접근이 아니라 │
│ │ SQL 세션 안에서도 동일하게 재현됨 │
│ ▼ │
│ CREATE FUNCTION system(cstring) RETURNS int AS │
│ '/lib/libc.so.6', 'system' LANGUAGE 'C' STRICT; │
│ │ C 함수 매핑에 PostgreSQL 확장 여부 검증 없음 │
│ │ 깨진 불변식 #2: "슈퍼유저가 매핑하는 라이브러리는 │
│ │ PostgreSQL 확장일 것"이라는 암묵적 전제가 강제되지 않음 │
│ ▼ │
│ SELECT system('...'); │
│ → postgres 서버 프로세스의 OS 계정 권한으로 셸 명령 실행 │
└──────────────────────────────────────────────────────────────────┘
두 단계 모두 개별 컴포넌트의 "버그"가 아니라는 점이 이 취약점의 교육적 핵심이다. dblink는
문서화된 대로 원격(또는 로컬) 접속을 열고 그 접속의 권한으로 쿼리를 실행했을 뿐이고,
CREATE FUNCTION ... LANGUAGE C도 문서화된 대로 지정된 경로·심볼을 로드했을 뿐이다.
취약점은 이 두 정상 기능이, "로컬 신뢰는 안전하다"는 배포 시점의 기본값과 "슈퍼유저가
불러오는 라이브러리는 안전할 것이다"라는 검증되지 않은 전제 위에서 조합됐을 때 비로소
드러난다.
dblink를 이용한 1단계 권한 상승은 PostgreSQL 자체의 코드 결함이 아니라 pg_hba.conf의
기본값 문제이므로, 코드 패치가 아니라 설정 변경으로 막힌다. 원 백서와 이후 배포판들의
권고는 동일하게, 로컬 접속에 대해 trust 대신 비밀번호나 OS 계정 기반 인증을 강제하도록
바꾸는 것이다.
# 취약한 기본값
local all all trust
# 권장 설정
local all all ident sameuser
host all all md5
이렇게 바꾸면 dblink로 127.0.0.1에 다시 접속하더라도, 대상 사용자의 비밀번호(또는
OS 계정 일치)를 모르는 이상 인증에 실패하므로 1단계 권한 상승 자체가 성립하지 않는다.
Debian 보안 트래커도 8.1/8.2 패키지를 "영향 없음"으로 분류하면서 그 근거로 "PL/pgSQL과
dblink가 기본적으로 활성화돼 있지 않다"는 점을 든다 — 즉 배포판 차원의 실질적 완화는
공격에 필요한 두 전제조건(위험한 절차 언어·모듈의 기본 설치, local trust) 중 하나를
없애는 방식이었다.
2단계(임의 라이브러리 함수 매핑)는 PostgreSQL 8.2에서 도입된 공유 라이브러리 호환성 검사로 별도로 막혔다. 공식 8.2 릴리스 노트는 이 변경을 다음과 같이 설명한다.
Add
PG_MODULE_MAGICheader block to all shared object files (Martijn van Oosterhout) The magic block prevents version mismatches between loadable object files and servers.
8.2부터는 LANGUAGE C 함수가 담긴 공유 오브젝트가 PG_MODULE_MAGIC 매크로로 선언된
특정 구조체(버전·빌드 옵션 정보를 담은 "매직 블록")를 내보내지 않으면 로드 자체가
거부된다. glibc의 libc.so.6는 애초에 PostgreSQL을 위해 빌드된 라이브러리가 아니므로 이
블록이 없고, 따라서 8.2 이상에서는 CREATE FUNCTION ... AS '/lib/libc.so.6', 'system' ...
시도 자체가 로드 단계에서 실패한다.
┌───────────────────────────────────────────────────────────────┐
│ 패치 후 (PostgreSQL 8.2+) │
├───────────────────────────────────────────────────────────────┤
│ CREATE FUNCTION ... AS '/lib/libc.so.6', 'system' LANGUAGE 'C' │
│ │ │
│ ▼ │
│ 동적 로더가 라이브러리를 열고 PG_MODULE_MAGIC 블록을 찾음 │
│ │ │
│ ├─ 블록 없음 (libc.so.6는 PostgreSQL 확장이 아님) │
│ ▼ │
│ 로드 거부 ──▶ system()/open()/write() 직접 매핑 경로 차단 │
│ │
│ 복구된 불변식: LANGUAGE C 함수는 PostgreSQL 빌드 규격을 명시한 │
│ 라이브러리에서만 로드된다 │
└───────────────────────────────────────────────────────────────┘
다만 원 백서(4장)는 이 완화가 완전하지 않다고 지적한다 — 8.2가 도입한 COPY ... TO/FROM PROGRAM 이전 시점 기준으로도, 슈퍼유저 권한만 있으면 COPY 명령으로 테이블 내용을
파일로 쓰는 경로는 매직 블록 검사와 무관하게 여전히 남아 있었고, 이진 데이터를 쓸 수는
없었지만 세계 쓰기 가능한 디렉터리(/tmp 등)에 ASCII 데이터를 쓰는 정도는 가능했다. 즉
매직 블록은 "임의 라이브러리 함수 직접 호출"이라는 가장 강력한 경로를 막았을 뿐, 슈퍼유저
권한을 얻는 것 자체(1단계)를 막지는 못한다 — 그래서 근본 대응은 여전히 local trust
비활성화다.
dblink가 호출자의 권한이 아니라 접속 문자열에 명시된 대상의 권한으로 쿼리를 실행한다는 설계는 다른 원격 접속 확장(예: postgres_fdw)에서는 어떻게 다르게 다뤄질까?
PostgreSQL 8.2의 매직 블록처럼 "실행 대상이 이 시스템을 위해 만들어졌는가"를 검증하는 서명·메타데이터 검사는 다른 플러그인 로딩 시스템(PAM 모듈, 브라우저 확장 등)에서 어떤 형태로 나타날까?
local trust 인증이 유닉스 소켓 파일 권한만으로 로컬 사용자를 충분히 걸러낸다고 가정한 설계는, 오늘날의 컨테이너·다중 테넌트 환경에서도 여전히 안전한 전제일까?
- NVD — CVE-2007-3280 — CVSS v2, 참조 링크 목록. (검증됨)
- Nico Leidecker, "Having Fun With PostgreSQL" (Portcullis Computer Security, 2007-06-05) — 저작권 표시로 전자적 재배포가 명시적으로 허용된 원 백서. dblink 권한 상승, 라이브러리 함수 매핑, 8.2 완화, 대응 권고를 모두 다룬 1차 자료. (검증됨, 1차 자료; NVD 공식 참조 목록에도 등재)
- PostgreSQL 공식 문서 — 36.10. C-Language Functions —
CREATE FUNCTION ... LANGUAGE C의 동적 로딩 경로 해석 규칙 원문. (검증됨, 1차 자료) - PostgreSQL 8.2 릴리스 노트 —
PG_MODULE_MAGIC매직 블록 도입 공식 기록. (검증됨, 1차 자료) - Debian Security Tracker — CVE-2007-3280 — 배포판 차원의 영향 평가와 근거("PL/pgSQL·dblink 기본 비활성"). (검증됨)
본문 참고 자료로 충분했다.
- PostgreSQL 8.1 당시 소스 배포판의
pg_hba.conf기본값이 정확히 어느 배포 경로(소스 빌드 vs. 각 리눅스 배포판의 패키지)에서trust였는지는 원 백서의 서술("configuration from the sources")에 의존했다. RPM/DEB 패키지가 자체적으로 더 안전한 기본값을 썼을 가능성은 배제하지 않았다. dblink와 PL/pgSQL이 8.1/8.2 기본 설치에 포함되지만 기본적으로 "활성화"되어 있지 않다는 Debian 트래커의 판단이, 다른 배포판·MySQL 자체 소스 빌드에도 동일하게 적용되는지는 개별적으로 확인하지 않았다.- PostgreSQL 8.2의
COPY ... TO/FROM PROGRAM도입 시점과 매직 블록 도입 시점의 정확한 선후 관계, 그리고 이 두 기능이 이후 버전에서 함께 어떤 권한 모델로 재구성됐는지는 이 리포트의 범위를 벗어나 추적하지 않았다.