Skip to content
KitploitKITPLOIT
도구익스플로잇블로그
Log in
제출
도구익스플로잇블로그
제출

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

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

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

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2021-3156 — CVE-2021-3156 POC, Docker 및 분석 정리 | Kitploit
도구/GitHubGitHub/chenaotian/cve-2021-3156
Privilege EscalationVulnerability AnalysisCode AnalysisExploitationReverse EngineeringDebuggersFuzzingPenetration TestingLearning & EducationBinary ExploitationLabs & Practice
112154년 전아직 검토되지 않음

인기

모두 보기 →

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

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유
GitHub
chenaotian/cve-2021-3156

CVE-2021-3156

CVE-2021-3156 POC, Docker 및 분석 정리

저장소 보기

CVE-2021-3156

[toc]

취약점 개요

취약점 번호: CVE-2021-3156

취약점 점수:

취약점 제품: linux sudo

영향 범위: 1.8.2-1.8.31sp12; 1.9.0-1.9.5sp1

이용 조건: linux 로컬; sudo가 suid이고 실행 가능해야 함

이용 효과: 로컬 권한 상승

소스 코드 획득: https://www.sudo.ws/getting/source/

환경 구축

docker 환경: chenaotian/cve-2021-3156

제가 직접 구축한 docker는 다음을 제공합니다:

  1. 직접 컴파일한 소스 코드 디버깅 가능한 sudo
  2. 디버그 심볼이 있는 glibc
  3. gdb 및 gdb 플러그인 pwngdb & pwndbg
  4. exp.c 및 컴파일된 exp

모든 것은 /root 디렉토리에 있습니다:

image-20220124223312224

  • exp 디렉토리는 exp 코드와 컴파일된 파일이 있는 디렉토리이며, 해당 docker에서 바로 실행할 수 있습니다.
  • glibc-2.27은 이 환경의 libc 버전 소스 코드 디렉토리입니다.
  • sudo-1.8.21은 이 환경의 sudo 소스 코드 디렉토리이며, 저는 이것으로 컴파일했습니다.

테스트 exp:``` cd exp su test ./exp whoami

调试相关的内容见后文[一些调试命令](#一些调试命令)


## 취약점 원리

취약점 트리거 payload```shell
sudoedit -s '\' `python3 -c "print('A'*80)"`

소스 코드 분석(sudo-1.8.21): 먼저 sudo.c의 main 함수(sudo.c: 133):```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)

··· ···
··· ···

/* Parse command line arguments. */
//在这里处理输入参数,设置sudo_mode
sudo_mode = parse_args(argc, argv, &nargc, &nargv, &settings, &env_add);

··· ···
··· ···
    
switch (sudo_mode & MODE_MASK) {
··· ···
··· ···
case MODE_EDIT:
case MODE_RUN:
    ok = policy_check(&policy_plugin, nargc, nargv, env_add,
	&command_info, &argv_out, &user_env_out);
    ··· ···
    ··· ···
}

··· ···
··· ···

}

- 먼저 `parse_args` 함수를 호출하여 입력한 인수를 처리합니다. 사실 여기서는 `-s` 하나만 입력했을 뿐 설정할 것이 없으며, `sudo_mode`를 `MODE_EDIT` 및 `MODE_SHELL`로 설정합니다.

- 그런 다음 `sudo_mode`에 따라, `MODE_EDIT`은 `policy_check`를 호출합니다.

다음은 `sudo.c`의 `policy_check` 함수입니다(sudo.c: 1136):```c
static int
policy_check(struct plugin_container *plugin, int argc, char * const argv[],
    char *env_add[], char **command_info[], char **argv_out[],
    char **user_env_out[])
{
    ··· ···
    ··· ···
    ret = plugin->u.policy->check_policy(argc, argv, env_add, command_info,
	argv_out, user_env_out);
    ···
}

콜백 함수 plugin->u.policy->check_policy가 호출되었습니다. 이 함수의 실제 함수를 디버깅하여 확인할 수 있습니다:

image-20220123113326096

호출되는 것은 policy.c의 sudoers_policy_check 함수입니다(policy.c: 760):```c static int sudoers_policy_check(int argc, char * const argv[], char *env_add[], char **command_infop[], char **argv_out[], char **user_env_out[]) { ··· ···

exec_args.argv = argv_out;
exec_args.envp = user_env_out;
exec_args.info = command_infop;

ret = sudoers_policy_main(argc, argv, 0, env_add, &exec_args);
··· ···
··· ···

}

그런 다음 sudoers.c의 sudoers_policy_main 함수를 호출했습니다(sudoers.c: 224):```c
int
sudoers_policy_main(int argc, char * const argv[], int pwflag, char *env_add[],
    void *closure)
{
    ··· ···
    ··· ···

    /*
     * Make a local copy of argc/argv, with special handling
     * for pseudo-commands and the '-i' option.
     */
    if (argc == 0) {
	··· ···
    } else {
	/* Must leave an extra slot before NewArgv for bash's --login */
	NewArgc = argc;
	NewArgv = reallocarray(NULL, NewArgc + 2, sizeof(char *));
	··· ···
	}
	memcpy(++NewArgv, argv, argc * sizeof(char *));
	NewArgv[NewArgc] = NULL;
	··· ···
	}
    }
	··· ···
    cmnd_status = set_cmnd();
    ··· ···
    ··· ···
    ··· ···
}

여기에 일부 전역 변수인 NewArgc와 NewArgv가 설정되어 있습니다. 아래와 같으며, 실제로는 전달된 매개변수입니다.

image-20220123113819116

