Skip to content

Latest commit

 

History

History
258 lines (220 loc) · 15.9 KB

File metadata and controls

258 lines (220 loc) · 15.9 KB

sudo가 타임스탬프 파일을 epoch로 재설정하는 것을 특수 케이스로 다루지 않아 인증을 통째로 우회하는 문제 (CVE-2013-1775)

메타데이터

항목 내용
CVE ID CVE-2013-1775
영향 소프트웨어 Sudo 1.6.0 ~ 1.7.10p6, Sudo 1.8.0 ~ 1.8.6p6
CWE CWE-287 (Improper Authentication) 계열 — 시간 기반 신뢰 검증 실패
CVSS v2 6.9 (Medium, AV:L/AC:M/Au:N/C:C/I:C/A:C) — NVD는 이 CVE에 v3 점수를 채점하지 않음
공개일 2013-03-05 (NVD 등록), 수정은 Sudo 1.7.10p7 / 1.8.6p7 (2013-02-21 커밋)
발견자 Marco Schoepl (보고), Todd C. Miller (sudo 저자, 수정)
수정 커밋 (1.7 브랜치) ddf399e3e306 "Completely ignore time stamp file if it is set to the epoch, regardless of what gettimeofday() returns."
수정 커밋 (1.8 브랜치, 태그 SUDO_1_8_6p7) ebd6cc75020f (1.7과 동일 로직, check.c)

개요

sudo는 매번 비밀번호를 묻지 않도록, 한 번 인증에 성공하면 그 사실을 "타임스탬프 파일"에 기록해 두고 일정 시간(기본 5분) 동안은 재인증 없이 sudo를 계속 쓸 수 있게 해준다. 그런데 sudo -k(자격 초기화)는 이 타임스탬프 파일을 삭제하지 않고 시각을 epoch(1970-01-01)로 되돌리는 방식으로 구현되어 있었다. 문제는 sudo가 "이 타임스탬프가 유효한가?"를 판단하는 로직 어디에도 "타임스탬프가 epoch, 즉 애초에 설정된 적이 없는 값인가?"를 특수하게 취급하는 분기가 없었다는 것이다. 그 결과 사용자가 (1) 예전에 한 번이라도 sudo 인증에 성공한 적이 있고, (2) sudo -k를 실행하고, (3) 시스템 시계를 epoch로 되돌릴 수 있다면 — 이 세 조건을 스스로 통제할 수 있는 로컬 사용자는 비밀번호 재입력 없이 sudo 명령을 계속 실행할 수 있었다. 데스크톱 환경(시계를 자유롭게 바꿀 수 있는 GUI가 있는 우분투, macOS 등)이나 배터리가 완전히 방전돼 시계가 저절로 epoch로 리셋되는 임베디드 환경에서 특히 위험이 컸다.

사전 지식

sudo의 "타임스탬프 캐시" — 왜 매번 비밀번호를 묻지 않는가

sudo는 인증에 성공할 때마다 사용자별 타임스탬프 파일(/var/run/sudo/ts/<user> 등, 배포판에 따라 경로는 다르다)의 수정 시각(mtime)을 현재 시각으로 갱신한다. 이후 sudo가 다시 호출되면, 이 파일의 mtime과 "지금" 사이의 차이가 timestamp_timeout(기본 5분) 이내이면 비밀번호를 다시 묻지 않고 곧바로 명령을 실행한다. 즉 이 파일의 mtime 자체가 "최근에 인증했다"는 사실을 나타내는 유일한 증거이며, sudo의 신뢰 모델은 전적으로 로컬 파일시스템의 타임스탬프와 시스템 시계(time(NULL))를 비교하는 것에 의존한다.

sudo -ksudo -K의 차이 — "리셋"과 "삭제"

sudo는 사용자가 이 캐시를 강제로 무효화할 수 있는 두 옵션을 제공한다.

  • sudo -K(대문자, kill): 타임스탬프 파일 자체를 완전히 삭제한다.
  • sudo -k(소문자, reset): 타임스탬프 파일을 지우는 대신 그 mtime을 epoch(0)로 덮어쓴다.

