Joe1sn's Cabinet

SUDO堆溢出提权:从fuzz到exp [2]

前文:https://blog.joe1sn.top/2022/01/04/CVE-2021-3156/

受到youtuber:LiveOverflow的系列教程的启发,我发现在中文互联网上并没有相关的翻译教程,所以我想以实验报告的形式来创造这个从fuzz到exp的系列图文教程

原始视频合集:https://www.youtube.com/watch?v=uj1FTiczJSE&list=PLhixgUqwRTjy0gMuT4C3bmjeZjuNQyqdx

原始Blog:https://liveoverflow.com/why-pick-sudo-research-target-part-1/

原作者代码仓库:https://github.com/LiveOverflow/pwnedit

My previous blog: https://blog.joe1sn.top/2022/01/04/CVE-2021-3156/

I was inspired by the LiveOverflow’s Sudo Vulnerability Walkthrough on youtube, but i found there’s no Chinese version of this walkthrough tutorial, so i decided to write in experimental report way to create this “from fuzz to exploit” series.

Original Videos: https://www.youtube.com/watch?v=uj1FTiczJSE&list=PLhixgUqwRTjy0gMuT4C3bmjeZjuNQyqdx

Original Blog: https://liveoverflow.com/why-pick-sudo-research-target-part-1/

Source Project Code: https://github.com/LiveOverflow/pwnedit


本节内容:

Troubleshooting AFL Fuzzing Problems | Ep. 03

Finding Buffer Overflow with Fuzzing | Ep. 04

Found a Crash Through Fuzzing? Minimize AFL Testcases! | Ep. 05

Root Cause Analysis With AddressSanitizer (ASan) | Ep. 06

Understanding C Pointer Magic Arithmetic | Ep. 07

C Code Review - Reaching Vulnerable Code in sudo | Ep. 08

解决AFL的小麻烦

因为时间原因,我并不能一直开着电脑跑,不过我翻译一下作者遇到的问题

No more free CPU cores

作者在遇到fuzz很慢的时候,尝试关闭一个fuzz,然后重启

然后使用ps aux产看全部运行过程,发现afl在尝试fuzz这些奇怪的东西(因为sudo中可能会有exec之类的)。然后pkill vi关闭所有vi的进程就短暂的解决了这个问题。

**解决:**彻底解决的话要关闭所有在sudo中的exec相关函数,然后重新编译

And of Disk Space

Today's surprising error message: we ran out of disk space overnight!

作者查看空间使用情况过后发现磁盘空间充足,但是任然不能创建文件

但是使用df -i查看inode节点,发现被占满了

**inode (index node)**是指在许多“类Unix文件系统”中的一种数据结构,用于描述文件系统对象(包括文件目录设备文件socket管道等)。每个inode保存了文件系统对象数据的属性和磁盘块位置[1]文件系统对象属性包含了各种元数据(如:最后修改时间) ,也包含用户组(owner )和权限数据

说明有过多的细小文件使用光了inode节点号,最后在/var/tmp找到了这些文件,原因是fuzz的时候产生了例如../../的路径穿越。

**解决:**手动在sudo要创建文件的时候添加上一个crash,这里用空指针引用

1
2
printf("mk tmp file(%s)\n",stuff);
*(int *)0=0;

image-20220413123850967

image-20220413125559550

之后开始fuzz

image-20220413130155444

然后分析crash

image-20220413132119763

但是又引入了新的问题:

root和普通用户相同吗?

这里就要说到sudo的原理,sudo是通过在root条件下使用setuid的方式来让普通用户指令得到root执行。

比如我们在user下fuzz,但是真实情况会将它变为root下运行

image-20220413130808491

如果要在fuzz时实现真实情况的效果,那么就要将当前用户uid设置为普通用户的

sudo-1.8.31p2/src/sudo.c get_user_info

1
2
3
4
ud->uid = 1000//getuid();
ud->euid = geteuid();
ud->gid = 1000//getgid();
ud->egid = getegid();

image-20220413131502294

忘写分号了

image-20220413132555058

找到缓冲区溢出

作者用上一节的fuzz得到了一些ctash样本,本章内容讲的基本上是分析这些样本

gdb调试

和我预料的一样,这样做会产生大量的非sudo从而crash的样本,可以用以下命令查看

