目录

1) 引言

我们收到了 Deepin 桌面环境一部分的 Deepin api-proxy D-Bus 服务的审查请求。在审查过程中,我们发现此 D-Bus 服务设计中存在一个严重的身份验证缺陷,允许本地用户以多种方式提升权限。

我们在十二月私下将此问题报告给了 Deepin 安全团队,但一个月未收到回复。在我们准备发布时,上游重新活跃起来并迅速发布了一个错误修复,但遗憾的是,该修复仍不完整。

本报告基于 dde-api-proxy 版本 1.0.17。这些发现仍然适用于 1.0.18 版本。上游已尝试在 1.0.19 版本中修复这些问题,但如第 6 节所述,修复效果不佳。

2) 身份验证绕过问题

Dde-api-proxy 以 root 用户身份运行,并在 D-Bus 系统总线上提供各种 D-Bus 服务。它之所以突出,是因为它提供了大量的 D-Bus 配置文件,但代码却很少。其原因是该服务仅在客户端和实际的 Deepin D-Bus 服务之间转发 D-Bus 请求。我们认为这是为了向后兼容,因为 Deepin D-Bus 接口名称发生了变化,尽管 该组件的 GitHub 存储库对其目的的洞察很少。

在启动期间,代理服务会主动注册请求的旧版 D-Bus 接口,并与实际的 Deepin D-Bus 服务建立连接,消息将转发到该服务。当客户端向其中一个旧版服务名称发送消息时,代理会同步地转发消息(参见 handleMessage())通过其现有连接,并将回复返回给客户端。

然而,这种相当直接的 D-Bus 消息代理方法存在一个严重的严重安全漏洞

  • 代理以 root 用户身份运行。
  • 代理会将任意本地用户的消息转发到实际的 D-Bus 服务,而没有任何身份验证要求。
  • 实际的 D-Bus 服务不知道代理情况,它们认为 root 用户正在要求它们执行操作。

因此,借助 dde-api-proxy,通常非 root 用户无法访问的旧版 D-Bus 方法将变得可以访问,且无需身份验证。

3) 复现步骤

无 Polkit 的 D-Bus 方法

以下是基于 Deepin Grub2 服务的问题的简单演示。该服务仅检查 D-Bus 客户端的 UID 以进行特权操作的身份验证。在下面显示的第一个命令中,使用了实际的 D-Bus 服务名称,并且服务拒绝了该操作,因为调用者没有特权。

user$ gdbus call -y -d org.deepin.dde.Grub2 \
    -o /org/deepin/dde/Grub2 -m org.deepin.dde.Grub2.SetTimeout 100
Error: GDBus.Error:org.deepin.dde.DBus.Error.Unnamed: not allow :1.167 call this method

在下一个命令中,使用了旧版 D-Bus 服务名称,这次目标服务执行了该操作,因为它认为请求源于特权的 UID 0 客户端(dde-api-proxy)。

user$ gdbus call -y  -d com.deepin.daemon.Grub2 \
    -o /com/deepin/daemon/Grub2 -m com.deepin.daemon.Grub2.SetTimeout 10
()

使用 Polkit 的 D-Bus 方法

在之前的示例中,Polkit 身份验证未被涉及。当 Polkit 身份验证被涉及时,调用者被视为“管理员”,从而导致类似的权限提升。我们使用 Polkit 在 Deepin accounts 服务中找到了一个合适的例子。该服务提供了大量的系统操作,其中包括将用户添加到组的可能性。它检查“org.deepin.dde.accounts.user-administration” Polkit 操作的授权,该操作默认情况下仅允许本地会话中的用户提供管理员凭据。

以下 gdbus 调用尝试将 UID 为 1000 的非特权用户添加到 root 组。该调用仅在此方式下运行,当它从(Deepin)图形会话中执行时才能成功。此处调用了实际的 accounts 服务接口,因此操作失败(需要输入 root 密码)。

user$ gdbus call -y -d org.deepin.dde.Accounts1 -o /org/deepin/dde/Accounts1/User1000 \
    -m org.deepin.dde.Accounts1.User.AddGroup root
Error: GDBus.Error:org.deepin.dde.DBus.Error.Unnamed: Policykit authentication failed

切换到 dde-api-proxy 提供的旧版 accounts 服务接口时,操作成功,无需任何身份验证请求,因为 accounts 服务再次认为 root 用户(UID 0)是请求此操作的客户端。

user$ gdbus call -y -d com.deepin.daemon.Accounts -o /com/deepin/daemon/Accounts/User1000 \
    -m com.deepin.daemon.Accounts.User.AddGroup root
()

4) 受影响的 D-Bus 接口

我们并未深入研究 dde-api-proxy 使未经身份验证的本地用户可用的所有特权 D-Bus 方法。在某些代理接口上,只允许调用特定的一组“过滤方法”。然而,其他接口对调用方法没有限制。初步看来,dde-api-proxy 提供的以下 D-Bus 接口似乎存在有趣的攻击面。

  • Accounts 服务(无方法过滤列表)
  • 网络代理设置(无方法过滤列表)
  • PasswdConf1 WriteConfig 方法
  • Lastore 服务(Apt 后端,无方法过滤列表)
  • Lastore manager 安装包方法

5) 建议的错误修复

