标签 Vulnerability Analysis 下的文章
议题分享: 企业设备安全设备漏洞分析与利用
议题分享: 企业设备安全设备漏洞分析与利用
CVE-2025-36463 Sudo_chroot Elevation of Privilege 漏洞分析
CVE-2025-36463 Sudo_chroot Elevation of Privilege 漏洞分析
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 | int |
代码逻辑大致是:
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 | static bool nss_database_check_reload_and_get |
我们在 pivot_root 之后对 nss_database_check_reload_and_get 下个断点,此时 gdb 的backtrace 如下:
1 | Breakpoint 1, nss_database_check_reload_and_get (local=0x5555555a1ad0, result=0x7fffffffc510, database_index=nss_database_initgroups) |
当前 nss_database_check_reload_and_get 的第三个参数 database_index 为 nss_database_initgroups, local 参数结构:
1 | (gdb) p *local |
其中 services 对应如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17DEFINE_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 | for (int i = 0; i < NSS_DATABASE_COUNT; ++i) |
由 nss_database_select_default 获取然后设置
1 | static const char per_database_defaults[NSS_DATABASE_COUNT] = |
在 nss_database_initgroups 设置的时候,默认为 None, 因此此时 service 为 nss_database_initgroups 是 0x0 (这个很重要)
1 | (gdb) p *local |
解释了下,此时((struct nss_database_state *)local)->data.services[nss_database_initgroups]为空的原因,我们接着回到 nss_database_check_reload_and_get的代码里:
1 |
|
在刚进 nss_database_check_reload_and_get 函数的时候, 先是判断 local->data.reload_dsiable
是否为 True, 如果为True 则直接 return
1 | if (atomic_load_acquire (&local->data.reload_disabled)) |
然后是判断/etc/nsswitch.conf文件是否修改:
1 | struct file_change_detection initial; |
因为此时是刚 chroot 进来, 所以此时的 /etc/nsswitch.conf是一个修改的状态,所以代码会继续往下走。然后是一个重点逻辑, 如果代码判断成功,则设置 local->data.reload_disabled 的值
1 | if (local->data.services[database_index] != NULL) |
因为当前 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 | (gdb) p ((struct nss_database_state *)local)->data.reload_disabled |
然后保存当前的 root inode 和 root dev
1 | if (stat_rv == 0) |
最后就走到 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 | >end |
我们可以清楚的看到在 pivot_root 和 unpivot_root 前后 nss_database_check_reload_and_get 的参数不同:
1 | Breakpoint 3.2, pivot_root (new_root=0x5555555a701c "woot", state=0x7fffffffcc38) at ./pivot.c:39 |
整理出来就是:
1 | nss_database_passwd 9 |
在章节 ”nss_database_check_reload_and_get 分析“的时候我们知道 nss_database_initgroups的时候 reload_disabled 不会设置。
当到第一个 nss_database_group 的时候, 由于文件没有修改, 所以会直接 return。
1 | (gdb) n |
不会走后续的逻辑。
当走完 unpivot_root 来到第二个nss_database_group, reload_disabled 没有设置, 走到文件修改比较。 因为此时已经 unpivot_root, 因此文件是有变化的, 程序会继续执行。
当走到 if (local->data.services[database_index] != NULL) 判断的时候
1 | if (local->data.services[database_index] != NULL) |
由于 local->data.services[database_index] 不为空, 因此会进入 if 的逻辑。 且此时
1 | stat_rv = 0 |
符合这个 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 |
|
在不可信任的路径里配置一个 etc/nsswitch.conf, 内容如下:
1 | bash-5.2$ cat woot/etc/nsswitch.conf |
一个有趣的说明,nsswitch.conf中的源的名称也被用作共享对象(库)的路径的一部分。例如,上述LDAP源转化为 libnss_/woot1337.so.2.so。
那么在哪里加载恶意 so 的呢? 我们对 dlopen 下一个断点, 然后查看一下他的 backtrace。
1 | #0 0x00007ffff7e86191 in woot () from libnss_/woot1337.so.2 |
从这个调用链,我们就很清楚的知道了是在 setspent 之后进行的 dlopen 加载恶意的 so
1 | policy_check -> sudoers_policy_check -> sudoers_check_cmnd |
那么 setspent 做了什么呢? setspent 函数会用来打开 shadows 文件的方法一个使用的例子
1 |
|
setspent 实现代码[3]
1 | void |
当调用到module_load的时候就会加载 so
1 | /* Internal implementation of __nss_module_load. */ |
复现
Patched
修复 commit [5]:
1 | --- sudo-1.9.17/plugins/sudoers/sudoers.c 2025-06-12 12:12:38.000000000 -0500 |
删除了 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.https://www.stratascale.com/vulnerability-alert-CVE-2025-32463-sudo-chroot↩
- 2.https://codebrowser.dev/glibc/glibc/nss/nss_database.c.html#nss_database_check_reload_and_get↩
- 3.https://codebrowser.dev/glibc/glibc/nss/getXXent_r.c.html#122↩
- 4.https://github.com/bminor/glibc/commit/fa78feca47fdc226b46e7f6fea4c08c10fccd182↩
- 5.https://github.com/sudo-project/sudo/commit/fdafc2ceb36382b07e604c0f39903d56bef54016#diff-6a3fc5e12751032d02db8970967b688eab54525c326699010870b3ffca2b6541↩
- 6.https://github.com/sudo-project/sudo/commit/bc88e5cbd3b41196cac727855e2446a02dfba51e↩
议题分享: 企业设备安全设备漏洞分析与利用
CVE-2025-36463 Sudo_chroot Elevation of Privilege 漏洞分析
CVE-2025-36463 Sudo_chroot Elevation of Privilege 漏洞分析
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 | int |
代码逻辑大致是:
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 | static bool nss_database_check_reload_and_get |
我们在 pivot_root 之后对 nss_database_check_reload_and_get 下个断点,此时 gdb 的backtrace 如下:
1 | Breakpoint 1, nss_database_check_reload_and_get (local=0x5555555a1ad0, result=0x7fffffffc510, database_index=nss_database_initgroups) |
当前 nss_database_check_reload_and_get 的第三个参数 database_index 为 nss_database_initgroups, local 参数结构:
1 | (gdb) p *local |
其中 services 对应如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17DEFINE_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 | for (int i = 0; i < NSS_DATABASE_COUNT; ++i) |
由 nss_database_select_default 获取然后设置
1 | static const char per_database_defaults[NSS_DATABASE_COUNT] = |
在 nss_database_initgroups 设置的时候,默认为 None, 因此此时 service 为 nss_database_initgroups 是 0x0 (这个很重要)
1 | (gdb) p *local |
解释了下,此时((struct nss_database_state *)local)->data.services[nss_database_initgroups]为空的原因,我们接着回到 nss_database_check_reload_and_get的代码里:
1 |
|
在刚进 nss_database_check_reload_and_get 函数的时候, 先是判断 local->data.reload_dsiable
是否为 True, 如果为True 则直接 return
1 | if (atomic_load_acquire (&local->data.reload_disabled)) |
然后是判断/etc/nsswitch.conf文件是否修改:
1 | struct file_change_detection initial; |
因为此时是刚 chroot 进来, 所以此时的 /etc/nsswitch.conf是一个修改的状态,所以代码会继续往下走。然后是一个重点逻辑, 如果代码判断成功,则设置 local->data.reload_disabled 的值
1 | if (local->data.services[database_index] != NULL) |
因为当前 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 | (gdb) p ((struct nss_database_state *)local)->data.reload_disabled |
然后保存当前的 root inode 和 root dev
1 | if (stat_rv == 0) |
最后就走到 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 | >end |
我们可以清楚的看到在 pivot_root 和 unpivot_root 前后 nss_database_check_reload_and_get 的参数不同:
1 | Breakpoint 3.2, pivot_root (new_root=0x5555555a701c "woot", state=0x7fffffffcc38) at ./pivot.c:39 |
整理出来就是:
1 | nss_database_passwd 9 |
在章节 ”nss_database_check_reload_and_get 分析“的时候我们知道 nss_database_initgroups的时候 reload_disabled 不会设置。
当到第一个 nss_database_group 的时候, 由于文件没有修改, 所以会直接 return。
1 | (gdb) n |
不会走后续的逻辑。
当走完 unpivot_root 来到第二个nss_database_group, reload_disabled 没有设置, 走到文件修改比较。 因为此时已经 unpivot_root, 因此文件是有变化的, 程序会继续执行。
当走到 if (local->data.services[database_index] != NULL) 判断的时候
1 | if (local->data.services[database_index] != NULL) |
由于 local->data.services[database_index] 不为空, 因此会进入 if 的逻辑。 且此时
1 | stat_rv = 0 |
符合这个 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 |
|
在不可信任的路径里配置一个 etc/nsswitch.conf, 内容如下:
1 | bash-5.2$ cat woot/etc/nsswitch.conf |
一个有趣的说明,nsswitch.conf中的源的名称也被用作共享对象(库)的路径的一部分。例如,上述LDAP源转化为 libnss_/woot1337.so.2.so。
那么在哪里加载恶意 so 的呢? 我们对 dlopen 下一个断点, 然后查看一下他的 backtrace。
1 | #0 0x00007ffff7e86191 in woot () from libnss_/woot1337.so.2 |
从这个调用链,我们就很清楚的知道了是在 setspent 之后进行的 dlopen 加载恶意的 so
1 | policy_check -> sudoers_policy_check -> sudoers_check_cmnd |
那么 setspent 做了什么呢? setspent 函数会用来打开 shadows 文件的方法一个使用的例子
1 |
|
setspent 实现代码[3]
1 | void |
当调用到module_load的时候就会加载 so
1 | /* Internal implementation of __nss_module_load. */ |
复现
Patched
修复 commit [5]:
1 | --- sudo-1.9.17/plugins/sudoers/sudoers.c 2025-06-12 12:12:38.000000000 -0500 |
删除了 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.https://www.stratascale.com/vulnerability-alert-CVE-2025-32463-sudo-chroot↩
- 2.https://codebrowser.dev/glibc/glibc/nss/nss_database.c.html#nss_database_check_reload_and_get↩
- 3.https://codebrowser.dev/glibc/glibc/nss/getXXent_r.c.html#122↩
- 4.https://github.com/bminor/glibc/commit/fa78feca47fdc226b46e7f6fea4c08c10fccd182↩
- 5.https://github.com/sudo-project/sudo/commit/fdafc2ceb36382b07e604c0f39903d56bef54016#diff-6a3fc5e12751032d02db8970967b688eab54525c326699010870b3ffca2b6541↩
- 6.https://github.com/sudo-project/sudo/commit/bc88e5cbd3b41196cac727855e2446a02dfba51e↩



