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
权限提升漏洞分析代码分析漏洞利用逆向工程调试器模糊测试渗透测试学习与教育二进制利用实验室与实践
112144年前尚未审核

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享
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

sudoedit -s '\' `python3 -c "print('A'*80)"`

源码分析(sudo-1.8.21): 首先是sudo.c 中的main 函数(sudo.c: 133):

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):

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):

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):

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):

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 ,但他的堆布局方式可遇不可求,这里详细分析了堆布局方法。通过传入环境变量 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()

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 中的该函数实现就是检索该信息的方法。

接下来看三个结构体:

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;
下载工具