CVE-2021-3156 (Sudo Baron Samedit 힙 오버플로우) 분석

이스케이프는 한 문에서만 걸렸다 — 다른 문으로 들어오면 빗장이 없었다

분류: CVE · 커널·OS · 권한상승(LPE) · CVE-2021-3156

리눅스·유닉스 계열의 사실상 표준 권한 상승 도구인 sudo에서, 10년 가까이 잠들어 있던 힙(heap, 프로그램이 실행 중 동적으로 할당하는 메모리 영역) 기반 버퍼 오버플로우가 발견됐습니다. “Baron Samedit”이라는 이름이 붙은 이 결함은 일반 로그인 계정 하나만 있으면 — sudoers 설정에서 그 계정에 아무 권한을 안 줘도 — root 권한을 통째로 넘겨줍니다. 원인은 화려한 메모리 손상 기법이 아니라, 명령줄 인자를 “이스케이프하는 코드”와 “그걸 다시 풀어내는(unescape) 코드”가 서로 다른 조건을 보고 있었다는 사소한 불일치였습니다.

명령줄 인자에 이스케이프 문자를 섞어 넣는다는 아이디어 자체는 낯설지 않습니다. 이런 부류의 취약점 상당수는 입력을 다루는 두 코드 경로가 “같은 상태”를 서로 다르게 해석할 때 생깁니다 — 하나는 “지금 셸을 거쳐 왔다”고 가정하고, 다른 하나는 그 가정 없이 그대로 되감습니다. CVE-2021-3156은 이 어긋남이 실제로 힙 메모리 손상까지 이어진 교과서 같은 사례입니다.

01 개념 30초 — sudo가 인자를 두 번 만지는 이유

sudo는 관리자가 지정한 규칙(sudoers)에 따라 일반 사용자에게 특정 명령을 root 권한으로 실행하게 해 주는 도구입니다. sudo -ssudo -i처럼 셸 모드(MODE_SHELL)로 실행하면, sudo 프론트엔드는 명령줄 인자에 들어있는 특수문자(공백이 아닌 문자 앞)에 백슬래시(\)를 붙여 이스케이프한 뒤 내부로 넘깁니다. 이렇게 하는 이유는 뒤에서 그 인자를 다시 셸에 넘길 때 의도치 않은 해석을 막기 위해서입니다.

문제는 sudo 안에는 이 인자를 쓰는 곳이 최소 두 곳이라는 점입니다.

  1. parse_args() — 명령줄을 처음 파싱하면서 셸 모드일 때 메타문자를 이스케이프.
  2. set_cmnd()(파일 plugins/sudoers/sudoers.c) — sudoers 매칭·로그 기록용으로 user_args 버퍼를 만들면서, 아까 이스케이프된 백슬래시를 다시 제거(unescape).

이스케이프를 “거는” 조건과 그걸 다시 “푸는” 조건이 정확히 같아야만 안전합니다. 그렇지 않으면 이스케이프되지 않은 날것의 입력이 “이스케이프된 입력을 되돌리는” 코드를 그대로 통과하게 됩니다.

02 빈틈 — sudoedit이 두 조건을 갈라놓았다

공개된 기술 분석(Qualys 원 advisory, References 참고)에 따르면 두 조건은 실제로 달랐습니다.

  • 이스케이프를 거는 조건(parse_args()): MODE_RUN이 켜져 있고 동시에 MODE_SHELL일 때만 백슬래시를 붙인다.
  • 이스케이프를 푸는 조건(set_cmnd(), 패치 전 plugins/sudoers/sudoers.c 964행): MODE_SHELL|MODE_LOGIN_SHELL만 보고, MODE_RUN이 켜져 있는지는 확인하지 않는다.

그런데 sudo 바이너리를 sudoedit이라는 이름으로 실행하면(또는 -e 대신 다른 경로로 MODE_EDIT가 켜지면) MODE_RUN은 꺼진 채로 MODE_EDIT가 켜지고, 이때 valid_flags에는 MODE_SHELL이 여전히 남아 있었습니다. 즉 sudoedit -s처럼 “편집 모드인데 셸 플래그도 같이 켜지는” 조합이 가능했고, 이 경로로 들어온 인자는 애초에 이스케이프된 적이 없는데도 set_cmnd()의 unescape 루프를 그대로 통과했습니다. 이스케이프를 거는 문과 푸는 문 사이에 자물쇠가 하나만 달려 있었던 셈입니다.

03 메커니즘 — 백슬래시 하나로 만드는 힙 오버플로우

set_cmnd()의 unescape 루프(패치 전 964~978행)는 각 인자를 훑으면서, \ 뒤에 공백이 아닌 문자가 오면 \를 건너뛰고 그 다음 문자만 복사하는 식으로 동작했습니다. 여기에 명확한 경계 검사(bounds check)가 없었다는 것이 결정적입니다 — 대상 버퍼(user_args)의 크기는 원본 인자 길이의 합으로 미리 계산해 두었을 뿐, 루프 안에서 지금까지 몇 바이트를 썼는지는 확인하지 않았습니다.