身份验证绕过深植于 dde-api-proxy 的设计中,因此修复它很困难。在以下子部分中,提出了解决此问题的可能方法。

a) 降低权限

代理可以暂时将特权降级到非特权调用者的凭据,创建一个新的 D-Bus 连接并消息转发到正确的服务。如果 D-Bus 服务正在使用 Polkit 进行身份验证,这仍然无法正常工作,因为 Polkit 会区分调用者是否处于活动会话中。然而,D-Bus 代理服务永远不会处于会话中。这意味着身份验证要求可能会比必要的更严格。至少这种方法比目前的情况更安全。

b) 重新实现身份验证检查

代理可以自行实现所有必要的身份验证检查。这将导致执行正确的身份验证,但会导致大量代码的重复。它还会带来代理服务和实际服务的身份验证要求可能不同步的危险。

c) 在受影响的服务中实现旧版接口

最后,可以完全放弃 dde-api-proxy,并将向后兼容性实现在每个受影响的服务中。当然,这比 dde-api-proxy 试图实现的目标更不通用。

6) 上游错误修复

在长时间的沉默后,上游出人意料地回复了我们的报告,并建立了一个短暂的禁运期,直到 1 月 17 日发布了一个错误修复。该错误修复已在 1 月 17 日发布的上游 提交 95b50dd 中,并已包含在 1.0.19 版本中。

为了修复错误,上游采取了我们第 5.b) 节概述的建议方向,即在代理服务中实现冗余的 Polkit 授权检查。现在源代码中维护了一个代理提供的敏感 D-Bus 方法列表。所有这些方法都受一个新引入的 Polkit 操作“org.deepin.dde.api.proxy”保护,该操作需要管理员身份验证。

但是,此错误修复引入了一个新问题。Polkit 授权检查的实现方式如下:

bool checkAuthorization(const QString &actionId, const QString &service,const QDBusConnection &connection) const
{
    auto pid = connection.interface()->servicePid(service).value();
    auto authority = PolkitQt1::Authority::instance();
    auto result = authority->checkAuthorizationSync(actionId,
                                                    PolkitQt1::UnixProcessSubject(pid),
                                                    PolkitQt1::Authority::AllowUserInteraction);
    /* snip */
}

此代码将客户端的进程 ID (PID) 转发给 Polkit 服务进行身份验证。这种使用 Polkit UnixProcessSubject 的方式已被弃用很长时间,因为它容易受到竞争条件的影响,从而可以绕过此类授权检查。此问题已在 2013 年由前 SUSE 安全工程师 Sebastian Krahmer 发现,并被分配了 CVE-2013-4288。

上游在发布前未与我们分享错误修复,因此我们未能阻止此不完整的错误修复。通过切换到 SystemBusName 主题进行身份验证,应该可以修正此不完整的错误修复。

即使进行了改进的修复,我们仍认为此方法并非理想,因为它需要上游维护所有代理的方法。如果以后添加新方法,安全问题可能会再次悄然出现。此外,新引入的 Polkit 操作使得代理服务透明度降低,并可能影响用户体验。对于各个 D-Bus 方法的身份验证要求不再有精细的控制,所有这些代理 D-Bus 方法的身份验证消息都是通用的,对最终用户无益。

7) 可能的临时解决方案

除了从系统中移除 dde-api-proxy 之外,我们看不到任何可行的临时解决方案。

8) CVE 分配

这一发现是 dde-api-proxy 中一个较大的设计问题,它允许本地 root(组)利用以及更多类似的攻击向量。我们决定请求 Mitre 分配一个 CVE,以提高社区对该问题的认识。Mitre 为跟踪此问题分配了 CVE-2025-23222。

从形式上看,还需要为第 6 节所述的不完整错误修复引入的新安全问题分配另一个 CVE,但我们目前未这样做。

9) 时间线

2024-12-18 我们通过电子邮件将问题报告给了 security@deepin.com,这在项目的联系页面上有记录。电子邮件被邮件服务器拒收。
2024-12-18 我们联系了 support@deepin.org,询问报告 Deepin 安全问题的正确方法。我们很快得到了回复,他们指向了他们的(公开)bug 跟踪器或 security@deepin.org。
2024-12-19 我们通过电子邮件将问题报告给了 security@deepin.org,并提议协同披露。这次电子邮件未被拒收。
2025-01-07 由于尚未收到 Deepin 安全团队的回复,我们又发了一封邮件,要求在 2025-01-12 之前得到初步回复,否则我们将公布信息。
2025-01-13 由于仍未收到回复,我们开始着手发布完整报告。我们向 Mitre 申请了 CVE。
2025-01-14 Mitre 分配了 CVE-2025-23222。
2025-01-16 一位上游联系人出人意料地回复并确认了该问题,并表示他们正在处理错误修复。我们再次询问是否需要协同披露,并转达了已分配的 CVE。
2025-01-17 上游回复表示,他们希望将禁运期延长至 2025-01-20。
2025-01-23 由于在发布日期时上游没有任何活动,并且我们未收到通知,我们询问上游发布是否按计划进行。
2025-01-24 上游将错误修复指向了我们,该修复已于 2025-01-17 发布,没有进一步的沟通。
2025-01-24 由于错误修复已发布,我们决定公布所有信息。在审查错误修复时,我们发现它不完整,并通过电子邮件通知了上游。

10) 参考资料