# CVE-2024-5290&CVE-2024-35235漏洞链本地提权分析 ## 漏洞原理 ### CVE-2024-35235 在设置配置文件`/etc/cups/cupsd.conf` 的 Listen 参数中配置的 unix 套接字的绑定时,代码执行 chmod 调用之前没有检查是否成功调用 unlink 和 bind: ```c unlink(addr->un.sun_path); // Save the current umask and set it to 0 so that all users can access // the domain socket... mask = umask(0); // Bind the domain socket... status = bind(fd, (struct sockaddr *)addr, (socklen_t)httpAddrLength(addr)); // Restore the umask and fix permissions... umask(mask); chmod(addr->un.sun_path, 0140777); ``` 在 Ubuntu 24.04 上,通过将 Listen 参数设置为 `/tmp/stage/cupsd.conf` 之类的路径(其中 `cupsd.conf` 是配置文件`/etc/cups/cupsd.conf`的符号链接),先前对该路径的unlink调用将因 AppArmor [4]而失败,并且由于文件仍然存在,后续的 bind 调用也将失败。在调用 chmod 之前不会检查 bind 调用的返回值,因此仍然可以将bind导入失败的符号链接通过chmod进行权限修改,并且文件权限将更改为所有用户组可读可写可执行。 ### CVE-2024-5290 由于`wpa_supplicant`允许加载任意路径下的链接库文件,这使得本地非特权攻击者可以提升权限至以 `wpa_supplicant` 身份运行的用户(通常是 root)。攻击者可以通过调用`CreateInterface`dbus接口,将伪造`wpa_conf` 的文件导入,配置文件中的 `opensc_engine_path`参数为恶意的链接库文件,当`wpa_supplicant`加载链接库文件时导致任意代码执行从而完成权限提升。 ## 利用过程 1. 在 `/tmp/stage` 中为将使用 cups 定位的符号链接创建一个临时文件夹。 2. 调用 `ServerSetSettings` DBus 方法将 cups Listen 参数更改为 `/tmp/stage/cupsd.conf`,它是 `/etc/cups/cupsd.conf` 的符号链接。 3. 文件 `/etc/cups/cupsd.conf` 现在可供所有用户组写入。修复 `ServerSetSettings` 所做的更改,并添加指向 `/tmp/stage/cups-files.conf` 的附加 Listen 配置项,它是 `/etc/cups/cups-files.conf` 的符号链接。 4. 使用 `ServerSetSettings` 更改 `IdleExitTimeout` 参数。该参数值任意,主要用于使用它来触发 cups 重启,这是一个特权操作。 5. 文件 `/etc/cups/cups-files.conf` 现在可供所有用户组写入。修改此文件以将 Group 参数设置为 `netdev`。实际上不需要为利用链设置 User 参数。 6. 重复步骤4,触发cups重新启动,使`cups-files.conf`中的Group参数生效。 7. 通过创建一个配置文件来定义指向共享对象的 `opensc_engine_path`,并创建一个 Python 脚本来调用 `wpa_supplicant` 的 `CreateInterface` 方法,为 后续利用做准备。由于此方法所需的数据类型,使用 Python 比使用 `dbus-send` 更容易。 8. 使用 `PrinterAddWithPpdFile` 的 DBus 方法将新打印机安装到 cups 中。此处传递的 PPD 文件将定义 `FoomaticRIPCommandLine` 参数以执行步骤 7 中创建的 Python 脚本。(还需要调用`PrinterSetEnabled`、`PrinterSetAcceptJobs`,用于启用打印机,设置为接收任务的状态) 9. 使用新创建的打印机打印示例 PDF 文件(`lp -d FAKEPRINTER /usr/share/cups/data/testprint`)。这将触发 Python 脚本,该脚本将调用 `CreateInterface` 方法,这将导致 `wpa_supplicant` 加载我们在配置文件中定义的共享对象。此共享对象将在加载后立即将 Python 设置为 setuid root(原博客设置的是python,这里设置的程序随意只要能后续提权即可)。 10. 恢复之前修改的配置文件环境,重启cups服务 11. 最后,向用户提供一个交互式的root shell来完成概念验证。(`find /usr/bin/su -exec /bin/bash -p \; -quit`) ## 其他 ### 复现环境相关 在准备Ubuntu 24.04 的环境时发现,Ubuntu官方已经将该漏洞在镜像内修补,并覆盖了原来有漏洞的镜像文件。 这里可以用objdump检查cups中的漏洞点是否存在,其中0xc1ff是`chmod(addr->un.sun_path, 0140777);`中的mode值: ```bash Ubuntu 23.04: test@test-VMware-Virtual-Platform:~$ objdump -d /lib/x86_64-linux-gnu/libcups.so.2 | grep -A 5 -B 5 "0xc1ff" 2feab: 89 c2 mov %eax,%edx 2fead: e8 be db fe ff call 1da70 2feb2: 44 89 e7 mov %r12d,%edi 2feb5: 89 c5 mov %eax,%ebp 2feb7: e8 24 dd fe ff call 1dbe0 2febc: be ff c1 00 00 mov $0xc1ff,%esi 2fec1: 4c 89 ef mov %r13,%rdi 2fec4: e8 d7 cf fe ff call 1cea0 2fec9: e9 56 ff ff ff jmp 2fe24 2fece: 66 90 xchg %ax,%ax 2fed0: 41 b8 04 00 00 00 mov $0x4,%r8d Ubuntu 24.04.1: test@test-VMware-Virtual-Platform:~$ objdump -d /lib/x86_64-linux-gnu/libcups.so.2 | grep -A 5 -B 5 "0xc1ff" test@test-VMware-Virtual-Platform:~$ ``` ### polkit如何判断会话是本地会话 首先来看一下在本地和ssh会话分别调用cupspkhelper接口polkit的debug日志: ``` local: ** (polkitd:2129): DEBUG: 10:03:00.854: system-bus-name::1.247 is inquiring whether system-bus-name::1.246 is authorized for org.opensuse.cupspkhelper.mechanism.server-settings ** (polkitd:2129): DEBUG: 10:03:00.854: user of caller is unix-user:cups-pk-helper ** (polkitd:2129): DEBUG: 10:03:00.855: user of subject is unix-user:test ** (polkitd:2129): DEBUG: 10:03:00.855: checking whether system-bus-name::1.246 is authorized for org.opensuse.cupspkhelper.mechanism.server-settings ** (polkitd:2129): DEBUG: 10:03:00.855: 0x5e6596310470 ** (polkitd:2129): DEBUG: 10:03:00.855: Checking whether session 133 is active. ** (polkitd:2129): DEBUG: 10:03:00.855: Session 133 has UID 1000. ** (polkitd:2129): DEBUG: 10:03:00.855: UID 1000 has state active. ** (polkitd:2129): DEBUG: 10:03:00.855: subject is in session 133 (local=1 active=1) ** (polkitd:2129): DEBUG: 10:03:00.856: is authorized (has implicit authorization local=1 active=1) ssh: ** (polkitd:2129): DEBUG: 10:30:12.841: system-bus-name::1.261 is inquiring whether system-bus-name::1.260 is authorized for org.opensuse.cupspkhelper.mechanism.server-settings ** (polkitd:2129): DEBUG: 10:30:12.843: user of caller is unix-user:cups-pk-helper ** (polkitd:2129): DEBUG: 10:30:12.844: user of subject is unix-user:test ** (polkitd:2129): DEBUG: 10:30:12.844: checking whether system-bus-name::1.260 is authorized for org.opensuse.cupspkhelper.mechanism.server-settings ** (polkitd:2129): DEBUG: 10:30:12.846: 0x5e65961265c0 ** (polkitd:2129): DEBUG: 10:30:12.846: Checking whether session 2 is active. ** (polkitd:2129): DEBUG: 10:30:12.846: Session 2 has UID 1000. ** (polkitd:2129): DEBUG: 10:30:12.846: UID 1000 has state active. ** (polkitd:2129): DEBUG: 10:30:12.846: subject is in session 2 (local=0 active=1) ** (polkitd:2129): DEBUG: 10:30:12.848: challenge (implicit_authorization = auth_admin) ``` 根据日志输出信息可以从源码中找到检查会话是否是本地的函数: ```c //polkit/src/polkitbackend/polkitbackendinteractiveauthority.c static PolkitAuthorizationResult * check_authorization_sync (PolkitBackendAuthority *authority, PolkitSubject *caller, PolkitSubject *subject, const gchar *action_id, PolkitDetails *details, PolkitCheckAuthorizationFlags flags, PolkitImplicitAuthorization *out_implicit_authorization, gboolean checking_imply, GError **error) { ... g_debug (" %p", session_for_subject); if (session_for_subject != NULL) { session_is_local = polkit_backend_session_monitor_is_session_local (priv->session_monitor, session_for_subject);// check whether session is local session_is_active = polkit_backend_session_monitor_is_session_active (priv->session_monitor, session_for_subject); g_debug (" subject is in session %s (local=%d active=%d)", polkit_unix_session_get_session_id (POLKIT_UNIX_SESSION (session_for_subject)), session_is_local, session_is_active); } ... //polkit/src/polkitbackend/polkitbackendsessionmonitor-systemd.c gboolean polkit_backend_session_monitor_is_session_local (PolkitBackendSessionMonitor *monitor, PolkitSubject *session) { char *seat; if (!sd_session_get_seat (polkit_unix_session_get_session_id (POLKIT_UNIX_SESSION (session)), &seat)) { free (seat); return TRUE; } return FALSE; } ``` 最终是调用`sd_session_get_seat`函数查询当前会话的seat值。seat指的是用户的设备资源集合。在传统的桌面环境中,可能是指一台物理电脑的键盘、鼠标和显示器。现代的 Linux 桌面管理器(如 GNOME、KDE 等)在管理用户会话时会通过 seat 来区分不同的物理设备和用户交互的上下文。 关于seat的概念可以参考:https://www.freedesktop.org/wiki/Software/systemd/multiseat/ 上面链接里面需要注意这段话: > A session is bound to one or no seats (the latter for 'virtual' ssh logins). 这就是判断是否是本地会话的基本原理。 debug日志中本地会话值是133,可以通过loginctl命令查看会话信息,可见seat值存在为`seat0`: ```bash test@test-VMware-Virtual-Platform:~$ loginctl show-session 133 Id=133 User=1000 Name=test Timestamp=Wed 2024-12-11 10:01:11 CST TimestampMonotonic=59793518935 VTNr=2 Seat=seat0 TTY=tty2 Remote=no Service=gdm-password Scope=session-133.scope Leader=6537 Audit=133 Type=wayland Class=user Active=yes State=active IdleHint=yes IdleSinceHint=1733882497709616 IdleSinceHintMonotonic=59819475875 LockedHint=yes ``` 而ssh会话的信息如下,可见没有seat值: ```bash test@test-VMware-Virtual-Platform:~$ cat /run/systemd/sessions/2 # This is private data. Do not parse. UID=1000 USER=test ACTIVE=1 IS_DISPLAY=1 STATE=active REMOTE=1 TYPE=tty ORIGINAL_TYPE=tty CLASS=user SCOPE=session-2.scope FIFO=/run/systemd/sessions/2.ref REMOTE_HOST=192.168.202.1 SERVICE=sshd POSITION=0 LEADER=1812 AUDIT=2 REALTIME=1733822761845904 MONOTONIC=83612163 ``` 另外查看session信息也可以直接查看 `/run/systemd/sessions/{SESSION_ID}`和loginctl命令输出几乎一样。 ### 绕过polkit本地会话检测 绕过方法是我邮件给文章作者请教23.04和24.04版本差异导致的一些问题,但文章作者在回复里直接给出了在ssh会话中绕过polkit检测调用目标接口的方法: > When creating the proof of concept I was also connecting via SSH, and I was able to get around this restriction in some cases by creating a .desktop file in ~/.local/share/applications and launching it with gtk-launch. This opens the application (the payload shell script) on the desktop and will effectively bypass the subject.active and subject.local checks. 伪造的desktop文件需要注意`Terminal`参数要为true,这样launcher才会在图形界面环境新启动一个terminal运行命令: ```bash test@test-VMware-Virtual-Platform:~$ cat ~/.local/share/applications/python3.12.desktop [Desktop Entry] Name=Python (v3.12) Comment=Python Interpreter (v3.12) Exec=/usr/bin/python3.12 /tmp/test.py Icon=/usr/share/pixmaps/python3.12.xpm Terminal=true Type=Application Categories=Development; StartupNotify=true NoDisplay=true test@test-VMware-Virtual-Platform:~$ gtk-launch python3.12 ``` destop文件格式可以参考:https://wiki.archlinux.org/title/Desktop_entries 链接里面还提到了不同桌面环境的不同launcher,均可以实现上述需求。例如Ubuntu中还可以使用gio: ```bash test@test-VMware-Virtual-Platform:~$ gio launch ~/.local/share/applications/vim.desktop ``` 但这个绕过方法有一个限制,就是用户必须要在图形界面认证登录一次(登录后锁屏也无所谓但必须登录成功一次),如果只在ssh会话登录后调用会报如下错误: ``` # Error constructing proxy for org.gnome.Terminal:/org/gnome/Terminal/Factory0: Error calling StartServiceByName for org.gnome.Terminal: Timeout was reached ``` ## 参考 [1] https://snyk.io/blog/abusing-ubuntu-root-privilege-escalation/ [2] https://bugs.launchpad.net/ubuntu/+source/wpa/+bug/2067613 [3] https://github.com/OpenPrinting/cups/security/advisories/GHSA-vvwp-mv6j-hw6f [4] https://git.launchpad.net/ubuntu/+source/apparmor/tree/profiles/apparmor.d/abstractions/user-tmp#n21 [5] https://github.com/OpenPrinting/cups/commit/a436956f374b0fd7f5da9df482e4f5840fa1c0d2#diff-7ad1a90873fca54ae10c73133619fc7e4d218c06d9792d4c27648761375f2d06L240 [6] https://launchpadlibrarian.net/742113586/lib_engine_trusted_path.patch