핵심은 인자가 백슬래시 하나로 끝나는 경우입니다. 개념적으로 재구성하면(실제 변수명·정확한 바이트 시퀀스는 원문·패치 참고):

# 개념용 단순화 — 실제 소스코드/페이로드 아님
while *from:
    if from[0] == '\\' and not isspace(from[1]):   # from[1]이 '\0'이어도 조건 통과
        from += 1                                   # '\0' 위치로 이동
    *to = *from                                      # '\0'을 한 칸 복사
    to += 1
    from += 1                                        # 문자열 끝을 넘어 계속 읽음
    # 버퍼 남은 크기를 확인하는 코드가 없다

개념용 단순화 — 실제 페이로드/원본 코드 아님. 실제 조건식·바이트 단위 동작은 아래 References의 패치 diff·원문을 참고하세요.

인자가 딱 하나의 백슬래시로 끝나면 from[1]은 문자열 종료 문자(\0)입니다. isspace('\0')는 거짓이므로 “이스케이프된 문자”로 오인해 한 칸 더 건너뛰고, 그 결과 문자열 경계를 넘어선 메모리를 계속 읽고 복사합니다. 반복문은 *from\0을 만날 때까지 도는데, 이 시점부터는 힙에 있는 원래 인자가 아닌 데이터(개념적으로는 뒤이어 배치된 환경변수 등)까지 계속 복사해 버퍼 끝을 넘습니다. Qualys의 원 분석은 이 오버플로우의 크기와 내용을 모두 공격자가 어느 정도 제어할 수 있었다고 설명합니다 — 마지막 명령줄 인자 바로 뒤에 공격자가 지정한 환경변수들이 이어지는 힙 배치를 이용했기 때문입니다.

이 취약점을 트리거하는 데 필요한 것은 sudoers 설정 없이도 로그인 가능한 로컬 계정 하나뿐이었습니다 — sudoers 규칙에 그 사용자가 아예 등재되어 있지 않아도, set_cmnd()는 사용자 인증 이전 단계에서 이미 이 처리를 거쳤기 때문입니다. 이후 힙 오버플로우를 실제 권한 상승으로 잇는 방법은 공개된 세 가지 경로(함수 포인터 변조, NSS 서비스 모듈 로딩 경로 조작, 타임스탬프 디렉터리 심볼릭 링크 경쟁 조건)가 알려져 있습니다(References 참고) — 이 글은 그 실행 단계의 세부는 다루지 않습니다.

04 논평 — 왜 이 실수가 반복되는가

이 취약점의 뿌리는 “메모리를 안전하게 다루지 못했다”보다 한 단계 앞에 있습니다. 같은 입력에 대해 “지금 몇 번째 처리 단계인가”를 코드 경로마다 다른 조건으로 판단했다는 것입니다. 이스케이프를 거는 쪽은 MODE_RUN && MODE_SHELL을 봤고, 푸는 쪽은 MODE_SHELL만 봤습니다. 두 조건이 정확히 같은 진입 경로를 가리킨다는 가정이 sudoedit이라는 대체 진입점 앞에서 깨졌습니다.

이건 파서 두 개가 같은 문자열을 다르게 읽어서 인증이 뚫리는 부류(파서 불일치, parser differential)나, 필터가 재귀적으로 걸리지 않아 중첩 입력으로 되살아나는 부류와 같은 가족입니다. 표면은 완전히 다르지만(메모리 손상 vs 파싱 로직 vs 필터 우회) 구조는 하나입니다 — “이 데이터는 이미 한 번 처리됐다”는 전제가 코드베이스 전체에서 일관되게 지켜지지 않으면, 그 전제를 깨는 진입 경로 하나만 찾아도 방어 전체가 뚫린다. 방어 코드를 감사할 때 “이 검증/이스케이프를 거는 조건”과 “이걸 되돌리거나 신뢰하는 조건”이 문자 그대로 동일한지를 짝지어 확인하는 습관이 이래서 필요합니다. 조건이 하나라도 다르면, 그 차이가 곧 우회 경로입니다.

또 하나 눈여겨볼 점은 진입점이 두 개(‘sudo’, ‘sudoedit’)였다는 것 자체가 공격 표면이었다는 사실입니다. 같은 로직에 도달하는 경로가 여러 개면, 그중 가장 덜 검토된 경로가 전체 보안 수준을 결정합니다. 바이너리 이름·심볼릭 링크·별칭으로 동작 모드가 갈리는 도구는 이 패턴을 특히 조심해야 합니다.

05 남는 교훈

  • 문자열을 이스케이프/언이스케이프하는 코드는 경계 문자(널 종료, 문자열 끝)를 별도로 취급해야 한다. \ 다음 문자를 확인할 때 “다음 문자가 아예 없는 경우”를 빠뜨리면 오프바이원이 난다.
  • 버퍼에 쓰는 루프는 매 반복마다 남은 공간을 확인해야 한다. 시작할 때 한 번 계산한 크기를 믿고 끝까지 안 세는 순간 힙 오버플로우가 열린다.
  • “이 조건이면 이스케이프됨”이라는 가정을 코드베이스 전역에서 조건식 하나로 통일하라. 조건이 파일마다 미묘하게 다르면 그 자체가 취약점이다.