이후 sudoers.c의 set_cmnd 함수(sudoers.c: 796)로 진입합니다.```c static int set_cmnd(void) { ··· ··· ··· ···

/* set user_args */
if (NewArgc > 1) {
    char *to, *from, **av;
    size_t size, n;

    /* Alloc and build up user_args. */
    //根据参数总长度计算size, 后续malloc 申请,没有问题
    for (size = 0, av = NewArgv + 1; *av; av++)
	size += strlen(*av) + 1;
    if (size == 0 || (user_args = malloc(size)) == NULL) {
	sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
	debug_return_int(-1);
    }
    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
	/*
	 * When running a command via a shell, the sudo front-end
	 * escapes potential meta chars.  We unescape non-spaces
	 * for sudoers matching and logging purposes.
	 */
     //将所有参数拷贝到一起放到堆中,逻辑是遇到'\'加非空格类型字符则只拷贝非空格字符
     //但这里\x00 并不算空格类型字符
     //他没有考虑参数如果只有一个'\'或以'\'结尾并且下两个字符后就是另一个字符串情况
	for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
	    while (*from) {
		if (from[0] == '\\' && !isspace((unsigned char)from[1]))
		    from++;
		*to++ = *from++;
	    }
	    *to++ = ' ';
	}
	*--to = '\0';
    } 
    ··· ···
}
}
··· ···
··· ···

}

溢出也发生在这里,根据代码中的注释可以看出,堆溢出发生在向堆中拷贝时,这段代码的原意不难理解就是将NewArgv中的所有参数都拷贝到堆中,空格分割,遇到`\+非空格类字符` 则只拷贝该字符。

**但它没有考虑到一种情况就是,某个NewArgv元素是以`\` 结尾,那么就是`\+\x00` 这种结构,而`\x00` 是不属于空格类字符的(离谱),也就是说,它会将`\x00` 拷贝到堆中之后,from 变量再 ++ (一个循环中加了两次)直接过了while 判断结束标记`\x00` 的机会,而认为参数没有拷贝完而继续向后拷贝,直到遇到下一个`\x00` 为止。**

在该场景下可以看到 `\+\x00` 后面紧跟着就是下一个参数 `A*80` 所以会继续拷贝到`A*80` 的结尾。但别忘了接下来还会继续真正处理`A*80` 这个参数,还会再拷贝一遍,所以这里总共对`A*80` 进行了两次拷贝,但chunk 的申请时按照只有一个 `A*80` 字符串的大小申请的,远远超过了chunk 申请的长度。

image-20220123113907744

然后造成溢出,拷贝前:

image-20220123114036691

拷贝后:

image-20220123114137794

总体漏洞触发路径为(调试的时候直接根据这几个函数下断点即可):

- sudo.c : main
  - sudo.c : policy_check
    - policy.c : sudoerrs_policy_check
      - sudoers.c : sudoers_policy_main
        - sudoers.c : set_cmnd
          - sudoers.c : 859

## 漏洞利用原理

参考了 [blasty/CVE-2021-3156](https://github.com/blasty/CVE-2021-3156) ,**但他的堆布局方式可遇不可求,这里详细分析了堆布局方法**。通过传入环境变量 `LC_*` 来布局堆,然后让溢出的chunk 正好覆盖到 nss_load_library 函数需要加载so 的结构体 service_user,覆盖该结构体中的so 名字符串,然后让程序加载我们指定的so来完成任意代码执行。

虽然逻辑看起来挺清晰,但需要搞定的细节还是比较麻烦的:

1. nss_load_library 中相关数据结构和机制
2. setlocale 如何通过环境变量`LC_*` 进行堆布局

接下来我们将漏洞发生出可以溢出的chunk 称之为vuln chunk,而将溢出的目标称为target chunk

### nss 原理

首先查看漏洞利用关键代码:

glibc/nss/nsswitch.c: 377 nss_load_library()```c
static int
nss_load_library (service_user *ni)
{
  if (ni->library == NULL)
    {
      static name_database default_table;
      ni->library = nss_new_service (service_table ?: &default_table,
				     ni->name);
      if (ni->library == NULL)
	return -1;
    }

  if (ni->library->lib_handle == NULL)
    {
      ··· ···
      __stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,
					      "libnss_"),
				    ni->name),
			  ".so"),
		__nss_shlib_revision);

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

ni는 힙 위의 service_user 구조체입니다. ni->library->lib_handle이 NULL일 때 __libc_dlopen이 호출되어 so를 로드합니다. 우리가 ni가 있는 힙 청크로 오버플로우할 수 있다면, library를 0으로 덮어쓰기만 하면 됩니다. 첫 번째 분기에서 library가 NULL이면 초기화되지 않은 것을 의미하며, nss_new_service가 호출되어 library를 초기화하고, 막 초기화된 handle은 반드시 NULL이기 때문입니다.

ok, 취약점 활용의 핵심 트리거 포인트를 알았으니, 이제 nss의 메커니즘에 대해 알아보겠습니다.

먼저 /etc/ 디렉토리 아래에 /etc/nsswitch.conf 파일이 있습니다(일반적으로 이런 모양이며, 모든 장치에서 동일하지는 않습니다):```

/etc/nsswitch.conf

Example configuration of GNU Name Service Switch functionality.

If you have the glibc-doc-reference' and info' packages installed, try:

`info libc "Name Service Switch"' for information about this file.

passwd: compat systemd group: compat systemd shadow: compat gshadow: files

hosts: files dns networks: files

protocols: db files services: db files ethers: db files rpc: db files

netgroup: nis

这是一个配置文件,通过这里记录的这些途径和顺序(其实就是用哪些so)来查找方法。还可以指定某个方法奏效时或失效时系统将采取什么动作。

我理解的就是,规定程序需要从哪里检索所需信息,比如用户信息、网络、地址信息等。程序中的体现就是,是从某个不同的so 中调用该函数。不同so 中的该函数实现就是检索该信息的方法。
도구 다운로드