1
2
grep -R sudoedit file_floder/
grep -R sudo file_floder/

为了方便分析,可以安装一些gdb的插件,如pwndbg,也在CVE分析的文章里讲过了该插件的安装(不要放在共享文件夹/pwd下安装)

有的crash是由于fuzzer的错误引起的,作者使用了这段代码判断

1
2
3
4
5
6
7
#include "argv-fuzz-inl.h"

int main(int argc, char *argv[], char *envp[])
{
AFL_INIT_ARGV(); // argv is now the fake argv
execve("/usr/local/bin/sudo", argv, envp);
}

作者遇到的第一个问题是argv-fuzz-inl中的ret数组造成的栈溢出,覆写了其他的函数指针造成crash

image-20220413134205097

解决 如果rc比最大参数数量大时退出循环

image-20220413134751535

更换fuzzer

使用更好的fuzzer:AFL++ 项目地址:https://github.com/AFLplusplus/AFLplusplus

AFL++支持对命令行的fuzz,所以之前的修改要去掉

要新建镜像的话,可以在Dockerfile中加上

1
RUN cd /root/ && git clone https://github.com/AFLplusplus/AFLplusplus && cd AFLplusplus && make source-only && make install
1
2
3
4
5
6
7
8
9
10
11
FROM ubuntu:20.04
ENV LC_CTYPE C.UTF-8
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -yq gcc make wget curl git vim gdb clang llvm lld llvm-dev bsdmainutils libstdc++-10-dev python3 python3-pip python3-dev automake flex bison build-essential libglib2.0-dev libpixman-1-dev python3-setuptools
RUN cd /root/ && wget https://www.sudo.ws/dist/sudo-1.8.31p2.tar.gz && tar -xvf sudo-1.8.31p2.tar.gz && cd sudo-1.8.31p2 && ./configure && make && make install
RUN cd /root/ && git clone https://github.com/AFLplusplus/AFLplusplus && cd AFLplusplus && make source-only && make install
RUN useradd -ms /bin/bash user
RUN echo 'export PS1="\[\e]0;\u@\h: \w\a\]\[\033[01;31m\]\u\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]# "' >> /root/.bashrc
RUN echo 'export PS1="\[\e]0;\u@\h: \w\a\]\[\033[01;32m\]\u\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]$ "' >> /home/user/.bashrc
USER user
WORKDIR /home/user

重新编译

1
2
3
whereis afl-clang-fast
ls -lah /usr/local/bin/afl-clang-fast
CC=afl-cc ./configure --disable-shared && make -j8

image-20220413150943670

开始fuzz,指令-T参数可以指定argv[0]

1
afl-fuzz -i /tmp/in/ -o /tmp/out/ -T sudoedit ./src/sudo

我这里故意放了能够引起crash的样本进去只为了加速过程

image-20220413162529243

分析新的crash

判断是否为误报

我直接使用作者的crash文件,你可以在:https://github.com/LiveOverflow/pwnedit/tree/main/episode05 中找到

id_000000,sig_06,src_000083+000451,time_23448104,op_splice,rep_8

image-20220413162911500

检验下在我的环境里面是否会有crash

root

image-20220413163015370

user

image-20220413164830186

gdb调试

原视频里面用的是GEF,这里我用pwndbg,新人(没有CTFpwn经验)建议用GEF

image-20220413165152014

程序自动运行后停止了

image-20220413165314583

说明这个错误是被malloc给抛出的

**这会是一个新的0day吗?**在最新平台上测试后发现并不是

简化crash

其实我做到这一步想到的是用afl-tmin,后来发现作者尝试其他方案失败后,我就直接用afl-tmin

image-20220413172941195

user下检验

  1. 创建软链接

    1
    2
    ln -s /usr/local/bin/sudo 0edit
    ls -lah 0edit

    image-20220413173329188

  2. 运行测试

    image-20220413173442633

  3. 有趣的发现

    image-20220413174215939

    结尾是xedit这种形式就可以调用sudoedit

使用ASAN分析漏洞

asan一直是一个很操蛋的工具,经常报错,作者也在这里报错很多,我也是直接展示正常(正常报错)做法