-k가 파일을 지우지 않고 epoch로 되돌리는 이유는 사용성 때문이다 — sudo는 타임스탬프 파일이 아예 "없을 때"(한 번도 인증한 적이 없을 때) 처음 실행 시 보안 경고 문구("lecture")를 출력하는데, -k를 실행할 때마다 이 문구가 다시 뜨는 것을 피하려고 파일 자체는 남겨두고 시각만 "아주 오래전"을 뜻하는 epoch로 써넣는 방식을 택한 것이다. 이 설계 자체는 오래전부터 있었고 문제로 여겨지지 않았다 — epoch는 어차피 "현재 시각보다 한참 과거"이므로 mtime과 now의 차이가 timestamp_timeout보다 훨씬 크게 나와 정상적으로는 "만료됨"으로 판정되기 때문이다.

시스템 부팅 시각(boottime)을 이용한 "시계 조작" 방어

sudo의 타임스탬프 검사에는 처음부터 한 가지 방어 장치가 더 있었다 — 만약 타임스탬프 파일의 mtime이 시스템이 부팅된 시각보다도 이전이라면, 그 타임스탬프는 이번 부팅 세션에서 만들어진 것이 아니라 리부팅 전 세션에서 남아 있던 "낡은" 것이므로 무효로 처리해야 한다는 논리다(get_boottime() + timevalcmp() 비교). 이 검사는 "시스템 시계를 사용자가 마음대로 앞뒤로 돌릴 수 있다"는 전제 자체를 어느 정도 보완하기 위한 것이었다. 하지만 이 검사는 get_boottime()이 성공했을 때만 작동하고, 이 함수가 실패하거나(당시 일부 플랫폼에서 부팅 시각 조회 API가 없거나 불안정했다) 애초에 그 비교 분기까지 도달하지 못하는 경로가 있다면 아무 효과가 없다.

취약점 분석

패치 전 로직 (check.c, check_user() 내부의 타임스탬프 판정부)

수정 커밋의 diff를 통해 확인한 패치 전 코드는 다음과 같은 구조였다.

/* check.c (수정 전) */
if (status == TS_OLD && !ISSET(flags, TS_REMOVE)) {
    mtim_get(&sb, &mtime);
    /* Negative timeouts only expire manually (sudo -k). */
    if (def_timestamp_timeout < 0 && mtime.tv_sec != 0)
        status = TS_CURRENT;
    else {
        now = time(NULL);
        if (def_timestamp_timeout &&
            now - mtime.tv_sec < 60 * def_timestamp_timeout) {
            /*
             * Check for bogus time on the stampfile.  The clock may
             * have been set back or someone could be trying to spoof us.
             */
            if (mtime.tv_sec > now + 60 * def_timestamp_timeout * 2) {
                /* "너무 미래" -> 타임스탬프 파일 자체를 삭제하고 TS_MISSING */
                ...
                status = TS_MISSING;
            } else if (get_boottime(&boottime) &&
                       timevalcmp(&mtime, &boottime, <)) {
                status = TS_OLD;       /* 부팅 전 타임스탬프 -> 만료 */
            } else {
                status = TS_CURRENT;   /* 그 외에는 "아직 유효" */
            }
        }
    }
}

핵심은 now - mtime.tv_sec < 60 * def_timestamp_timeout 이 한 줄의 비교다. 이 식은 "현재 시각과 타임스탬프 시각의 차이가 타임아웃보다 작은가"만 볼 뿐, 두 값이 서로 어떤 절대적인 시점을 가리키는지는 전혀 검증하지 않는다. 정상적인 상황이라면 now는 항상 "현재의 실제 시각"이라 mtime(과거의 인증 시각)보다 크고, 그 차이도 합리적인 범위 안에 있다. 하지만 이 비교는 두 값이 동시에 조작되는 경우를 고려하지 않았다.

