0x00 简介
sudo的一个漏洞:影响范围1.8.2 – 1.8.31p2 以及 1.9.0 -1.9.5p1。非root可以使用sudo来以root的权限执行操作。漏洞本质是由于sudo错误的转义了\过滤掉了截断符号,最终导致了一个堆溢出漏洞。
提权方式:最终通过覆写加载的动态库为自编译的危险文件,实现提权
详细介绍:
在 service_user结构体中指定了要动态加载的动态链接库,如果能够修改 service_user->name,那么就能指定加载伪造的动态链接库。
在一个 service_user结构体前面释放一个堆块,然后 分配 user_args分配到该堆块,随后使用堆溢出覆盖 service_user结构体。在 nssload_libray中,构造了满足调用新动态链接库的条件,所以会通过 ni->name构造动态链接库的名字 shlib_name为 libnss_X/POP_SH3LLZ .so.2。最终会通过 __libc_dlopen(shlib_name)打开。而 libnssX/POP_SH3LLZ .so.2中只含有一个 init函数,该函数的作用就是id(0)调用 execv(‘/bin/sh’),自此完成了提权。
0x01 背景
之前爆出的漏洞,这里对漏洞进行调试分析。
CVE-2021-3156漏洞可以在类uninx中非root可以使用sudo来以root的权限执行操作。漏洞本质是由于sudo错误的转义了\导致了一个堆溢出漏洞。
漏洞影响范围是1.8.2 – 1.8.31p2 以及 1.9.0 -1.9.5p1
本地环境:
0x02 代码分析
1.8.21p2
sudo.c文件:
sudo加上 -s选项会设置 MODE_SHELL,加上 -i选项会设置 MODE_SHELL和 MODE_LOGIN_SHELL。
parse_args.c文件的parse_args函数中:
在 main()(sudo.c)函数中调用了parse_args(),parse_args()会连接所有命令行参数,并加反斜杠来重写 argv。
在 set_cmnd()函数中,首先根据参数使用 strlen()函数计算了参数的 size,再调用 malloc()函数分配了 size大小的堆空间 user_args 。随后判断是否开启了 MODE_SHELL,如果开启了将会连接命令行参数并存入堆空间 user_args。
假如a[0] = “", a[1]= “\x00”, 那么满足if(from[0]==”\“&&isspace(unsigned char)from[1])
while (*from) {
if (from[0] == '\\' && !isspace((unsigned char)from[1])) // a[0]是反斜杠,a[1]是空格则地指针自增1
from++; // from加一则指向null结束符
null结束符号则会被拷贝到user_args堆缓冲区中,from加1,from指向null结束符后面第一个字符。随后会继续将后面的字符拷贝到user_args,发生堆溢出。
0x03 触发漏洞
在 parse_args()会对启用了 -s或 -i的 MODE_SHELL和 MODE_RUN 的 sudo的参数加上 反斜杠 转义。
而 set_cmnd()函数中触发堆溢出前,会判断是否启用了 MODE_SHELL 和 MODE_RUN、MODE_EDIT、MODE_CHECK 中的一个。那么就存在一个矛盾,如果要触发漏洞就需要启用 MODE_SHELL,但是如果启用了 MODE_SHELL,在 parse_args()函数中就会对所有参数转义,触发漏洞的 \,将会被转义为 \,这样就无法触发漏洞了。
// parse_args
if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) { // 检查是否开启了-s或-i的MODE_SHELL
转义 "\"==> "\\"
// soduers_policy_main
if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { // 判断是否开启MODE_SHELL
漏洞代码
那么想要成功执行堆溢出就只能设置flags=MODE_SHELL/MODE_LOGIN_SHELL的条件下不设置mode=MODE_RUN以免转移代码的执行。所以我们只能设置MODE_RUN来避免转义代码的执行,同时为了执行漏洞代码,又需要设置MODE_EDIT或MODE_CHECK。
我们看到如果设置这两个参数和MODE_SHELL/MODE_LOGIN_SHELL,在后续检测会退出
但是如果用sudoedit:
0x04 漏洞调试
1.首先使用如下命令运行 exp:
sudo gdb --args ./sudo-hax-me-a-sandwich 0
2.随后,在 execve下断点:
catch exec
gdb会断在execve函数,我们下断点b setlocale,在继续运行,此时就会停在setlocale函数。该函数是我们在执行sudo最开始会调用的。finish后就可以进入sudo的main函数中。
sudo.c
当main函数加载了sudoer.so后对malloc下断点得到heap的地址
0x05 提权方法
覆盖动态链接库地址
在 service_user结构体中指定了要动态加载的动态链接库,如果能够修改 service_user->name,那么就能指定加载伪造的动态链接库。而 nss_load_library函数就是加载动态链接库的函数,其会调用 __libc_dlopen打开动态库。
了解sevice_library结构体和service_user结构体
typedef struct service_library
{
/* Name of service (`files', `dns', `nis', ...). */
const char *name;
/* Pointer to the loaded shared library. */
void *lib_handle;
/* And the link to the next entry. */
struct service_library *next;
} service_library;
typedef struct service_user
{
/* And the link to the next entry. */
struct service_user *next;
/* Action according to result. */
lookup_actions actions[5];
/* Link to the underlying library object. */
service_library *library;
/* Collection of known functions. */
void *known;
/* Name of the service (`files', `dns', `nis', ...). */
char name[0];
} service_user;
再看看nss_load_library函数
第一个地方要求:ni->library=null
第二个地方可以设置ni->library为name,所以不会退出,初始直接覆盖为null(运行完既为fake libc name的地址)
第三个地方要求:ni->library->lib_handle=null 才能加载新库
第四个地方要求:伪造的库文件名字必须是libnss_xxx.so
第五个地方调用_libc_dlopen来加载动态库
那么我们要思考如何通过一个堆溢出覆盖service_user结构体,思路大致是在分配到service_user这个结构体前面的堆块,通过溢出来修改service_user结构体。
堆排布
由于我们想通过user_args来覆盖到service_user那么需要实现一个好的堆排布,否则距离太远导致覆盖到重要数据可能会令程序崩溃。在这里我们使用setlocale来分配释放堆块,分配释放堆块后由于setlocale在sudo前,那么在分配service_user前可以提前占个堆块的位置,那么就可以在分配user_args时分配在service_user堆地址前,就可以成功溢出到该结构体,修改name为指定lib。
0x555555587360是我们下个分配的堆块
此时我们看看附近的堆排布:
接下来查看service_user结构体位置
search -s systemd [heap]
我们得知在可以溢出的堆块user_args下面最近的是0x5555555873e0位置的service_user结构体
将离 user_args最近的 service_user结构体覆盖后,程序会调用 getgrgid()函数,最后去调用 nss_load_library
溢出后堆内存布局:
我们成功将ni->name改为X/P0P_SH3LLZ_
调用nss_load_library
我们在nss_load_library下个断点:
第一次调用nss_load_library:
被覆盖的那个service_user作为参数
可以看到ni->library被设置为fake libc name
可以看到成功加载了fake libc
总结
1.利用setlocale进行堆占位(堆排布),为了可以分配user_args溢出后面的service_user
2.堆溢出覆盖library为null,name为指定libc名字