1
make clean && ./configure CFLAGS="-fsanitize=address,undefined -g" LDFLAGS="-fsanitize=address,undefined" CC=clang --disable-shared && make -j8

送入mini_crash样例检测

image-20220413175350857

如果没有加上--disable-shared的话,就算有-g参数,也不会知道具体代码在哪里

现在我们知道漏洞的位置在/plugins/sudoers/./sudoers.c:868set_cmnd函数内

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
static int
set_cmnd(void)
{
struct sudo_nss *nss;
char *path = user_path;
int ret = FOUND;
debug_decl(set_cmnd, SUDOERS_DEBUG_PLUGIN)

/* Allocate user_stat for find_path() and match functions. */
user_stat = calloc(1, sizeof(struct stat));
if (user_stat == NULL) {
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
debug_return_int(NOT_FOUND_ERROR);
}

/* Default value for cmnd, overridden below. */
if (user_cmnd == NULL)
user_cmnd = NewArgv[0];

if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
if (ISSET(sudo_mode, MODE_RUN | MODE_CHECK)) {
if (def_secure_path && !user_is_exempt())
path = def_secure_path;
if (!set_perms(PERM_RUNAS))
debug_return_int(-1);
ret = find_path(NewArgv[0], &user_cmnd, user_stat, path,
def_ignore_dot, NULL);
if (!restore_perms())
debug_return_int(-1);
if (ret == NOT_FOUND) {
/* Failed as root, try as invoking user. */
if (!set_perms(PERM_USER))
debug_return_int(-1);
ret = find_path(NewArgv[0], &user_cmnd, user_stat, path,
def_ignore_dot, NULL);
if (!restore_perms())
debug_return_int(-1);
}
if (ret == NOT_FOUND_ERROR) {
if (errno == ENAMETOOLONG)
audit_failure(NewArgc, NewArgv, N_("command too long"));
log_warning(0, "%s", NewArgv[0]);
debug_return_int(ret);
}
}

/* set user_args */
if (NewArgc > 1) {
char *to, *from, **av;
size_t size, n;

/* Alloc and build up 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';
}
}
}

if ((user_base = strrchr(user_cmnd, '/')) != NULL)
user_base++;
else
user_base = user_cmnd;

/* Convert "sudo sudoedit" -> "sudoedit" */
if (ISSET(sudo_mode, MODE_RUN) && strcmp(user_base, "sudoedit") == 0) {
CLR(sudo_mode, MODE_RUN);
SET(sudo_mode, MODE_EDIT);
sudo_warnx(U_("sudoedit doesn't need to be run via sudo"));
user_base = user_cmnd = "sudoedit";
}

TAILQ_FOREACH(nss, snl, entries) {
if (!update_defaults(nss->parse_tree, NULL, SETDEF_CMND, false)) {
log_warningx(SLOG_SEND_MAIL|SLOG_NO_STDERR,
N_("problem with defaults entries"));
}
}

debug_return_int(ret);
}

漏洞造成的原因在CVE那篇文章分析过了

简化漏洞模型

这里的视角更像是给CTF出题,我也确实一句这个漏洞出过一道,不过在这里我们后面会完成exp的编写,所以只写c程序分析就行了

精简一下上面的源代码,问题出现在这里

1
2
3
4
5
6
7
8
9
10
11
if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
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';
}
  • 如果最后一个参数是\的话,from++
  • 然后*to++ = *from++,此时的from指针就超出了边界,造成堆溢出

可以写一个小的例子调试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>

int main()
{
char from[100];
puts("please input some data, max is 100");
read(0,from,100);
int len = strlen(from);

char *src = from;
char *to = (char *)malloc(len);
char *dst = to+1;

puts("start copy file");

while(*src){
if (src[0] == '\\' && !isspace((unsigned char)src[1]))
src++;
*dst++ = *src++;
}
*to++ = '\n';

printf("src> %s",from);
printf("dst> %s",to);
}

构造这样的特殊输入,在输入的时候已经输入0x18个字符串了,所以是malloc(0x18)

image-20220413183616868

按照程序的效果,会将下一个chunk的头部份覆写为0xbbbbbbbb

image-20220413183740441

成功覆盖掉,实现预期堆溢出的目标,说明当结尾的反斜杠后面还有数据的时候会产生堆溢出