공격 시나리오 — 왜 세 단계가 맞물려야 하는가

  1. 공격자(공용 PC의 로그인 사용자 등)가 과거에 정상적으로 sudo <cmd>를 한 번 실행해 인증에 성공한 적이 있다 — 이 때 타임스탬프 파일이 생성된다.
  2. 공격자가 sudo -k를 실행한다. 이 명령은 비밀번호를 요구하지 않고, 로그에도 특별히 남지 않는다. 이 시점에서 mtime.tv_sec = 0 (epoch)이 된다.
  3. 공격자가 시스템 시계를 epoch(1970-01-01 01:00:00, 로컬 타임존 오프셋 포함)로 되돌린다. 데스크톱 환경(GNOME/KDE의 날짜·시간 설정, macOS의 시스템 환경설정)에서는 일반 사용자도 이 조작이 가능한 경우가 많다.
  4. 이제 now = time(NULL) 도 거의 0에 가깝다. 따라서:
    • now - mtime.tv_sec0 - 0 = 0, 이는 60 * timeout보다 작으므로 "아직 타임아웃 안 지남" 분기로 들어간다.
    • mtime.tv_sec > now + 60*timeout*20 > (~0) + X 이므로 거짓 — "너무 미래" 경고 분기를 피해간다.
    • 마지막 방어선은 get_boottime() 비교뿐인데, 이 함수가 지원되지 않거나 실패하는 환경에서는 else 분기로 떨어져 곧바로 status = TS_CURRENT가 된다 — sudo는 이 사용자가 "방금 막 인증했다"고 믿어버린다.
  5. 공격자는 비밀번호를 한 번도 다시 입력하지 않고 sudoers가 허용하는 명령을 계속 실행할 수 있다.

주의할 점은 이 취약점이 "sudoers 정책 자체를 우회"하는 것은 아니라는 것이다 — 여전히 sudoers가 허용한 명령만 실행 가능하다. 다만 "방금 비밀번호를 확인했다"는 인증 절차 자체가 통째로 생략된다는 점에서 CWE-287(부적절한 인증) 성격의 결함이다.

텍스트로 그린 그림 — 패치 전: epoch 타임스탬프가 "유효"로 오판되는 경로

   1단계: 과거의 정상 인증
   +--------------------------+
   | 타임스탬프 파일           |
   | mtime = 실제 인증 시각 T0 |
   +--------------------------+

   2단계: "sudo -k" (비밀번호 불필요)
   +--------------------------+
   | 타임스탬프 파일           |
   | mtime = 0  (epoch)       |  <- 삭제가 아니라 "리셋"
   +--------------------------+

   3단계: 시스템 시계를 epoch로 되돌림 (사용자가 직접 조작 가능)
   +--------------------------+        +--------------------------+
   | mtime.tv_sec = 0         |        | now = time(NULL) ≈ 0    |
   +--------------------------+        +--------------------------+
              \                              /
               \                            /
                v                          v
        now - mtime.tv_sec = 0  <  60 * timeout   ---> "아직 안 지남" (통과)
        mtime.tv_sec > now + 60*timeout*2 = 거짓  ---> "너무 미래 아님" (통과)
        get_boottime() 실패/미지원            ---> else 분기
                                                        |
                                                        v
                                          status = TS_CURRENT
                                          ("방금 인증했음" 으로 오판)
                                                        |
                                                        v
                              비밀번호 재입력 없이 sudo 명령 실행 허용

수정 방법

timevalisset() — "값이 아예 설정된 적 없음"을 명시적으로 걸러냄

수정 커밋(ddf399e3e306, 1.7 브랜치 / ebd6cc75020f, 1.8 브랜치)은 기존의 시각 비교 로직 전체를 if (timevalisset(&mtime)) 조건으로 한 번 더 감쌌다. timevalisset()tv_sectv_usec이 모두 0인지, 즉 "이 타임스탬프가 애초에 유효한 시각으로 설정된 적이 있는가"만 확인하는 아주 단순한 헬퍼다.

