| 항목 | 내용 |
|---|---|
| CVE ID | CVE-2002-0969 |
| 영향 소프트웨어 | MySQL Server(mysqld) Win32 빌드, 3.23.50 이전 및 4.0 beta 4.0.2 이전 |
| CWE | CWE-120 (Buffer Copy without Checking Size of Input, "Classic Buffer Overflow") — NVD 분류 |
| CVSS | NVD CVSS v3.1(소급 산정) 7.8 HIGH AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H / CVSS v2.0 4.6 MEDIUM AV:L/AC:L/Au:N/C:P/I:P/A:P |
| 공개일 | 2002-10-02 (Bugtraq, Westpoint 자체 권고 wp-02-0003) / 2002-10-11 (CVE 등록) |
| 발견자 | Matt Moore, Westpoint Ltd. |
| 공식 수정 | MySQL 3.23.50, MySQL 4.0.2 (구체적 패치 diff는 원본 소스가 남아 있지 않아 확인하지 못함 — "수정 방법" 참고) |
MySQL의 Windows 배포판은 mysqld-nt.exe를 NT 서비스로 등록해 기본적으로 LocalSystem
계정, 즉 해당 컴퓨터에서 가장 강력한 권한으로 실행한다. 이 서비스는 시작할 때 c:\my.ini
(또는 c:\WINNT\my.ini)라는 설정 파일을 읽어 datadir 같은 옵션 값을 가져오는데, 이
파일의 기본 접근 제어 목록(ACL)은 Everyone 그룹에 전체 제어(Full Control) 권한을 부여한다
— 즉 로그인 가능한 그 어떤 저권한 계정도 이 파일의 내용을 마음대로 바꿀 수 있다. 문제는
datadir 값을 실제로 저장하는 코드가 길이를 검사하지 않는 문자열 복사 함수를 고정 크기
512바이트 전역 버퍼에 그대로 적용한다는 데 있다. 저권한 사용자가 my.ini의 datadir=
줄을 아주 긴 문자열로 바꿔 두고 서비스가 재시작(또는 시스템이 재부팅)되기를 기다리면,
SYSTEM 권한으로 실행되는 mysqld-nt.exe의 설정 파싱 코드가 그 값을 그대로 복사하다가
버퍼 경계를 넘어서게 된다. Westpoint의 원 보고서는 이 오버플로우로 명령 포인터(EIP)를
덮어써 SYSTEM 컨텍스트에서 임의 코드를 실행할 수 있다고 설명한다.
Windows의 서비스 제어 관리자(SCM)는 부팅 시점이나 수동 시작 시 등록된 서비스 실행 파일을
지정된 계정의 보안 토큰과 함께 구동한다. 별도로 지정하지 않으면 기본값은 LocalSystem이며,
이 계정은 로컬 시스템 자원에 사실상 제한 없는 접근 권한을 가진다. MySQL의 Windows 설치
프로그램은 mysqld-nt를 이 기본값으로 등록하는 경우가 흔했다. 따라서 이 서비스가 시작할
때 실행하는 모든 코드 — 설정 파일을 읽고 파싱하는 코드를 포함해서 — 는 그 컴퓨터에서 가장
높은 권한으로 동작한다. 서비스가 "무엇을 읽어서 무엇을 하는가"가 공격 표면이 되는 이유가
여기에 있다.
Windows의 파일 시스템은 각 파일·디렉터리에 임의 접근 제어 목록(DACL)을 붙여, 어떤 보안
주체(사용자·그룹)가 읽기·쓰기·실행 중 무엇을 할 수 있는지를 규정한다. Everyone 그룹은
익명에 가까운 로컬 계정까지 포함해 사실상 그 시스템에 로그인할 수 있는 모든 주체를
가리키며, 여기에 Full Control이 부여됐다는 것은 소유권 변경까지 포함한 모든 조작이
허용된다는 뜻이다. my.ini가 이런 권한으로 설치되면, 그 파일은 더 이상 관리자만 통제하는
신뢰된 입력이 아니라 로컬의 아무 사용자나 채워 넣을 수 있는 입력 통로가 된다. 서비스가 이
파일을 SYSTEM 권한으로 다시 읽는 순간, 파일 권한의 신뢰 경계와 프로세스 권한의 신뢰 경계가
어긋난다.
MySQL의 옵션 처리기(mysys의 load_defaults()/my_getopt)는 my.cnf/my.ini 같은
설정 파일의 key=value 줄들을 읽어, 마치 커맨드라인에서 --key=value를 넘긴 것과 동일한
형태로 변환한 뒤 프로그램의 옵션 파서에 넘긴다. 즉 my.ini에 적힌 datadir=C:/mysql/data
한 줄은, mysqld가 실제로 옵션을 처리하는 코드 입장에서는 커맨드라인 인자
--datadir=C:/mysql/data를 받은 것과 구별되지 않는다. 이 설계 덕분에 설정 파일과
커맨드라인을 하나의 파서로 처리할 수 있지만, 반대로 말하면 설정 파일에 쓰인 값도
커맨드라인 인자와 동일한 신뢰 수준(즉 "검증이 필요한 외부 입력")으로 다뤄야 한다는 뜻이다.
이 시기 MySQL의 C 코드베이스는 문자열을 담을 때 malloc으로 매번 할당하는 대신, 미리
정해 둔 최대 길이의 고정 크기 배열(char buf[FN_REFLEN] 같은)을 전역 변수로 선언해 두고
재사용하는 관례를 자주 썼다. 이런 버퍼에 값을 채워 넣을 때 흔히 쓰인 strmov()는 MySQL이
자체 정의한 문자열 복사 함수로, 표준 C의 stpcpy()와 동일하게 동작한다 —
"목적지 버퍼가 충분히 큰가"를 전혀 검사하지 않고, 원본 문자열의 끝(널 종료 바이트)까지
그대로 복사한다. 원본 문자열의 길이가 목적지 버퍼보다 길면, 복사는 버퍼 경계를 넘어 그
뒤에 있는 다른 메모리를 덮어쓰며 계속된다. 이것이 Buffer Overflow의
가장 고전적인 형태다.
여기서 중요한 구분은, 오버플로우가 일어나는 버퍼가 스택이 아니라 전역(정적 저장소, BSS/데이터 세그먼트) 변수라는 점이다. 스택 버퍼 오버플로우는 흔히 같은 스택 프레임에 저장된 반환 주소(EIP가 될 값)를 직접 덮어써 제어 흐름을 바꾸지만, 전역 버퍼 오버플로우는 컴파일러·링커가 그 버퍼 바로 뒤에 배치한 다른 전역 변수나 자료구조를 순서대로 덮어쓴다. 그 뒤에 정확히 무엇이 있고, 그것을 덮어썼을 때 실제로 명령 포인터 제어까지 이어지는지는 컴파일러 버전·최적화 옵션·링크 순서에 따라 달라지는 이진 파일 수준의 사실이라, 소스 코드만 정적으로 읽어서는 100% 재구성할 수 없다.
sql/mysqld.cc는 데이터 디렉터리 경로를 담을 전역 변수를 다음과 같이 선언한다.
char mysql_real_data_home[FN_REFLEN],
mysql_data_home[2],language[LIBLEN],reg_ext[FN_EXTLEN],
...FN_REFLEN은 include/my_global.h에서 512로 정의된, "전체 경로의 최대 길이"를 나타내는
상수다.
#define FN_REFLEN 512 /* Max length of full path-name */즉 mysql_real_data_home은 정확히 512바이트짜리 고정 크기 전역 버퍼다.
명령행/설정 파일 옵션 테이블은 datadir을 단문자 옵션 'h'로 등록해 두었다.
{"datadir", required_argument, 0, 'h'},그리고 실제 옵션 값을 소비하는 get_one_option()의 스위치문에서, 'h' 케이스는 다음
한 줄로 처리된다.
case 'h':
strmov(mysql_real_data_home,optarg);
break;optarg는 my.ini의 datadir= 줄에서 그대로 넘어온, 길이 제한이 없는 문자열이다.
strmov()는 m_string.h에서 다음과 같이 stpcpy()의 별칭으로 정의된다.
#define strmov(A,B) stpcpy((A),(B))stpcpy(dst, src)는 src가 dst보다 길든 짧든 상관없이 널 종료 바이트까지 그대로
복사한다 — mysql_real_data_home이 512바이트라는 사실은 이 호출 지점에서 전혀
고려되지 않는다.
T0 로컬 저권한 사용자가 c:\my.ini를 연다
└─ 기본 ACL: Everyone에게 Full Control 부여 → 편집 가능
T1 [datadir] datadir=C:/mysql/data 줄을
datadir=C:/AAAA...(수백 바이트 이상)...AAAA 로 교체 후 저장
T2 mysqld 서비스가 재시작되거나 컴퓨터가 재부팅됨
└─ SCM이 LocalSystem 토큰으로 mysqld-nt.exe를 기동
T3 mysqld-nt.exe 시작 코드가 load_defaults()로 my.ini를 읽음
└─ "datadir=..." 줄을 --datadir=... 형태의 옵션으로 변환
T4 get_one_option()의 case 'h'
└─ strmov(mysql_real_data_home, optarg) ← 길이 검사 없음
T5 optarg 길이 > FN_REFLEN(512)
└─ mysql_real_data_home 뒤의 전역 메모리 영역까지 연쇄적으로 덮어씀
T6 (Westpoint 보고에 따르면) 덮어써진 영역이 결국 실행 흐름에
영향을 미쳐 EIP가 공격자가 채워 넣은 바이트로 대체됨
└─ SYSTEM 권한 컨텍스트에서 임의 코드 실행
┌────────────────────────────────────────────────────────────────┐
│ 패치 전 get_one_option() │
├────────────────────────────────────────────────────────────────┤
│ optarg (my.ini의 datadir= 값, 길이 무제한) │
│ │ │
│ ▼ │
│ strmov(mysql_real_data_home, optarg) │
│ │ stpcpy와 동일: 목적지 크기 확인 없이 끝까지 복사 │
│ ▼ │
│ mysql_real_data_home[512] ← 이 경계에서 멈추지 않음 │
│ │ │
│ ▼ │
│ 전역 영역의 인접 메모리(다음 전역 변수들)를 순차적으로 덮어씀 │
│ │
│ 깨진 불변식: "복사되는 데이터 길이는 목적지 버퍼 크기 이하다" │
└────────────────────────────────────────────────────────────────┘
이 흐름에서 로컬 저권한 사용자가 SYSTEM 권한 코드에 임의 길이의 데이터를 주입할 수 있는
근본 이유는 두 가지 결함의 조합이다 — (1) 설정 파일의 ACL이 지나치게 느슨해서 신뢰 경계가
파일 시스템 수준에서부터 무너져 있고, (2) 그 값을 소비하는 코드가 길이를 검사하지 않는다.
Windows 배포판 특유의 문제인 이유도 여기에 있다 — 같은 버그 패턴(길이 검사 없는 strmov)이
Unix 계열의 my.cnf에도 존재할 수 있지만, Unix에서는 관례적으로 설정 파일이 root 소유의
제한된 권한으로 설치되므로 저권한 사용자가 애초에 값을 바꿔 넣을 수 없다.
Westpoint의 원 권고와 이를 인용한 벤더 대응 기록은 "MySQL 3.23.50과 4.0.2에서 수정됨"이라고
명시하지만, 이 두 릴리스의 원본 소스 트리는 공개 아카이브(GitHub 미러 포함)에서 확보하지
못했다. 따라서 실제 패치가 strmov()를 길이 제한이 있는 strmake()/strnmov() 계열로
바꾸는 방식이었는지, mysql_real_data_home을 동적 할당 버퍼로 바꾸는 방식이었는지, 아니면
my.ini의 권장 ACL을 더 제한적으로 바꾸는 문서·설치 스크립트 변경이었는지는 정확한 diff로
확인하지 못한 미해결 지점으로 남긴다. 다만 결함의 성격(길이 미검사 복사)을 고려하면, 같은
MySQL 코드베이스의 다른 곳에서 실제로 쓰인 strmake(dst, src, sizeof(dst)-1) 같은
길이 제한 복사 패턴으로 교체하는 것이 가장 자연스러운 수정 방향이었을 것으로 추정된다 — 이는
코드베이스의 일반적인 관례에 기반한 추정이며, 이 CVE의 실제 패치 커밋에서 직접 확인한
내용은 아니다.
수정 diff를 확인하지 못했더라도, 이 결함이 두 개의 독립된 문제(느슨한 파일 ACL, 길이 미검사 복사)로 구성된다는 점에서 원칙적인 대응 방향은 명확하다.
- 옵션 값을 고정 크기 버퍼에 채울 때는 목적지 크기를 아는 상태에서 복사해야 한다
(
strmake(dst, src, sizeof(dst)-1)처럼 최대 길이를 명시하는 함수를 사용하거나, 애초에 고정 크기 배열 대신 필요한 만큼 할당하는 동적 버퍼를 쓴다). - SYSTEM 권한으로 재해석되는 설정 파일은 SYSTEM 또는 관리자만 쓸 수 있도록 ACL을 제한해야 한다. 설치 스크립트가 기본으로 관대한 권한을 부여한다면, 이는 애플리케이션 로직과 무관하게 그 자체로 별도의 취약점이다.
같은 시기 MySQL 코드베이스에서 strmov로 채워지는 다른 고정 크기 전역 버퍼들도 비슷한 위험을 안고 있었는지 살펴볼 만하다.
전역/BSS 버퍼 오버플로우가 스택 버퍼 오버플로우와 달리 어떤 조건에서 실행 흐름 제어로 이어지는지 비교해 보면 유익하다.
설정 파일을 커맨드라인 인자와 동일한 파서로 처리하는 설계가 편의성과 공격 표면 사이에서 어떤 트레이드오프를 만드는지 생각해 볼 수 있다.
- Bugtraq — "wp-02-0003: MySQL Locally Exploitable Buffer Overflow" (Matt Moore, Westpoint Ltd., 2002-10-02) — 최초 공개 권고 원문, ACL 문제와 익스플로잇 개요. (검증됨, 1차 자료)
- NVD — CVE-2002-0969 — CVSS, CWE-120 분류, 영향 버전. (검증됨)
sql/mysqld.cc(mysql-3.23.49 소스 미러) —mysql_real_data_home선언과case 'h': strmov(...)처리 코드. GPL, 3.23.49는 이 CVE가 수정되기 직전 버전. (검증됨, 1차 자료에 준함 — 공식 배포 태그와의 완전 일치는 별도 확인하지 않음)include/m_string.h(mysql-3.23.49 소스 미러) —strmov가stpcpy의 별칭임을 보여주는 정의. (검증됨, 1차 자료에 준함)include/my_global.h(mysql-3.23.49 소스 미러) —FN_REFLEN 512정의. (검증됨, 1차 자료에 준함)
본문 참고 자료로 충분했다.
- MySQL 3.23.50과 4.0.2의 실제 패치 diff를 확보하지 못해, 수정이 정확히 어떤 함수 호출로 바뀌었는지는 코드베이스의 일반적인 관례에 근거한 추정일 뿐 확인된 사실이 아니다.
- 이 리포트가 참조한
mysql-3.23.49소스는 공식 MySQL AB 배포 아카이브가 아니라 제3자 GitHub 미러다. 취약점이 존재하는 코드 경로(case 'h': strmov(...)) 자체는 Westpoint 권고의 설명과 정확히 일치하지만, 이 미러가 실제 3.23.49 공식 릴리스와 바이트 단위로 동일한지는 별도로 검증하지 않았다. - Westpoint 권고가 주장하는 "EIP를 덮어써 임의 코드 실행"이라는 결과는, 전역 버퍼 오버플로우 가 실제로 명령 포인터 제어로 이어지는 정확한 메모리 배치를 이 리포트가 정적으로 재구성한 결과가 아니라 원 보고서의 주장을 그대로 인용한 것이다. Win32 PE 바이너리의 실제 전역 데이터 배치는 확인하지 못했다.