대응방안

  1. 근본 — 안전 버전으로 업데이트. sudo 1.9.5p2 이상(1.8 계열은 1.8.32 이상)으로 업데이트합니다. 이 버전은 unescape 루프에 버퍼 잔여 공간 확인을 추가하고, 언이스케이프 처리 자체를 MODE_RUN이 켜진 경우로만 제한해 sudoedit 경로를 원천 차단했습니다(패치 diff 근거, References 참고). 메이저 배포판(Debian, Fedora, Ubuntu 등)은 2021년 1~2월에 백포트 패치를 냈습니다.
  2. 완화(즉시 업데이트가 어려울 때). setuid(파일을 실행하는 동안 그 소유자 권한, 여기서는 root로 동작하게 하는 권한 비트) 가 걸린 sudo 바이너리를 일시적으로 chmod 0755(setuid 제거)로 바꿔 권한 상승 경로를 차단하거나, sudoedit 심볼릭 링크·별칭 실행을 제한합니다. 다만 이는 sudo 자체를 무력화하는 임시 조치이므로 패치 배포 즉시 원복·업데이트해야 합니다.
  3. 탐지 관점. 짧은 시간 내 반복되는 sudo/sudoedit 비정상 종료(세그폴트), 그리고 sudo 관련 프로세스가 예상치 못한 공유 라이브러리를 로드하거나 /etc/passwd·타임스탬프 디렉터리 권한이 예기치 않게 바뀌는 이벤트를 EDR(Endpoint Detection and Response, 엔드포인트 이상행위 탐지 도구)·감사 로그에서 살핍니다. CISA는 이 취약점을 실제 악용 사례가 확인된 KEV(Known Exploited Vulnerabilities) 목록에 등재했습니다(References 참고) — 패치 적용 여부를 자산 인벤토리에서 우선순위로 추적할 가치가 있습니다.

결론

한 줄 요약. sudo는 인자를 “이스케이프하는 코드”와 “다시 푸는 코드”를 따로 두었는데, 두 코드가 같은 조건을 서로 다르게 해석하는 문 하나(sudoedit)가 남아 있었습니다. 그 문으로 들어온 인자는 이스케이프된 적이 없는데도 언이스케이프 로직을 통과해 힙 밖으로 흘러넘쳤습니다.

이 한 틈은 2021년에 고쳐졌지만, “검증을 거는 조건과 그 검증 결과를 신뢰하는 조건이 코드 곳곳에서 정확히 일치하는가”라는 질문은 다른 파서·다른 언어·다른 프로토콜에서 계속 되풀이됩니다. 그래서 우리는 이 CVE 하나가 아니라 그 질문을 남깁니다.

References / 참고자료

  1. Qualys Security Advisory, “Heap-Based Buffer Overflow in Sudo (CVE-2021-3156)” — 이 글이 재구성한 원 기술분석. 근본원인(이스케이프/언이스케이프 조건 불일치)·오프바이원 메커니즘·세 가지 권한 상승 경로의 1차출처. https://www.openwall.com/lists/oss-security/2021/01/26/3
  2. sudo-project/sudo, 패치 커밋 “Fix potential buffer overflow when unescaping backslashes in user_args”plugins/sudoers/sudoers.c 실제 패치 diff(부모 커밋 c4d3840, 패치 커밋 1f86385). https://github.com/sudo-project/sudo/commit/1f8638577d0c80a4ff864a2aad80a0d95488e9a8
  3. NVD — CVE-2021-3156. CVSS 3.1 AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H(7.8, HIGH), CWE-193(오프바이원 오류), 영향 버전(1.8.2–1.8.31, 1.9.0–1.9.5p1). https://nvd.nist.gov/vuln/detail/CVE-2021-3156
  4. sudo 프로젝트, Stable Release History — 수정 버전 1.9.5p2 릴리스 노트. https://www.sudo.ws/stable.html#1.9.5p2
  5. CISA, Known Exploited Vulnerabilities Catalog — CVE-2021-3156 KEV 등재(실악용 확인, 대응 기한 명시). https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2021-3156

이 글은 위 1차출처(Qualys advisory·sudo 프로젝트 GitHub 패치 커밋·NVD·sudo.ws·CISA)를 우리 관점에서 다시 쓰고 논평을 더한 것입니다. 특정 문장·코드를 그대로 옮기지 않았으며, 작동하는 익스플로잇/페이로드는 싣지 않았습니다. 코드 예시는 개념 설명용입니다. 이 초안은 RN-11 독립 팩트체크(sudo-project/sudo GitHub 원문·NVD API 직접 대조, 2026-08-31)를 통과했습니다.