/* check.c (수정 후) */
if (status == TS_OLD && !ISSET(flags, TS_REMOVE)) {
    mtim_get(&sb, &mtime);
    if (timevalisset(&mtime)) {
        /* Negative timeouts only expire manually (sudo -k). */
        if (def_timestamp_timeout < 0) {
            status = TS_CURRENT;
        } else {
            now = time(NULL);
            if (def_timestamp_timeout &&
                now - mtime.tv_sec < 60 * def_timestamp_timeout) {
                /* ... 기존 "너무 미래" / boottime 비교 로직은 그대로 유지 ... */
                if (mtime.tv_sec > now + 60 * def_timestamp_timeout * 2) {
                    ...
                    status = TS_MISSING;
                } else if (get_boottime(&boottime) &&
                           timevalcmp(&mtime, &boottime, <)) {
                    status = TS_OLD;
                } else {
                    status = TS_CURRENT;
                }
            }
        }
    }
    /* timevalisset(&mtime)가 거짓이면(=epoch) 이 블록 전체를 건너뛰고
     * status는 여전히 TS_OLD로 남는다 -> 재인증 요구 */
}

즉 수정 전에는 "mtime이 epoch인지"를 시간 차이 계산의 결과로 우연히 통과시킬 수 있는지 여부로 간접적으로만 다뤘다면, 수정 후에는 "mtime이 epoch(설정 안 됨)이면 그 어떤 시각 비교도 하지 않고 무조건 재인증"이라는 명시적인 특수 케이스로 분리했다. 이제 sudo -k로 mtime을 0으로 되돌린 상태는, 시스템 시계가 무엇을 가리키든 상관없이 "인증 정보 없음"과 동일하게 취급된다.

텍스트로 그린 그림 — 패치 후: epoch는 조건 분기 진입 자체를 차단

   2단계: "sudo -k"  ->  mtime.tv_sec = 0
   3단계: 시계를 epoch로 되돌림  ->  now ≈ 0

              timevalisset(&mtime)?
                      │
                      │  mtime == 0 (epoch)  ->  거짓
                      ▼
        ┌─────────────────────────────────────────┐
        │  기존의 "now - mtime < timeout" 비교,     │
        │  "너무 미래" 검사, boottime 비교 전부      │
        │  건너뜀 (겹치는 지점: 여기서 우회가 막힘)  │
        └─────────────────────────────────────────┘
                      │
                      ▼
              status = TS_OLD (그대로 유지)
                      │
                      ▼
            재인증 요구 (비밀번호 다시 입력)

이 수정은 "시계가 조작되었는지 더 정교하게 탐지"하는 방향이 아니라, "epoch는 애초에 시간 비교의 대상이 될 자격이 없다"고 선언해 그 비교 로직 전체의 입력에서 배제하는 방향을 택했다는 점이 특징이다 — 시계 자체를 신뢰할 수 없는 상황에서, 시계 값을 이용한 정교한 판정 대신 "값이 설정된 적 없음"이라는 시계와 무관한 사실만으로 안전한 기본값을 선택한 것이다.

참고 자료

미해결/불확실 지점

  • 원 발견자 Marco Schoepl의 최초 보고 내용(정확한 재현 절차, 최초 보고 일시)은 sudo 버그 트래커/메일에 남아 있을 가능성이 높으나 이번 조사에서는 직접 확인하지 못했고, 공식 권고문에 요약된 내용만 근거로 삼았다.
  • "일부 플랫폼에서 get_boottime()이 실패하거나 지원되지 않는다"는 서술은 패치 커밋의 전후 문맥(이 함수가 && 조건의 앞부분에서 실패를 전제로 분기가 짜여 있다는 코드 구조)에서 추론한 것이며, 2013년 당시 어떤 구체적인 OS/커널 버전에서 이 함수가 실패했는지는 확인하지 못했다.